- Add task for completing service mode in clinicpro with detailed objectives and acceptance criteria. - Create architecture documentation for task 00b, outlining involved components and necessary changes. - Develop checklist for task 00b to ensure all requirements are met. - Document implementation notes for task 00b, emphasizing API contract checks and design system adherence. - Update task documentation for task 00b, specifying goals and current issues with service mode.
11 KiB
نکات پیادهسازی — تسک ۰۰
۱. اول تست خط سرخ، بعد هر چیز دیگر
ترتیب کار:
۱. tests/Appointment/SlotModeFrozenTest.php + سه fixture ← اول این
۲. ddev exec php bin/phpunit --group=slot-mode-frozen ← باید سبز باشد قبل از هر تغییری
۳. بقیهٔ تسک
۴. دوباره گام ۲ — باید همچنان سبز باشد
اگر fixture را بعد از تغییرات بسازی، هیچ چیزی را تضمین نکردهای — snapshot وضعیت تغییریافته را گرفتهای.
۲. isServiceMode گِیت همهچیز است
هر خط کد جدید در مسیر مشترک باید داخل این شرط باشد:
if ($this->serviceCalc->isServiceMode($doctor, $clinic)) {
// … منطق جدید
}
نه بیرونش، نه با ??، نه با «اگر سرویس دارد». معیار فقط booking_mode است.
نوبت اسلاتی هم میتواند service_item_id داشته باشد (فیلدهای Figma نوبتها) — آن دلیل
سرویسی بودن نیست.
اشتباه رایج:
// ❌ نوبت اسلاتیِ دارای سرویس را وارد مسیر جدید میکند
if ($appointment->getServiceItems()->count() > 0) { … }
۳. جمع سادهٔ مدت را همینجا اصلاح نکن
$totalMinutes += $duration; // ← اشتباه است، ولی دست نزن
مستند بند ۵ میگوید این فرمول ظرفیت را الکی پر میکند و راهحلش «زمان تنها / زمان اضافه» است — که تسک ۰۴ میسازد. اصلاحش اینجا یعنی:
- مدت همهٔ نوبتهای چندسرویسیِ در حال رزرو یکشبه کم میشود
- سایت و اپ دسکتاپ عدد متفاوت میبینند بدون اینکه چیزی در build بشکند
- و هیچ دادهای برای «زمان اضافه» وجود ندارد، پس اصلاح بیورودی غیرممکن است
کاری که این تسک میکند: محاسبه را به یک نقطه منتقل میکند تا تسک ۰۴ یک خط عوض کند.
۴. excludeAppointmentId — الگوی موجود را تکرار کن
AppointmentRepository::isSlotTaken($doctor, $start, $end, $excludeId) از قبل این پارامتر
را دارد. findBusyIntervals هم همان را بگیرد، با همان نام و همان جای پارامتر و همان
پیشفرض null.
دو الگوی متفاوت برای یک کار (مثلاً یکی ?int $excludeId، دیگری array $excludeIds)
یعنی اولین کسی که هر دو را میبیند یکی را اشتباه صدا میزند.
۵. اعتبارسنجی زمان: عضویت در فهرست، نه «اشغال نبودن»
// ❌ ناکافی
if ($this->appointmentRepo->isSlotTaken($doctor, $start, $end, $excludeId)) { /* 409 */ }
// ✅
$starts = $this->slotCalculator->getServiceStartTimes(…);
if (!in_array($req->start, array_column($starts, 'start'), true)) { /* 422 */ }
isSlotTaken فقط تداخل با نوبت دیگر را میگوید. getServiceStartTimes علاوه بر آن
شیفت، تعطیلی، date_override، پنجرهٔ رزرو و بافر را هم اعمال میکند. با شرط اول،
منشی میتواند نوبت را ساعت ۳ بامداد بگذارد.
۶. warnings[] بهجای 422 برای سرویس غیرفعال در نوبت موجود
$duration = $this->serviceCalc->calculate(…, allowInactive: true);
// $duration->warnings === ['سرویس «لیزر صورت» دیگر برای نوبتدهی فعال نیست']
نوبت موجود با سرویسی که کلینیک غیرفعالش کرده، باید قابل جابهجایی و لغو بماند. 422
یعنی آن نوبت برای همیشه قفل میشود و منشی هیچ کاری نمیتواند بکند.
ولی افزودن سرویس غیرفعال به نوبت → 422. تفاوتش allowInactive است که فقط برای
uuid های موجودِ نوبت true میشود، نه برای uuid های تازهی درخواست.
۷. replaceServiceItems باید serviceItem تکی را همگام کند
$this->serviceItem = $items[0] ?? null;
چهار مصرفکننده روی service_item تکی خواندهاند (AppointmentsPage،
ReserveAppointmentsPage، nobat724_front/services/response.js،
clinic-pro-tauri/src/service/response.js). این دقیقاً همان الگویی است که
ServiceItem::setStaffMembers() برای staff تکی دارد — تکرارش کن.
۸. refreshActiveSlotKey پس از setIsReserve(false)
public function setIsReserve(bool $v): self
{
$this->isReserve = $v;
$this->refreshActiveSlotKey(); // ← اگر نیست، اضافه کن
$this->updatedAt = time();
return $this;
}
بدون آن، رزروِ تبدیلشده active_slot_key = NULL میماند و دو نفر میتوانند همان ساعت
را بگیرند. این تنها تغییر مجاز در مکانیزم active_slot_key است و فقط چون یک شرط
موجود را اعمال میکند، نه عوضش میکند. تست: ConvertReserveSlotKeyTest.
۹. تبدیل رزرو، اتمی
$this->em->wrapInTransaction(function () use ($reserve, $req) {
$duration = …; // یا اسلات اسلاتی
$reserve->setIsReserve(false);
$reserve->reschedule($req->start, $end);
$reserve->replaceServiceItems($duration->serviceItems);
// UniqueConstraintViolationException روی active_slot_key → 409
});
try/catch روی UniqueConstraintViolationException و ترجمه به 409 ERR_SLOT_TAKEN —
همان چیزی که SlotTakenException موجود در src/Appointment/Repository/ انجام میدهد.
از همان استفاده کن.
۱۰. ReserveAppointmentsPage → DataTable
صفحه امروز جدول خام با <td style={td}> دارد. چون در این تسک دستش میزنیم، همانجا به
DataTable مهاجرت کند: توکن inline خلاف ui-conventions
است و در دارکمود میشکند.
این «scope creep» نیست — قاعدهٔ پروژه است که صفحهٔ دستخورده باید با دیزاینسیستم بخواند.
۱۱. edge case ها
| حالت | رفتار درست |
|---|---|
| نوبت سرویسی بدون هیچ سرویس (داده قدیمی) | مدت موجود حفظ · warnings[] · رد نمیشود |
PATCH فقط یادداشت روی نوبت سرویسی |
بدون اعتبارسنجی مدت — مثل امروز |
service-reschedule روی نوبت اسلاتی |
422 ERR_WRONG_BOOKING_MODE |
service-reschedule روی نوبت رزرو |
422 — مسیرش convert-reserve است |
| زمان فعلی نوبت با سرویس جدید | با excludeAppointmentId در فهرست میآید |
| بافر عوض شد بعد از ثبت | نوبت موجود سالم؛ فقط جابهجایی جدید بافر جدید میگیرد |
سرویس محیط دیگر در service_item_uuids[] |
404 — TenantOwnershipChecker پیش از هر بررسی دیگر |
durations override با مقدار ۰ یا منفی |
نادیده گرفته شود (رفتار موجود serviceSlots) |
| نوبت گذشته | service-reschedule → 422 |
| دو درخواست جابهجایی همزمان به یک زمان | active_slot_key → یکی 409 |
| نوبت در محیطی که وسط کار به اسلاتی برگشت | booking_mode قفل است پس رخ نمیدهد؛ ولی اگر داده دستی عوض شد → 422 روشن |
۱۲. تست
tests/Appointment/SlotModeFrozenTest.php ← ⭐ اول از همه
- قرارداد appointment-slots بیتبهبیت
- قرارداد month-availability
- امضای متدهای عمومی SlotCalculatorService
tests/Appointment/ServiceBookingCalculatorTest.php
- جمع مدت چند سرویس (رفتار فعلی حفظ شود)
- override منشی
- سرویس بدون مدت → 422
- سرویس غیرفعال با allowInactive → warning نه خطا
- سرویس محیط دیگر → استثنا، بدون افشای وجود
tests/Appointment/ServiceRescheduleTest.php ← ⭐
- جابهجایی با همان سرویسها → مدت یکسان
- حذف یک سرویس → مدت خودکار کم میشود، کلاینت عددی نفرستاده
- زمان بیرون getServiceStartTimes → 422
- زمان فعلی خود نوبت با excludeAppointmentId در فهرست است
- روی نوبت اسلاتی → 422 ERR_WRONG_BOOKING_MODE
tests/Appointment/PatchServiceDurationTest.php
- slot_end ناسازگار → 422 ERR_SERVICE_DURATION_MISMATCH با مدت درست در پیام
- service_item_uuids[] جایگزینی کامل میکند و serviceItem تکی همگام میشود
- در حالت اسلاتی هیچکدام از اینها اجرا نمیشود (رفتار امروز)
tests/Appointment/ConvertReserveTest.php
- رزرو سرویسی → نوبت زماندار با مدت درست
- رزرو اسلاتی → مسیر اسلاتی، بدون تغییر
- active_slot_key بعد از تبدیل پر میشود
- دو تبدیل همزمان → یکی 409
tests/Appointment/ServiceModeSectionDurationTest.php ← موجود، باید سبز بماند
tests/Appointment/BookingTenantTest.php ← موجود، باید سبز بماند
assets/admin/pages/AppointmentEditPage.test.tsx
- حالت اسلاتی: سه فیلد ساعت هست، ServiceSlotPicker نیست
- حالت سرویسی: ServiceSlotPicker هست، فیلد ساعت پنهان است
- تغییر سرویسها، slot انتخابی را باطل میکند
۱۳. مستندات
docs/api/appointment.md:
- بخش «روشهای نوبتدهی» با ماتریس «کدام endpoint در کدام حالت»
- دو endpoint جدید
- توسعهٔ
PATCHو پارامترexclude_appointment_uuid - دو کد خطای جدید
docs/api/appointment-settings.md: یادآوری قفل بودن booking_mode پس از اولین ثبت.
و یک سند کوتاه docs/architecture/booking-modes.md که ماتریس کامل را ثبت کند —
تسک ۰۶ حالت سومی به همین ماتریس اضافه میکند.