Files
clinicpro/tests/Resource/ResourceTimelineEndpointTest.php
T
hamedandClaude Opus 5 444ebc897a feat(appointments): a resource-first view on the timeline
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>
2026-08-02 14:14:10 +03:30

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']);
}
}