Files
clinicpro/docs/api/appointment-plan.md
T
hamedandClaude Opus 5 b3c331f0cb perf(reports): read every resource's calendar in one batch, and close the owed tests
Writing the query-count test that task 14 owed showed the growth was real: one
resource cost 10 queries, six cost 33 — about five per resource, because the
available-minutes figure walked each resource's calendar on its own.

Holidays, tenant overrides and branch hours are identical for every resource in
a report, so they now load once outside the loop; shifts and exceptions load for
all resources in one query each. The batched path is a new method rather than a
change to rawAvailability, which the booking engine also calls. The test pins
the shape of the growth, not an exact count.

Also landed:

- app:segment:seed-templates with beauty, dental and physio presets. Building
  four segments and their requirements by hand is the first thing a new clinic
  must do and the most tedious; this gives them something to edit instead of an
  empty page. It refuses to touch a service that already has segments unless
  --force, and it will not invent resource types the tenant never defined.
- book-all is all-or-nothing, proven rather than asserted: with a calendar open
  one day a week and a 1-2 day protocol gap, session one finds a slot and
  session two cannot, and every session must come back planned.
- credit_refundable: false takes the credit back with a negative adjustment and
  deletes nothing — the ledger stays append-only.
- the segments editor has frontend tests, including that it sends back what the
  user sees and renders read-only without the permission.

useBranches now returns [] for a non-array payload instead of throwing
"branches.map is not a function" and taking the page down with it.

BookingLocationsScanTest built a Clinic around a Doctor loaded from a different
manager, which Doctrine treats as a new entity; it flushed fine most runs and
failed on cascade in others. It now loads the doctor from the same manager.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 14:22:22 +03:30

9.2 KiB
Raw Blame History

Appointment Plan API — بخش‌های نوبت و سازندهٔ برنامه

Base: /api/v1 · Auth: JWT وابسته به resource.md (منبع و مهارت) و clinic-services.md (مدت آیتم‌ها).


چرا نوبت یک تکه نیست

بند ۷ مستند. یک جلسهٔ لیزر:

بخش مدت اتاق اپراتور دستگاه
مالیدن کرم بی‌حسی ۵ اشغال اشغال آزاد
انتظار اثر کرم ۳۰ اشغال آزاد آزاد
خود لیزر ۲۰ اشغال اشغال اشغال
مراقبت بعد ۵ اشغال اشغال آزاد

با مدل تک‌بازه‌ای، اپراتور ۶۰ دقیقه قفل می‌شود در حالی که ۳۰ دقیقه کار می‌کند — نصف ظرفیت هدر می‌رود.

⚠️ این بخش هیچ زمان مطلقی و هیچ منبع مشخصی تعیین نمی‌کند. فقط شکل نوبت را می‌سازد؛ پیدا کردن وقت و منبع آزاد کارِ تسک بعدی است.


GET/PUT /api/v1/service-item/{uuid}/segments

PUT جایگزینی کامل است. هر بخش:

فیلد نوع توضیح
sequence int ترتیب اجرا
name string الزامی
duration_source fixed | items پیش‌فرض fixed
duration_minutes int برای fixed باید مثبت باشد
patient_present bool پیش‌فرض true؛ «انتظار در خانه» خلافش است
mergeable bool با چند آیتم یک بار می‌آید
requirements array نیازمندی منبع

duration_source: "items" یعنی مدت این بخش از آیتم‌های انتخاب‌شده می‌آید و با فرمول تسک ۰۴ حساب می‌شود. «خود لیزر» با دو ناحیه طولانی‌تر می‌شود، ولی «انتظار اثر کرم» نه — به همین دلیل دو منبع مدت لازم است و یک عدد ثابت کافی نیست.

هر نیازمندی:

فیلد توضیح
type_uuid نوع منبع (اتاق، اپراتور، دستگاه)
skill_uuid مهارت لازم
count پیش‌فرض ۱
occupancy exclusive (قفل کامل) یا shared (ظرفیت می‌شمارد)
constraints فعلاً فقط same_gender_as_patient

۴۲۲: نام خالی · بخش fixed با مدت صفر · مجموع بیش از ۴۸۰ دقیقه · occupancy یا constraint ناشناخته.


POST /api/v1/appointment-plan/preview

{
  "service_uuid": "…لیزر",
  "branch_uuid": "…شعبه",
  "item_uuids": ["…صورت", "…بیکینی"],
  "patient_gender": "female"
}

۲۰۰:

{
  "success": true,
  "data": {
    "total_minutes": 60,
    "segments": [
      { "sequence": 1, "name": "بی‌حسی موضعی", "offset_minutes": 0, "duration_minutes": 5,
        "patient_present": true, "mergeable": false,
        "requirements": [
          { "role": "room", "role_name": "اتاق", "skill_name": null, "count": 1,
            "occupancy": "exclusive", "constraints": [], "candidates": 3,
            "occupancy_offset": { "setup_minutes": 0, "cleanup_minutes": 0 } }
        ] }
    ]
  }
}

candidates تعداد منابع واجد شرایط است. occupancy_offset در نمای کاربر نمایش داده نمی‌شود؛ برای موتور جستجوی وقت است و محافظه‌کارانه از بیشترین مقدارِ کاندیدها گرفته می‌شود — کم گرفتنش یعنی نوبت بعدی روی زمان تمیزکاری بیفتد.

