Files
clinicpro/docs/new_feture/taskes/task-00-service-mode-completion/checklist.md
T
hamedandClaude Opus 5 50759bf663 feat(admin): service-aware time picking on the appointment edit page
In service mode the page now mounts the existing ServiceSlotPicker and hides the
three free-form time inputs plus the single-service select: a 45-minute service
could previously be shortened to 20 and the next patient would sit on top of it.
Hidden rather than disabled — a disabled field reads as "you must do something
here".

Saving splits in two: the service-aware endpoint takes the time and services
(the client sends no duration), then the usual PATCH carries deposit, insurance,
status and note without slot_start/slot_end/version, since the reschedule already
advanced the optimistic-lock version.

Booking mode is read from the appointment's own schedule via an explicit
clinic_uuid, not from the panel's current environment: a doctor can be slot-based
in their office and service-based in a clinic. That required exposing clinic_uuid
in Appointment::toArray(), which was missing.

appointment-service-slots accepts exclude_appointment_uuid, gated on canManage of
that appointment — an ungated parameter would let anyone fabricate availability.

ServiceSlotPicker gained two optional props; its existing callers pass neither and
are unaffected. Its reset-on-doctor-change effect now skips the first run, which
would otherwise wipe the initial selection.

Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green)

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

21 KiB
Raw Blame History

چک‌لیست — تسک ۰۰ (تکمیل نوبت‌دهی سرویسی در clinicpro)

وضعیت کلی: 🔄 در حال انجام — قابلیت ۷ از ۱۰ تمام شد (مانده: ReserveAppointmentsPage، backfill، مستندات) آخرین بازبینی: ۱۴۰۵/۰۵/۰۸

قواعد: _shared/definition-of-done.md · خط سرخ‌ها: _shared/red-lines.md · UI: _shared/ui-conventions.md


۰. خط سرخ — منطق اسلاتی

# مورد وضعیت یادداشت
۰.۱ SlotModeFrozenTest + سه fixture ساخته شد پیش از هر تغییر کد tests/Appointment/SlotModeFrozenTest.php + tests/Appointment/fixtures/ — هیچ کد تولیدی‌ای هنوز لمس نشده
۰ fixture ها با تاریخ ثابت‌اند، نه time() → fixture ساختاری است انحراف عمدی از متن تسک. تاریخ ثابتِ گذشته را isWithinBookingWindow رد می‌کند و snapshot خالی چیزی را تضمین نمی‌کند. به‌جایش: برنامهٔ قطعی (هر ۷ روز یک شیفت ۰۹:۰۰–۱۱:۰۰/۳۰ دقیقه) روی +3 days، و epoch/uuid با placeholder نرمال می‌شوند. آنچه قفل می‌شود: کلیدها، ترتیب، نوع‌ها، ساعت‌های محلی
۰ کامنت «read-only، هیچ تسکی به‌روزش نمی‌کند» بالای هر سه fixture کلید _readme در دو JSON (در loader حذف می‌شود) + docblock در فایل PHP
۰ هیچ متد موجود SlotCalculatorService ویرایش نشد فقط getServiceStartTimes یک پارامتر اختیاری با پیش‌فرض null گرفت. اثبات کارکرد تور ایمنی: تست منجمد همان لحظه قرمز شد و دقیقاً همان پارامتر را نشان داد، در حالی که دو قرارداد پاسخ سبز ماندند. fixture امضا یک بار با تاریخچهٔ مکتوب به‌روز شد (header خودش مجاز کرده)
۰.۵ GET /appointment-slots بیت‌به‌بیت دست‌نخورده در پایان تسک تأیید می‌شود
۰ GET /month-availability/{doctorUuid} دست‌نخورده در پایان تسک تأیید می‌شود
۰.۷ active_slot_key و refreshActiveSlotKey() دست‌نخورده setIsReserve() وجود ندارد؛ toggle رزرو از قبل با rescheduleTo($start,$end,$isReserve) انجام می‌شود که خودش refreshActiveSlotKey() را صدا می‌زند (Appointment.php:316). یادداشت قبلی چک‌لیست غلط بود
۰ isSlotTaken امضا و معنا دست‌نخورده لمس نشد؛ فقط الگویش تکرار شد
۰ --group=slot-mode-frozen سبز OK (3 tests, 8 assertions) — نیازمند #[Group] attribute بود، نه @group (PHPUnit 12 annotation را حذف کرده)
۰.۱۰ baseline: کل tests/Appointment پیش از تغییرات سبز OK (166 tests, 382 assertions)

