Files
clinicpro/docs/new_feture/taskes/task-00-service-mode-completion/implementation_notes.md
T
hamed 158dcb58aa feat: implement service mode completion for nobat724_front
- 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.
2026-07-30 11:56:08 +03:30

11 KiB
Raw Blame History

نکات پیاده‌سازی — تسک ۰۰

۱. اول تست خط سرخ، بعد هر چیز دیگر

ترتیب کار:

۱. 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/ انجام می‌دهد. از همان استفاده کن.

۱۰. ReserveAppointmentsPageDataTable

صفحه امروز جدول خام با <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[] 404TenantOwnershipChecker پیش از هر بررسی دیگر
durations override با مقدار ۰ یا منفی نادیده گرفته شود (رفتار موجود serviceSlots)
نوبت گذشته service-reschedule422
دو درخواست جابه‌جایی هم‌زمان به یک زمان 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 که ماتریس کامل را ثبت کند — تسک ۰۶ حالت سومی به همین ماتریس اضافه می‌کند.