- Add implementation notes for cancellation and waitlist features. - Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting. - Establish architecture for domain events and outbox pattern to ensure reliable event publishing. - Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports. - Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
8.6 KiB
معماری — تسک ۱۲
ساختار فایل
src/Course/
├── Entity/
│ ├── CourseProtocol.php
│ ├── CourseProtocolStep.php # پارامتر هر جلسه
│ ├── TreatmentCourse.php
│ └── CourseSession.php
├── Service/
│ ├── CourseStarter.php # شروع دوره از پروتکل
│ ├── CourseScheduler.php # رزرو یکجا + پیشنهاد جلسهٔ بعدی
│ ├── CourseProgressCalculator.php
│ └── CourseSessionLinker.php # اتصال نوبت ↔ جلسهٔ دوره
├── Controller/{CourseProtocolController, TreatmentCourseController}.php
└── Repository/…
CourseProtocol و CourseProtocolStep
class CourseProtocol
{
use TenantOwnedTrait;
private ServiceItem $service;
private int $sessionCount; // ۸
private int $minDays; // ۲۱
private int $idealDays; // ۲۸
private int $maxDays; // ۴۵
private bool $preferSameResource = true;
private Collection $steps; // CourseProtocolStep
}
class CourseProtocolStep
{
private int $sessionNumber; // ۱..۸
private array $params = []; // {"energy": 12} — اسکالر، فهرست آزاد
private ?int $overrideDurationMinutes = null; // جلسهٔ اول طولانیتر است
}
params آزاد است چون هر تخصص پارامتر خودش را دارد (سطح انرژی، ضخامت، دوز). ولی مثل
ClinicResource.attributes فقط اسکالر — و هیچ منطقی به مقدارش وابسته نیست، فقط نمایش و
ثبت میشود.
minDays <= idealDays <= maxDays قید اجباری.
TreatmentCourse و CourseSession
class TreatmentCourse
{
use TenantOwnedTrait;
public const STATUS_ACTIVE = 'active';
public const STATUS_COMPLETED = 'completed';
public const STATUS_ABANDONED = 'abandoned';
private PatientRecord $patient;
private ServiceItem $service;
private CourseProtocol $protocol;
// ── snapshot پروتکل در لحظهٔ شروع (قانون پنجم مستند) ──
private int $sessionCount;
private int $minDays;
private int $idealDays;
private int $maxDays;
private ?PatientPackage $package = null; // تسک ۱۱ — اختیاری
private ?ClinicResource $preferredResource = null; // منبع جلسهٔ اول
private string $status = self::STATUS_ACTIVE;
private int $startedAt;
}
class CourseSession
{
public const STATUS_PLANNED = 'planned';
public const STATUS_BOOKED = 'booked';
public const STATUS_COMPLETED = 'completed';
public const STATUS_SKIPPED = 'skipped';
private TreatmentCourse $course;
private int $sessionNumber;
private array $params = []; // snapshot از CourseProtocolStep
private ?Appointment $appointment = null;
private string $status = self::STATUS_PLANNED;
private ?int $completedAt = null;
}
چهار فیلد فاصله و params کپی میشوند نه FK: تغییر پروتکل فردا نباید دورهٔ در جریان
را عوض کند. این همان تصمیمی است که در appointment_segments و patient_packages گرفته شد.
CourseScheduler — رزرو یکجا
public function bookAll(TreatmentCourse $course): BookAllResult
{
return $this->em->wrapInTransaction(function () use ($course) {
$anchor = $this->lastCompletedAt($course) ?? time();
$planned = $course->plannedSessions(); // مرتب بر اساس sessionNumber
$holds = [];
foreach ($planned as $session) {
$target = $anchor + $course->getIdealDays() * 86400;
if ($target > time() + 90 * 86400) {
// بیرون از بازهٔ مجاز جستجو — این و بقیه planned میمانند
break;
}
$slot = $this->findNearestInRange(
$course, $session,
min: $anchor + $course->getMinDays() * 86400,
ideal: $target,
max: $anchor + $course->getMaxDays() * 86400,
);
if ($slot === null) {
throw new AppException(ErrorCodes::ERR_VALIDATION_001, sprintf(
'برای جلسهٔ %d هیچ وقت مناسبی در بازهٔ مجاز پیدا نشد', $session->getSessionNumber()
), 422);
}
$holds[] = $this->holdService->hold($this->holdRequestFor($course, $session, $slot));
$anchor = $slot->start; // ← لنگر جلسهٔ بعدی، همین جلسه
}
foreach ($holds as $hold) { $this->bookingService->confirm($hold->uuid, $course->owner()); }
return new BookAllResult(count($holds), count($planned) - count($holds));
});
}
سه نکتهٔ حیاتی:
- همه یا هیچ — کل حلقه در یک تراکنش. استثنا در جلسهٔ ۵ یعنی rollback جلسات ۱ تا ۴. رزرو نیمهکاره بدترین حالت است: بیمار فکر میکند دورهاش رزرو شده.
- لنگر متحرک — فاصله از جلسهٔ قبلی حساب میشود، نه از شروع دوره. اگر جلسهٔ ۲ سه روز دیرتر افتاد، جلسهٔ ۳ هم جابهجا میشود.
- سقف ۹۰ روز — محدودیت جستجوی تسک ۰۶. جلسات بیرون بازه
plannedمیمانند و بیمار بعداً رزرو میکند. پیام روشن اجباری است.
findNearestInRange — نزدیکترین به ایدهآل
$slots = $this->availability->search($req->withRange($min, $max));
if ($slots === []) return null;
usort($slots, fn($a, $b) => abs($a->start - $ideal) <=> abs($b->start - $ideal));
return $slots[0];
نزدیکترین به ایدهآل، نه اولین موجود. ۲۸ روز ایدهآل است؛ روز ۲۱ (حداقل) از نظر درمانی بدتر از روز ۲۷ است.
same_as_previous — اتصال به تسک ۰۶
// SameAsPreviousPicker (تسک ۰۶) به یک ورودی نیاز دارد که تا حالا نداشت
public function pick(array $freeIds, PlannedRequirement $req, OccupancyIndex $idx, AppointmentPlan $plan): int
{
$preferred = $plan->context()->preferredResourceIds ?? [];
foreach ($preferred as $id) {
if (in_array($id, $freeIds, true)) return $id;
}
return $this->fallback->pick($freeIds, $req, $idx, $plan); // least_gap
}
preferredResourceIds از TreatmentCourse.preferredResource میآید و در PlanRequest
حمل میشود. اگر منبع ترجیحی آزاد نبود، رزرو رد نمیشود — به least_gap برمیگردد.
اجبار به همان منبع یعنی بیمار دو هفته منتظر بماند.
پیشنهاد جلسهٔ بعدی
GET /treatment-course/{uuid}/next-slot-suggestion
▼
{
"session_number": 4,
"params": { "energy": 18 },
"ideal_date": "1405-06-01",
"range": { "min": "1405-05-25", "max": "1405-06-18" },
"suggested_slots": [ … سه وقت نزدیک به ایدهآل … ],
"warning": null
}
warning وقتی پر میشود که now > lastCompleted + maxDays:
«از حداکثر فاصلهٔ مجاز (۴۵ روز) عبور شده است. برای ادامهٔ دوره با پزشک مشورت کنید.»
اتصال به spacing تسک ۰۹
قانون spacing و پروتکل دوره هر دو فاصله را محدود میکنند. قانون برنده است اگر
سختگیرانهتر باشد:
$effectiveMin = max($course->getMinDays(), $policyMinDays ?? 0);
دلیل: پروتکل پیشنهاد بالینی است، قانون سیاست کلینیک. سیاست کلینیک نمیتواند شلتر شود.
این را در docs/api/course.md بنویس.
پنل ادمین
CourseProtocolsPage.tsx— پروتکل per سرویس + جدول پارامتر جلساتTreatmentCoursePage.tsx— نوار پیشرفت («۳ از ۸»)، جدول جلسات با وضعیت و تاریخ، دکمهٔ «رزرو جلسهٔ بعدی» و «رزرو همهٔ جلسات»- کارت دورهها در
PatientDetailPage.tsx - نوار پیشرفت باید فاصلهٔ واقعی بین جلسات را هم نشان دهد (۲۸ · ۳۱ · ۲۶ روز) — کلینیک از همان میفهمد بیمار منظم است یا نه