Files
clinicpro/tests/Resource/ResourceFreeTimeCalculatorTest.php
T
hamedandClaude Opus 5 5e87bbc18b feat(appointments): resources share the doctors' timeline
Three views become two. The resource lanes were a separate tab, which meant
reading a doctor's free hour on one screen and the laser's on another and
matching them by eye — while in the resource-first model it is the device and
the room that decide whether that hour is really free. They now sit under the
same "زمانبندی" view, below the doctor's slots.

Each lane says how much of its shift is still free, and that number respects
capacity: a minute counts as busy only once the overlapping bookings reach
the resource's capacity, so a three-bed room with two appointments is still
open. Treating it otherwise would silently turn every multi-capacity resource
into a single-capacity one. ResourceFreeTimeCalculator does the sweep and
carries nine cases of its own.

only_bookable=1 keeps resources with no service offering out of the view;
they could only ever render an empty lane. On the seeded clinic that is five
resources down to two.

Two ruler defects the screenshot caught: hours rendered in Latin digits, and
the last label was half-clipped by the container so 21 read as 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:14:04 +03:30

151 lines
5.5 KiB
PHP

<?php
namespace App\Tests\Resource;
use App\Resource\Service\ResourceFreeTimeCalculator;
use PHPUnit\Framework\TestCase;
/**
* وقت آزاد با **ظرفیت** حساب می‌شود، نه با «پر/خالی».
*
* اتاق سه‌تخته‌ای که دو نوبت هم‌زمان دارد هنوز یک جا دارد؛ اگر آن ساعت پر شمرده شود،
* ظرفیت عملاً یک می‌شود و همان چیزی که مدل Resource-First برایش ساخته شده از بین می‌رود.
*/
class ResourceFreeTimeCalculatorTest extends TestCase
{
private const DAY = 1785875400; // نیمه‌شب یک روز نمونه
private ResourceFreeTimeCalculator $calc;
protected function setUp(): void
{
$this->calc = new ResourceFreeTimeCalculator();
}
/** @return array{starts_at: int, ends_at: int} */
private function at(int $fromMinute, int $toMinute): array
{
return [
'starts_at' => self::DAY + $fromMinute * 60,
'ends_at' => self::DAY + $toMinute * 60,
];
}
// ── ✅ موفق ──────────────────────────────────────────────────────────────
public function testAnEmptyDayIsEntirelyFree(): void
{
$load = $this->calc->forDay([['start_minute' => 540, 'end_minute' => 1020]], [], self::DAY, 1);
self::assertSame(480, $load['shift_minutes']);
self::assertSame(0, $load['busy_minutes']);
self::assertSame(480, $load['free_minutes']);
}
public function testASingleAppointmentOnACapacityOneResourceIsBusy(): void
{
$load = $this->calc->forDay(
[['start_minute' => 540, 'end_minute' => 1020]],
[$this->at(600, 660)],
self::DAY,
1,
);
self::assertSame(60, $load['busy_minutes']);
self::assertSame(420, $load['free_minutes']);
}
// ── ⚠️ مرزی ─────────────────────────────────────────────────────────────
/** همان حالتی که کل این محاسبه برایش نوشته شده. */
public function testCapacityThreeWithTwoOverlappingAppointmentsStillHasRoom(): void
{
$load = $this->calc->forDay(
[['start_minute' => 540, 'end_minute' => 1020]],
[$this->at(600, 660), $this->at(600, 660)],
self::DAY,
3,
);
self::assertSame(0, $load['busy_minutes'], 'دو نوبت هم‌زمان روی ظرفیت ۳ هیچ دقیقه‌ای را پر نمی‌کند');
self::assertSame(480, $load['free_minutes']);
}
public function testCapacityIsOnlyFullWhenEveryPlaceIsTaken(): void
{
$load = $this->calc->forDay(
[['start_minute' => 540, 'end_minute' => 1020]],
[$this->at(600, 660), $this->at(600, 660), $this->at(600, 630)],
self::DAY,
3,
);
// فقط ۱۰:۰۰ تا ۱۰:۳۰ هر سه جا گرفته است.
self::assertSame(30, $load['busy_minutes']);
self::assertSame(450, $load['free_minutes']);
}
/** اشغالِ بیرون از شیفت، وقتِ آزادِ داخل شیفت را کم نمی‌کند. */
public function testOccupancyOutsideTheShiftDoesNotCount(): void
{
$load = $this->calc->forDay(
[['start_minute' => 540, 'end_minute' => 1020]],
[$this->at(1100, 1200)],
self::DAY,
1,
);
self::assertSame(0, $load['busy_minutes']);
self::assertSame(480, $load['free_minutes']);
}
/** اشغالی که نیمی‌اش بیرون شیفت است، فقط نیمهٔ داخلش شمرده می‌شود. */
public function testOnlyTheOverlappingPartOfAnAppointmentCounts(): void
{
$load = $this->calc->forDay(
[['start_minute' => 540, 'end_minute' => 1020]],
[$this->at(480, 600)],
self::DAY,
1,
);
self::assertSame(60, $load['busy_minutes'], 'از ۰۸:۰۰ تا ۱۰:۰۰ فقط یک ساعتش داخل شیفت است');
}
public function testARestDayHasNoFreeTime(): void
{
$load = $this->calc->forDay([], [$this->at(600, 660)], self::DAY, 1);
self::assertSame(0, $load['shift_minutes']);
self::assertSame(0, $load['free_minutes']);
}
/** ظرفیت صفر یا منفی نباید تقسیم‌بر-صفرِ منطقی بسازد؛ کف ۱ است. */
public function testCapacityBelowOneIsTreatedAsOne(): void
{
$load = $this->calc->forDay(
[['start_minute' => 540, 'end_minute' => 600]],
[$this->at(540, 600)],
self::DAY,
0,
);
self::assertSame(60, $load['busy_minutes']);
}
/** دو شیفت در یک روز (صبح و عصر) هر دو شمرده می‌شوند. */
public function testTwoShiftsInADayAreBothCounted(): void
{
$load = $this->calc->forDay(
[['start_minute' => 540, 'end_minute' => 720], ['start_minute' => 960, 'end_minute' => 1200]],
[$this->at(600, 660)],
self::DAY,
1,
);
self::assertSame(420, $load['shift_minutes']);
self::assertSame(60, $load['busy_minutes']);
self::assertSame(360, $load['free_minutes']);
}
}