feat(treatment): project a whole course's calendar without storing it
TreatmentScheduler deliberately writes only the next session's due_at, because a date not yet anchored to anything real is a false claim about the future and has to be rewritten every time a patient runs late. But the panel still needs to show the whole course. TreatmentPlanProjector builds that chain at display time and writes nothing. Each date carries is_estimate so a projection is never mistaken for a fact. A session's anchor is, in order: when it was finished, when its appointment is, or its written due_at; a case with none falls back to when it was opened, so a course that has not been booked yet still shows dates instead of blanks. Read and write stay in separate classes — mixing them risks storing an estimate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Treatment\Service;
|
||||
|
||||
use App\Treatment\Entity\TreatmentCase;
|
||||
use App\Treatment\Entity\TreatmentProtocolStep;
|
||||
use App\Treatment\Entity\TreatmentSession;
|
||||
|
||||
/**
|
||||
* تقویمِ کل یک دوره — نمای مشتق، نه دادهٔ ذخیرهشده.
|
||||
*
|
||||
* `TreatmentScheduler` عمداً فقط سررسید جلسهٔ **بعدی** را مینویسد و بقیه `null`
|
||||
* میمانند: عددی که هنوز به هیچ واقعیتی گره نخورده، ذخیرهکردنش یک ادعای غلط دربارهٔ
|
||||
* آینده است و با هر تأخیر بیمار باید کل زنجیره بازنویسی شود.
|
||||
*
|
||||
* ولی پنل باید کل دوره را نشان بدهد. پس اینجا همان زنجیره **در لحظهٔ نمایش** ساخته
|
||||
* میشود و هیچچیز نوشته نمیشود. هر تاریخ برچسب `is_estimate` دارد تا تخمین با
|
||||
* واقعیت اشتباه گرفته نشود.
|
||||
*
|
||||
* این کلاس فقط میخواند؛ `TreatmentScheduler` فقط مینویسد. قاطی کردنشان یعنی یک کلاس
|
||||
* با دو مسئولیت و ریسک اینکه تخمین اشتباهی ذخیره شود.
|
||||
*/
|
||||
final class TreatmentPlanProjector
|
||||
{
|
||||
private const DAY = 86400;
|
||||
|
||||
/**
|
||||
* @return list<array{session: TreatmentSession, planned_at: ?int, is_estimate: bool}>
|
||||
*/
|
||||
public function project(TreatmentCase $case): array
|
||||
{
|
||||
$sessions = $case->getSessions()->toArray();
|
||||
usort($sessions, static fn (TreatmentSession $a, TreatmentSession $b): int
|
||||
=> $a->getSessionNumber() <=> $b->getSessionNumber());
|
||||
|
||||
$offsets = $this->offsets($case);
|
||||
|
||||
// لنگر = آخرین زمانِ قطعیِ دیدهشده. تا وقتی هیچ جلسهای قطعی نشده، خودِ
|
||||
// بازشدنِ پرونده لنگر است — وگرنه دورهای که هنوز نوبت نگرفته هیچ تاریخی
|
||||
// نشان نمیدهد و کارتها خالی میآیند.
|
||||
$anchor = $case->getOpenedAt();
|
||||
$out = [];
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
$actual = $this->actualTimeOf($session);
|
||||
|
||||
if ($actual !== null) {
|
||||
$anchor = $actual;
|
||||
$out[] = ['session' => $session, 'planned_at' => $actual, 'is_estimate' => false];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$anchor += ($offsets[$session->getSessionNumber()] ?? 0) * self::DAY;
|
||||
$out[] = ['session' => $session, 'planned_at' => $anchor, 'is_estimate' => true];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* زمانِ قطعیِ یک جلسه، یا `null` اگر هنوز به هیچ واقعیتی گره نخورده.
|
||||
*
|
||||
* ترتیب عمدی است: انجامشده > نوبتگرفته > سررسیدِ نوشتهشده. جلسهای که انجام شده
|
||||
* زمان واقعی دارد و نوبتش دیگر مهم نیست.
|
||||
*/
|
||||
private function actualTimeOf(TreatmentSession $session): ?int
|
||||
{
|
||||
return $session->getFinishedAt()
|
||||
?? $session->getAppointment()?->getSlotStart()
|
||||
?? $session->getDueAt();
|
||||
}
|
||||
|
||||
/**
|
||||
* فاصلهٔ هر گام از گام قبلی، طبق پروتکلِ همان پرونده.
|
||||
*
|
||||
* @return array<int, int> شمارهٔ گام => روز
|
||||
*/
|
||||
private function offsets(TreatmentCase $case): array
|
||||
{
|
||||
$out = [];
|
||||
|
||||
foreach ($case->getProtocol()->getSteps() as $step) {
|
||||
/** @var TreatmentProtocolStep $step */
|
||||
$out[$step->getStepNumber()] = $step->getOffsetDays();
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user