The appointments page only ever showed one doctor's row, but in the resource-first model a single appointment can hold a room and a device at the same time, and that — not the doctor's schedule — is what runs the capacity out. An hour could look free on the doctor's lane while the only alexandrite laser was already taken. A third view, "منابع", draws one lane per resource for the selected day. Blocks come from resource_occupancy rather than the appointment: that range includes the device's setup and cleanup minutes and is the same range the availability engine treats as busy. A multi-segment appointment therefore shows up on every resource it holds, and each block links to the appointment it belongs to. GET /api/v1/resources/timeline keeps a fixed query count — one for occupancy, one for shifts, one for the patient names — instead of one per resource. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
162 lines
6.7 KiB
PHP
162 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Resource;
|
|
|
|
use App\Appointment\Availability\Entity\ResourceOccupancy;
|
|
use App\Auth\Entity\User;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Doctor\Entity\DoctorAddress;
|
|
use App\Resource\Entity\ClinicResource;
|
|
use App\Resource\Entity\ResourceCalendar;
|
|
use App\Resource\Entity\ResourceType;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* تایملاین منابع در صفحهٔ نوبتها: «کدام دستگاه/اتاق کِی گرفته است».
|
|
*
|
|
* ردیفها از `resource_occupancy` میآیند نه از خودِ نوبت، چون بازهٔ اشغال شاملِ
|
|
* آمادهسازی و تمیزکاری هم هست و همان چیزی است که ظرفیت را تمام میکند.
|
|
*/
|
|
class ResourceTimelineEndpointTest extends ApiTestCase
|
|
{
|
|
/** @return array{0: User, 1: ClinicResource} */
|
|
private function clinicWithResource(): array
|
|
{
|
|
$user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
|
$clinic = new Clinic($user);
|
|
$clinic->setName('کلینیک تایملاین');
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
$address = DoctorAddress::forClinic($clinic->getId());
|
|
$address->setName('شعبهٔ مرکزی');
|
|
$this->em->persist($address);
|
|
$this->em->flush();
|
|
|
|
$type = new ResourceType('clinic', (int) $clinic->getId(), 'device', 'دستگاه لیزر');
|
|
$this->em->persist($type);
|
|
$this->em->flush();
|
|
|
|
$resource = new ClinicResource($address, $type, 'لیزر دایود');
|
|
$this->em->persist($resource);
|
|
$this->em->flush();
|
|
|
|
return [$user, $resource];
|
|
}
|
|
|
|
private function occupy(ClinicResource $resource, int $start, int $end, string $segment): void
|
|
{
|
|
$occupancy = new ResourceOccupancy($resource, $start, $end);
|
|
$occupancy->setSegmentName($segment);
|
|
$this->em->persist($occupancy);
|
|
$this->em->flush();
|
|
}
|
|
|
|
private function shift(ClinicResource $resource, int $day, int $from, int $to): void
|
|
{
|
|
$this->em->persist(new ResourceCalendar($resource, $day, $from, $to));
|
|
$this->em->flush();
|
|
}
|
|
|
|
/** ۰ = شنبه، همان قرارداد تقویم منبع. */
|
|
private function dayOfWeek(int $timestamp): int
|
|
{
|
|
return (int) (((int) date('w', $timestamp) + 1) % 7);
|
|
}
|
|
|
|
// ── ✅ موفق ──────────────────────────────────────────────────────────────
|
|
|
|
public function testTheDayShowsEachResourceWithItsOccupiedRanges(): void
|
|
{
|
|
[$user, $resource] = $this->clinicWithResource();
|
|
|
|
$midnight = (int) strtotime('today midnight');
|
|
$this->shift($resource, $this->dayOfWeek($midnight), 540, 1020);
|
|
$this->occupy($resource, $midnight + 10 * 3600, $midnight + 11 * 3600, 'لیزر');
|
|
|
|
$body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame($midnight, $body['data']['date']);
|
|
|
|
$row = $body['data']['resources'][0];
|
|
self::assertSame('لیزر دایود', $row['name']);
|
|
self::assertSame('دستگاه لیزر', $row['type_name']);
|
|
self::assertSame([['start_minute' => 540, 'end_minute' => 1020]], $row['shifts']);
|
|
|
|
self::assertCount(1, $row['items']);
|
|
self::assertSame('لیزر', $row['items'][0]['segment_name']);
|
|
self::assertSame($midnight + 10 * 3600, $row['items'][0]['starts_at']);
|
|
}
|
|
|
|
public function testOnlyTheShiftsOfThatWeekdayComeBack(): void
|
|
{
|
|
[$user, $resource] = $this->clinicWithResource();
|
|
|
|
$midnight = (int) strtotime('today midnight');
|
|
$today = $this->dayOfWeek($midnight);
|
|
|
|
$this->shift($resource, $today, 540, 1020);
|
|
// شیفت روز دیگر نباید در تایملاین امروز ظاهر شود.
|
|
$this->shift($resource, ($today + 3) % 7, 60, 120);
|
|
|
|
$body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user);
|
|
|
|
self::assertSame([['start_minute' => 540, 'end_minute' => 1020]], $body['data']['resources'][0]['shifts']);
|
|
}
|
|
|
|
// ── ⚠️ مرزی ─────────────────────────────────────────────────────────────
|
|
|
|
public function testARestDayReturnsTheResourceWithNothingOnIt(): void
|
|
{
|
|
[$user, $resource] = $this->clinicWithResource();
|
|
|
|
$midnight = (int) strtotime('today midnight');
|
|
$this->occupy($resource, $midnight - 5 * 86400, $midnight - 5 * 86400 + 3600, 'هفتهٔ پیش');
|
|
|
|
$body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $user);
|
|
|
|
// منبع میماند تا ردیفش در تایملاین دیده شود؛ فقط خالی است.
|
|
self::assertCount(1, $body['data']['resources']);
|
|
self::assertSame([], $body['data']['resources'][0]['items']);
|
|
self::assertSame([], $body['data']['resources'][0]['shifts']);
|
|
}
|
|
|
|
public function testWithoutADateItFallsBackToToday(): void
|
|
{
|
|
[$user] = $this->clinicWithResource();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/resources/timeline', $user);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertSame((int) strtotime('today midnight'), $body['data']['date']);
|
|
}
|
|
|
|
// ── ❌ خطا ───────────────────────────────────────────────────────────────
|
|
|
|
public function testAMalformedDateIsRefused(): void
|
|
{
|
|
[$user] = $this->clinicWithResource();
|
|
|
|
$this->authJson('GET', '/api/v1/resources/timeline?date=05-08-2026', $user);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testAnotherEnvironmentDoesNotSeeTheseResources(): void
|
|
{
|
|
[, $resource] = $this->clinicWithResource();
|
|
|
|
$midnight = (int) strtotime('today midnight');
|
|
$this->occupy($resource, $midnight + 3600, $midnight + 7200, 'لیزر');
|
|
|
|
[$stranger] = $this->clinicWithResource();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/resources/timeline?date=' . date('Y-m-d', $midnight), $stranger);
|
|
|
|
// منبعِ خودش را میبیند، نه منبع کلینیک دیگر.
|
|
self::assertCount(1, $body['data']['resources']);
|
|
self::assertSame([], $body['data']['resources'][0]['items']);
|
|
}
|
|
}
|