خطاها

کد کِی
ERR_NO_ELIGIBLE_RESOURCE (۴۲۲) هیچ منبعی شرایط یک بخش را ندارد
ERR_VALIDATION_002 (۴۲۲) قید جنسیت هست ولی جنسیت بیمار نامشخص است
ERR_VALIDATION_001 (۴۲۲) مدت بخش تعیین نشده · مجموع بیش از سقف
۴۰۴ سرویس یا شعبهٔ محیط دیگر

پیام ERR_NO_ELIGIBLE_RESOURCE انسانی است و نقش، مهارت و شعبه را می‌گوید: «هیچ اپراتور خانمی با مهارت «لیزر آلکساندرایت» در شعبهٔ «مرکزی» موجود نیست» (بند ۱۰).


سه قرارداد

۱. سرویس بدون الگوی بخش، همان رفتار امروز را می‌گیرد. یک بخش پیوسته به اندازهٔ کل مدت که منبعِ type=doctor را می‌گیرد. بدون این، هر سرویس موجود بی‌برنامه می‌شد.

۲. بخش بدون هیچ نیازمندی معتبر است. «انتظار در خانه» زمان می‌گیرد ولی هیچ منبعی اشغال نمی‌کند.

۳. قید جنسیت وقتی جنسیت بیمار نامشخص است نادیده گرفته نمی‌شود. ۴۲۲ می‌دهد، چون رد کردن بی‌صدا یعنی بیمار به منبعی می‌رسد که قرار نبود.

ادغام بخش‌ها

برنامه از الگوهای سرویس اصلی به‌علاوهٔ آیتم‌های انتخاب‌شده ساخته می‌شود. تا پیش از این فقط الگوهای سرویس اصلی خوانده می‌شد، پس بخش‌هایی که کلینیک روی خودِ ناحیه تعریف کرده بود بی‌صدا نادیده می‌ماند و mergeable هرگز کاری نمی‌کرد — در یک سرویس، دو بخشِ هم‌نام معنا ندارد.

قاعده چرا
هم‌نام‌های mergeable یک بار می‌آیند «آماده‌سازی» برای دو ناحیه یک بار انجام می‌شود
از میان هم‌نام‌ها طولانی‌ترین می‌ماند آماده‌سازی دو ناحیه کوتاه‌تر از طولانی‌ترینشان نیست
duration_source: "items" هم یک بار می‌آید DurationCalculator از قبل مجموع همهٔ آیتم‌ها را داده؛ تکرارش یعنی دوبار شمردن
تعداد منبع پس از ادغام بیشینه است دو ناحیه با هم دو اتاق نمی‌خواهند، ولی اگر یکی دو اپراتور لازم داشت ادغام نباید به یک تنزلش بدهد

مثال: «لیزر صورت» با آماده‌سازی ۵ دقیقه و «لیزر بیکینی» با آماده‌سازی ۱۲ دقیقه، هر دو انتخاب شوند → یک آماده‌سازیِ ۱۲ دقیقه‌ای، و کارِ اصلی به اندازهٔ مجموع دو ناحیه.

برنامه قطعی است: دو preview با همان ورودی خروجیِ بایت‌به‌بایت یکسان می‌دهند (تست دارد). برنامه‌ای که بین پیش‌نمایش و رزرو جابه‌جا شود یعنی کاربر چیزی را تأیید کرده که رزرو نشد.


طبقه‌بندی محیط

جدول وضعیت
segment_templates جفت محیط (از بخشِ سرویس مشتق می‌شود)
segment_requirements AGGREGATE_CHILDREN — ریشه SegmentTemplate

الگوی نمونه

ddev exec php bin/console app:segment:seed-templates --service=<uuid> --preset=beauty

سه الگو: beauty (آماده‌سازی · بی‌حسی · انتظار · کار اصلی · تمیزکاری) · dental (معاینه · درمان · ضدعفونی یونیت) · physio (ارزیابی · جلسهٔ درمان · استراحت).

نقطهٔ شروع است نه پیکربندی نهایی: کلینیک از روی چیزی که می‌بیند ویرایش می‌کند، نه از روی صفحهٔ خالی. روی سرویسی که از قبل بخش دارد کاری نمی‌کند مگر --force — بازنویسی خاموشِ چیزی که کلینیک خودش ساخته، بدترین رفتار ممکن است. نقشی که آن محیط تعریف نکرده، ساخته نمی‌شود و در خروجی گزارش می‌شود؛ نوع منبع تصمیم کلینیک است.

تست‌ها

ddev exec php bin/phpunit tests/Appointment/AppointmentPlanTest.php   # ۱۵ تست

اثر موتور قوانین

  • دستهٔ timing: min_duration_minutes (بیشترین برنده) و add_duration_minutes (جمع) روی مجموع نوبت اعمال می‌شوند؛ رشدِ لازم به آخرین بخش می‌چسبد تا آفست بخش‌های قبلی جابه‌جا نشود.
  • دستهٔ resource: نقشی که require_resource می‌خواهد، اگر هیچ بخشی نداشته باشد، به اولین بخشی که بیمار حاضر است اضافه می‌شود. نقش ناشناخته یا بی‌منبع 422 می‌دهد، نه بی‌اثر ماندن.
  • هر دو روی سرویسِ بی‌الگو هم اجرا می‌شوند.

جزئیات: policy.md