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

205 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نکات پیاده‌سازی — تسک ۰۰
## ۱. اول تست خط سرخ، بعد هر چیز دیگر
ترتیب کار:
```
۱. tests/Appointment/SlotModeFrozenTest.php + سه fixture ← اول این
۲. ddev exec php bin/phpunit --group=slot-mode-frozen ← باید سبز باشد قبل از هر تغییری
۳. بقیهٔ تسک
۴. دوباره گام ۲ — باید همچنان سبز باشد
```
اگر fixture را بعد از تغییرات بسازی، هیچ چیزی را تضمین نکرده‌ای — snapshot وضعیت
تغییریافته را گرفته‌ای.
## ۲. `isServiceMode` گِیت همه‌چیز است
هر خط کد جدید در مسیر مشترک باید داخل این شرط باشد:
```php
if ($this->serviceCalc->isServiceMode($doctor, $clinic)) {
// … منطق جدید
}
```
نه بیرونش، نه با `??`، نه با «اگر سرویس دارد». معیار **فقط** `booking_mode` است.
نوبت اسلاتی هم می‌تواند `service_item_id` داشته باشد (فیلدهای Figma نوبت‌ها) — آن دلیل
سرویسی بودن نیست.
اشتباه رایج:
```php
// ❌ نوبت اسلاتیِ دارای سرویس را وارد مسیر جدید می‌کند
if ($appointment->getServiceItems()->count() > 0) { }
```
## ۳. جمع سادهٔ مدت را همین‌جا اصلاح نکن
```php
$totalMinutes += $duration; // ← اشتباه است، ولی دست نزن
```
مستند بند ۵ می‌گوید این فرمول ظرفیت را الکی پر می‌کند و راه‌حلش «زمان تنها / زمان اضافه»
است — که تسک ۰۴ می‌سازد. اصلاحش اینجا یعنی:
- مدت همهٔ نوبت‌های چندسرویسیِ در حال رزرو یک‌شبه کم می‌شود
- سایت و اپ دسکتاپ عدد متفاوت می‌بینند بدون اینکه چیزی در build بشکند
- و هیچ داده‌ای برای «زمان اضافه» وجود ندارد، پس اصلاح بی‌ورودی غیرممکن است
کاری که این تسک می‌کند: محاسبه را به **یک نقطه** منتقل می‌کند تا تسک ۰۴ یک خط عوض کند.
## ۴. `excludeAppointmentId` — الگوی موجود را تکرار کن
`AppointmentRepository::isSlotTaken($doctor, $start, $end, $excludeId)` از قبل این پارامتر
را دارد. `findBusyIntervals` هم همان را بگیرد، با همان نام و همان جای پارامتر و همان
پیش‌فرض `null`.
دو الگوی متفاوت برای یک کار (مثلاً یکی `?int $excludeId`، دیگری `array $excludeIds`)
یعنی اولین کسی که هر دو را می‌بیند یکی را اشتباه صدا می‌زند.
## ۵. اعتبارسنجی زمان: عضویت در فهرست، نه «اشغال نبودن»
```php
// ❌ ناکافی
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` برای سرویس غیرفعال در نوبت موجود
```php
$duration = $this->serviceCalc->calculate(, allowInactive: true);
// $duration->warnings === ['سرویس «لیزر صورت» دیگر برای نوبت‌دهی فعال نیست']
```
نوبت موجود با سرویسی که کلینیک غیرفعالش کرده، باید قابل جابه‌جایی و لغو بماند. `422`
یعنی آن نوبت برای همیشه قفل می‌شود و منشی هیچ کاری نمی‌تواند بکند.
ولی **افزودن** سرویس غیرفعال به نوبت → `422`. تفاوتش `allowInactive` است که فقط برای
uuid های موجودِ نوبت `true` می‌شود، نه برای uuid های تازه‌ی درخواست.
## ۷. `replaceServiceItems` باید `serviceItem` تکی را هم‌گام کند
```php
$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)`
```php
public function setIsReserve(bool $v): self
{
$this->isReserve = $v;
$this->refreshActiveSlotKey(); // ← اگر نیست، اضافه کن
$this->updatedAt = time();
return $this;
}
```
بدون آن، رزروِ تبدیل‌شده `active_slot_key = NULL` می‌ماند و دو نفر می‌توانند همان ساعت
را بگیرند. **این تنها تغییر مجاز در مکانیزم `active_slot_key` است** و فقط چون یک شرط
موجود را اعمال می‌کند، نه عوضش می‌کند. تست: `ConvertReserveSlotKeyTest`.
## ۹. تبدیل رزرو، اتمی
```php
$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](../_shared/ui-conventions.md)
است و در دارک‌مود می‌شکند.
این «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` که ماتریس کامل را ثبت کند —
تسک ۰۶ حالت سومی به همین ماتریس اضافه می‌کند.