۱. بک‌اند

# مورد وضعیت یادداشت
۱.۱ ServiceBookingCalculator ساخته شد src/Appointment/Service/ServiceBookingCalculator.php + VO در src/Appointment/ValueObject/ServiceBookingDuration.php (قرارداد پروژه ValueObject است نه Dtosrc/Billing/ValueObject، src/Insurance/ValueObject)
۱ serviceSlots() موجود از آن استفاده می‌کند، خروجی‌اش عوض نشده ۲۰ خط از کنترلر حذف شد. بیت‌به‌بیت‌بودن: error() و ExceptionSubscriber یک envelope یکسان می‌سازند (['success'=>false,'data'=>null,'errors'=>[{code,message,field?}]])، پس AppException جای return $this->error() نشست. شاهد: ۱۶۶ تست موجود سبز ماند، شامل ServiceBasedSlotsTest و ServiceModeSectionDurationTest که همین مسیرهای خطا را می‌سنجند
۱ جمع سادهٔ += حفظ شد (اصلاحش تسک ۰۴ است) با کامنت صریح در docblock کلاس: «تسک ۰۴ همان‌جا یک خط عوض می‌کند»
۱ ServiceRescheduleService + POST /appointment/{uuid}/service-reschedule route ثبت شد (app_appointment_appointment_servicereschedule). کنترلر نازک: فقط ۴۰۴/۴۰۳/اعتبار start و ترجمهٔ دو استثنای همزمانی؛ منطق در سرویس. forManagement از canManageContext() می‌آید نه از canManage() — بیمارِ صاحب نوبت باید پنجرهٔ رزرو را رعایت کند
۱.۵ PATCH /appointment/{uuid} توسعه یافت — منطق جدید داخل isServiceMode() سرویس‌ها پیش از بلوک زمان حل می‌شوند چون مدت به آن‌ها وابسته است. بررسی مدت فقط وقتی $movingToLiveSlot است (نوبت رزرو معاف — slot_start == slot_end). در حالت اسلاتی $duration === null می‌ماند و هیچ خطی اجرا نمی‌شود
۱ PATCH مقدار service_item_uuids[] می‌پذیرد وقتی فهرست کامل آمد، service_item_uuid تکی در حلقهٔ relations نادیده گرفته می‌شود (continue) — دو منبع برای یک چیز به نوبتِ ناسازگار می‌رسد. allowInactive فقط وقتی فهرست نیامده روشن است: نوبت موجود قفل نشود ولی افزودن سرویس غیرفعال رد شود
۱.۷ ReserveConversionService + POST /convert-reserve → توسعهٔ PATCH endpoint جدید ساخته نشد و توجیه نداشت (قاعدهٔ #۸ پروژه). PATCH از قبل رزرو→زمان‌دار را دارد: rescheduleTo($start,$end,$isReserve) که خودش refreshActiveSlotKey() را صدا می‌زند. تست testReserveConvertsToATimedAppointmentThroughPatch این را می‌سنجد
۱ نوبت رزرو در حالت سرویسی سرویس‌ها را ذخیره می‌کند سرویس‌ها + service_total_minutes روی رزرو ذخیره می‌شوند تا تبدیل بعدی مدت را از دست ندهد؛ بررسی مدت روی خودِ رزرو اعمال نمی‌شود
۱ excludeAppointmentId روی getServiceStartTimes و findBusyIntervals همان نام/جای/پیش‌فرضِ isSlotTaken($doctor,$start,$end,?int $excludeId=null). در repository به occupiedIntervals() هم پاس داده می‌شود، پس findBlockingIntervals بی‌تغییر ماند
۱.۱۰ Appointment::replaceServiceItems() + currentServiceUuids() کنار addServiceItem() موجود؛ currentServiceUuids() ترتیب ورودی را حفظ می‌کند و برای نوبت قدیمیِ فقط-تکی هم uuid می‌دهد
۱.۱۱ replaceServiceItems مقدار serviceItem تکی را هم‌گام می‌کند برخلاف addServiceItem() که فقط وقتی null است ست می‌کند، این یکی بی‌قید بازنویسی می‌کند (و با فهرست خالی null می‌شود). چهار مصرف‌کننده روی service_item تکی خوانده‌اند
۱.۱۲ setIsReserve() صدا زدن refreshActiveSlotKey() موردی برای انجام نبود: setIsReserve() وجود ندارد؛ toggle رزرو از قبل با rescheduleTo($start,$end,$isReserve) است که خودش refreshActiveSlotKey() را صدا می‌زند
۱.۱۳ اعتبارسنجی زمان با عضویت در getServiceStartTimes، نه فقط isSlotTaken assertStartIsOffered(). تست testTimeOutsideTheOfferedListIsRejected با ۰۳:۰۰ (بیرون شیفت ۰۹–۱۸) می‌سنجد — isSlotTaken تنها این را نمی‌گرفت
۱.۱۴ TenantOwnershipChecker روی همهٔ uuid های سرویس، پیش از هر بررسی دیگر belongsToPair() (نه belongsTo()): مسیر عمومی است و کاربر احراز‌شده ندارد. جای مقایسهٔ دستیِ getSection()->getEntityType() نشست — نقطهٔ واحد tenancy.md. ⚠️ نام property باید itemRepo باشد وگرنه TenantLookupInventoryTest آن را نمی‌شمارد و از تور ایمنی فرار می‌کند
۱.۱۵ allowInactive فقط برای سرویس‌های موجود نوبت، نه uuid های تازه پرچم روی calculate()؛ غیرفعال‌بودن → warnings[] به‌جای ۴۲۲. مدتِ نامعتبر همچنان حتی با allowInactive رد می‌شود (تست دارد)
۱.۱۶ کنترلر نازک ماند — منطق در سرویس serviceSlots() از ۶۰ خط به ۴۰ خط؛ هیچ حلقهٔ محاسباتی در کنترلر نماند

۲. دیتابیس و مهاجرت

# مورد وضعیت یادداشت
۲.۱ service_total_minutes و service_buffer_minutes (تهی‌پذیر) SMALLINT DEFAULT NULL هر دو · migrations/Version20260730091332.php · setServiceDuration() هر دو را با هم ست می‌کند (بافرِ بی‌مدت معنا ندارد)
۲.۲ هیچ ستون موجودی حذف/تغییر نوع/تغییر معنا نداد تأیید با SHOW COLUMNS: slot_start int NOT NULL · slot_end int NOT NULL · active_slot_key varchar(64) UNI · is_reserve tinyint default 0 — همه دست‌نخورده
۲.۳ دو کد خطای جدید در ErrorCodes.php با پیام فارسی ERR_APPOINTMENT_003 (مدت نمی‌خواند) و ERR_APPOINTMENT_004 (روش نوبت‌دهی ناسازگار) — نه _010/_011 که متن تسک حدس زده بود؛ فایل فقط تا _002 داشت
۲.۴ app:appointment:backfill-service-duration — dry-run پیش‌فرض، idempotent
۲.۵ backfill مقدار را از خود نوبت می‌گیرد، نه بازمحاسبه از سرویس‌ها
۲.۶ migration اجرا شد و TenantSchemaCoverageTest سبز dev با migration؛ db_test با ALTER دستی — تاریخچهٔ migration آن DB از dev جدا است (Table users already exists) و این روش نگهداشتِ موجودِ همان DB است

۳. UI — پنل ادمین

# مورد وضعیت یادداشت
۳.۱ AppointmentEditPage: حالت سرویسی ServiceSlotPicker نشان می‌دهد روش نوبت‌دهی از برنامهٔ همین نوبت پرسیده می‌شود (clinic_uuid صریح)، نه از محیط جاری پنل — پزشک می‌تواند در مطب اسلاتی و در کلینیک سرویسی باشد. لازمهٔ این کار: افزودن clinic_uuid به toArray() که نبود
۳.۲ AppointmentEditPage: حالت اسلاتی دقیقاً رفتار امروز ۵ تست موجود صفحه سبز ماند + دو تست خط سرخ تازه: سه فیلد ساعت با مقدار هیدریت‌شده، و همان PATCH با version بدون هیچ POST
۳.۳ ورودی دستی ساعت در حالت سرویسی پنهان، نه disabled queryByLabelText('ساعت شروع') در حالت سرویسی null است. SearchableSelect سرویس تکی هم پنهان می‌شود — نگه‌داشتنش دو منبع برای یک چیز بود
۳.۴ ServiceSlotPicker موجود بازاستفاده شد؛ نسخهٔ موازی ساخته نشد دو prop اختیاری: excludeAppointmentUuid و initialSelection. رفتار فعلی‌اش در AppointmentCreatePage و AppointmentsPage دست‌نخورده (هر دو prop undefined). یک اصلاح لازم شد: effect ریست روی تعویض پزشک، اجرای نخست را معاف کند وگرنه initialSelection همان لحظه پاک می‌شد
۳.۵ ReserveAppointmentsPage: سرویس‌ها + دکمهٔ تبدیل
۳.۶ ReserveAppointmentsPage از جدول خام به DataTable مهاجرت کرد <td style={td}> حذف شد
۳.۷ مدت و بافر فارسی با واحد: «۳۵ دقیقه (+۱۰ دقیقه فاصله)» 🔄 در AppointmentEditPage هست («زمان انتخابی: ۱۶:۰۰ (+۱۰ دقیقه فاصله)») و ServiceSlotPicker خودش «مدت کل: N دقیقه» را نشان می‌دهد. ReserveAppointmentsPage مانده — قابلیت ۸
۳.۸ هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن‌های styles.css
۳.۹ دارک‌مود (data-theme="dark") بررسی شد
۳.۱۰ حالت فشرده (data-density="compact") بررسی شد
۳.۱۱ انتخاب چند سرویس با SearchableSelect؛ هیچ <select> بومی ServiceSlotPicker از SearchableSelect برای بخش و دکمه‌های toggle برای سرویس‌ها استفاده می‌کند (کد موجود). هیچ <select> بومی اضافه نشد
۳.۱۲ backTo/BackButton روی هر دو صفحه
۳.۱۳ وضعیت لیست رزروها در URL با useUrlState
۳.۱۴ تاریخ با PersianDateInput · مبلغ با formatRial PersianDateInput در هر دو حالت؛ مبالغ با formatRial/PriceInput موجود
۳.۱۵ RTL بررسی شد (ms/me نه ml/mr)
۳.۱۶ موبایل بررسی شد — بدون اسکرول افقی
۳.۱۷ همهٔ رشته‌ها فارسی و از i18n
۳.۱۸ داده با TanStack Query و استخراج envelope درست useDoctorBookingServices (موجود) + useQuery داخل picker؛ استخراج data?.data

۴. تست

# مورد وضعیت یادداشت
۴.۱ SlotModeFrozenTest — سه سنجه OK (3 tests, 8 assertions)
۴.۲ ServiceBookingCalculatorTest — موفق/خطا/مرزی ۱۳ تست / ۲۹ assertion سبز. شامل: جمع مدت + بافر · endFor بدون بافر · override منشی بدون تغییر پیش‌فرض سرویس · چهار مسیر خطا با کد/پیام/فیلد دقیق · uuid ناموجود از سرویسِ محیط دیگر قابل تفکیک نیست · allowInactive → warning · فهرست خالی → صفر · override نامعتبر (۰ و منفی) → fallback · پزشک بی‌برنامه → پیش‌فرض slot
۴.۳ ServiceRescheduleTest — شامل «حذف سرویس → مدت خودکار» ۱۵ تست / ۳۴ assertion. شامل: جابه‌جایی بی‌ارسال مدت · حذف سرویس → کوچک‌شدن خودکار · زمان فعلیِ خود نوبت با سرویس بلندتر پذیرفته می‌شود (اثر excludeAppointmentId) · سرویس غیرفعالِ موجود → warning · حالت اسلاتی → ERR_APPOINTMENT_004 · بیرون شیفت → ERR_APPOINTMENT_001 · رزرو → پیام ارجاع به ویرایش · start غایب · سرویس بیگانه · افزودن سرویس غیرفعال تازه · گذشته · نوبت بی‌سرویس · نسخهٔ کهنه → ۴۰۹ · نوبت شخص دیگر → ۴۰۳ · uuid ناموجود → ۴۰۴
۴.۴ PatchServiceDurationTest — شامل «در حالت اسلاتی هیچ‌کدام اجرا نمی‌شود» ۹ تست / ۲۴ assertion. شامل: بازمحاسبهٔ مدت با تعویض سرویس · هم‌گامی ستون تکی · PATCH فقط-یادداشت بی‌اعتبارسنجی · رزرو معاف ولی مدت‌دار · تبدیل رزرو با همان PATCH · مدت ناسازگار → ERR_APPOINTMENT_003 با عدد درست در پیام · سرویس بیگانه → ۴۲۲ · حالت اسلاتی هر مدتی را می‌پذیرد و ستون سرویسی null می‌ماند · نوبت سرویسیِ بی‌سرویس قفل نمی‌شود
۴.۵ ConvertReserveTest — شامل active_slot_key و رقابت
۴.۶ ServiceModeSectionDurationTest موجود سبز ماند
۴.۷ BookingTenantTest موجود سبز ماند داخل tests/Appointment — کل ۳۰۹ تست tests/Appointment + tests/Shared سبز
۴.۱۰ ServiceSlotExcludeSelfTest — رفتار exclude ۶ تست / ۱۱ assertion. شامل: بازهٔ خودِ نوبت با exclude برمی‌گردد · مدت بلندتر روی همان ساعت · نوبتِ دیگری همچنان اشغال می‌ماند · null صریح و ضمنی خروجی یکسان · فیلتر repository فقط همان ردیف · exclude کردن نوبت رزرو بی‌اثر
۴.۹ AppointmentServiceFieldsTest — متدها و ستون‌های جدید ۹ تست / ۲۴ assertion. شامل: هم‌گامی ستون تکی · حفظ ترتیب · فهرست خالی → null · حالت اسلاتی هر دو ستون null · مدتِ null بافر را هم null می‌کند · تکراری‌ها dedup · نوبت قدیمیِ فقط-تکی · بقای مقادیر پس از flush/clear
۴.۸ AppointmentEditPage — دو حالت فایل جدید AppointmentEditPage.serviceMode.test.tsx: ۹ تست. شامل: پنهان‌بودن ورودی ساعت · دکمهٔ غیرفعال + راهنما · exclude_appointment_uuid در query · service-reschedule بدون هیچ مدتی · PATCH بعدی بی‌version · هشدار سرویس غیرفعال · و سه تست خط سرخ (اسلاتی و رزرو). فایل موجود AppointmentEditPage.test.tsx هم سبز ماند
۴.۱۱ کل vitest سبز 86 files / 604 tests passed روی host (داخل ddev باینری esbuild پلتفرم اشتباه دارد — مسئلهٔ محیطی از قبل)

۵. مستندات

# مورد وضعیت یادداشت
۵.۱ docs/api/appointment.md — دو endpoint جدید + توسعهٔ PATCH
۵ ماتریس «کدام endpoint در کدام حالت»
۵ docs/architecture/booking-modes.md ساخته شد تسک ۰۶ حالت سوم را اضافه می‌کند
۵ دو کد خطای جدید مستند شد

۵.۵ یافته‌های جانبی

# مورد وضعیت یادداشت
ج.۱ یک شکست flaky در اجرای ترکیبی tests/Appointment tests/Shared ⚠️ یک بار ۱ failure دید، سه اجرای بعدی سبز (۳۲۴ تست). نام تست ثبت نشد چون خروجی از دست رفت. بازتولید نشد — بدهی ثبت‌شده، نه «حل‌شده». احتمال: برخورد شمارهٔ موبایل تصادفی در ApiTestCase::createUser روی db_test که هرگز ریست نمی‌شود (خودِ کلاس این را مستند کرده)
ج.۲ یک PHPUnit Notice در tests/Shared ⚠️ پیش از تغییرات این تسک هم بود (baseline). خارج از دامنهٔ این تسک
ج.۳ db_test تاریخچهٔ migration جدا دارد ⚠️ doctrine:migrations:migrate روی آن می‌شکند (Table users already exists)؛ ستون‌های جدید با ALTER دستی اضافه شدند. برای تسک‌های بعدی هم همین لازم است

۶. بازبینی پایانی

# مورد وضعیت یادداشت
۶.۱ همهٔ ردیف‌های بالا وضعیت نهایی دارند (هیچ 🔄 و بی‌دلیل)
۶.۲ ddev exec php bin/phpunit کامل سبز
۶.۳ ddev exec php bin/phpunit --group=slot-mode-frozen سبز
۶.۴ phpstan analyse بدون خطای جدید 🔄 analyse src/AppointmentNo errors. تحلیل کامل src ۱۴ خطا دارد ولی هیچ‌کدام در فایل‌های این تسک نیست (AuthController، BillingController، ClinicServiceController، ServiceItem، DoctorClaimService، InventoryService، PatientService، SecretaryService، HealthController) — از قبل بوده‌اند. در پایان تسک با baseline مقایسه می‌شود
۶.۵ npx tsc --noEmit بدون خطا
۶.۶ yarn test سبز
۶.۷ TenantSchemaCoverageTest + TenantLookupInventoryTest سبز
۶.۸ docs/api/* به‌روز شد
۶.۹ چک‌لیست UI (بخش ۳) کامل شد
۶.۱۰ nobat724_front و clinic-pro-tauri دستی بررسی شدند service_item تکی هم‌گام است؟
۶.۱۱ commit شد، سپس graphify update .
۶.۱۲ موارد به‌تعویق‌افتاده با دلیل و تسک مقصد ثبت شدند