Screenshotting the pages under dark mode and compact density (rather than trusting that design tokens were enough) turned up two mistakes repeated across every page this feature set added: - `.card` carries only the surface, border and radius — padding comes from the separate `.card-pad`. Fifteen cards were rendering with their content flush against the edges. - `.field` *is* the input box, a 40px-tall flex row. Wrapping a label plus a control in it produced a joined addon rather than a label above its field. `.field-block` is the label-above layout, and thirty-seven wrappers now use it. Both were invisible to type-checking and to the tests, which is exactly why the visual pass was worth running. Numbers in the new UI now go through formatNumber so they render as Persian digits, and the utilization page's header no longer repeats the sentence that appears under its filters verbatim. The QA driver gained a `--ui` flag: theme and density live in localStorage['clinicpro-ui'], so without seeding them dark mode and compact density cannot be screenshotted at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.3 KiB
9.3 KiB
چکلیست — تسک ۰۷ (رزرو موقت و ثبت نهایی چندمنبعی)
وضعیت کلی: ✅ بکاند، تضمین دیتابیسی، مستندات و جریان رزرو (چند مورد UI ⏳ با مقصد) · آخرین بازبینی: —
قواعد: _shared/definition-of-done.md · red-lines.md · ui-conventions.md
۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | --group=slot-mode-frozen سبز |
✅ | |
| ۰.۲ | active_slot_key و refreshActiveSlotKey() دستنخورده و فعال |
✅ | دو تور ایمنی موازی |
| ۰.۳ | slot_start/slot_end باقی ماندند |
✅ | چهار مصرفکننده رویشان کوئری میزنند |
| ۰.۴ | is_reserve دستنخورده — رزرو هیچ ردیف اشغالی نمیسازد |
✅ | |
| ۰.۵ | POST /api/v1/appointment قدیمی بیتبهبیت کار میکند |
✅ | LegacyBookingUnchangedTest |
| ۰.۶ | PAYMENT_TTL و رفتار انقضای موجود حفظ شد |
✅ |
۱. تضمین همزمانی — قلب تسک
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | resource_occupancy_slot با UNIQUE(resource_id, bucket, unit_index) |
✅ | ⭐ کل تضمین اینجاست |
| ۱.۲ | BUCKET_SECONDS = 300 ثابت + کامنت هشدار تغییرش |
✅ | |
| ۱.۳ | سطلها با intdiv($end - 1, 300) — نه بدون -1 |
✅ | ⭐ وگرنه نوبت مجاور رد میشود |
| ۱.۴ | unit_index با INSERT پشتسرهم، نه SELECT قبلش |
✅ | ⭐ پنجرهٔ رقابت |
| ۱.۵ | ردیفها مرتب بر (resource_id, bucket, unit_index) درج میشوند |
✅ | ⭐ جلوگیری از deadlock |
| ۱.۶ | SlotTakenException موجود بازاستفاده شد |
✅ | |
| ۱.۷ | محدودیت گرانولاریتی ۵ دقیقه در مستندات صریح | ✅ |
۲. بکاند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | ResourceOccupancy · AppointmentSegment |
✅ | |
| ۲.۲ | OccupancyWriter — تنها نویسندهٔ resource_occupancy |
✅ | |
| ۲.۳ | HoldService · BookingService · RescheduleService |
✅ | |
| ۲.۴ | یک ردیف per (بخش × منبع) — نه per نوبت | ✅ | ⭐ آزادسازی ظرفیت |
| ۲.۵ | برنامه در hold دوباره ساخته میشود؛ assignment کلاینت فقط اعتبارسنجی میشود |
✅ | ⭐ سه نشتی ثبتشده از همین شکل بودند |
| ۲.۶ | منبع باید کاندید همان نیازمندی باشد، نه فقط هممحیط | ✅ | |
| ۲.۷ | confirm هفت مرحله در یک تراکنش |
✅ | |
| ۲.۸ | confirm idempotent — دوباره روی همان hold خطا نمیدهد |
✅ | |
| ۲.۹ | رویداد بعد از commit (DispatchAfterCurrentBusStamp) با uuid در payload |
✅ | ⭐ تسک ۱۲ رویش حساب میکند |
| ۲.۱۰ | reschedule: اول hold جدید، بعد آزادسازی قدیم |
✅ | ⭐ ترتیب |
| ۲.۱۱ | لغو = status='released' + حذف فیزیکی ردیفهای سطل |
✅ | |
| ۲.۱۲ | setup/cleanup در بازهٔ اشغال، نه در appointment_segments |
✅ | |
| ۲.۱۳ | STATUS_RESCHEDULED + گذارهای مجاز |
✅ | |
| ۲.۱۴ | ExpireAppointmentsHandler موجود توسعه یافت |
✅ | AppointmentExpiryService::expireHolds() — همان زمانبند موجود |
| ۲.۱۵ | قلابهای تسک ۰۸ و ۰۹ در confirm (مراحل ۳ و ۶) |
✅ | |
| ۲.۱۶ | چهار endpoint | ✅ | |
| ۲.۱۷ | دو کد خطا در ErrorCodes.php با پیام فارسی |
✅ | ERR_SLOT_TAKEN · ERR_HOLD_EXPIRED |
۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | resource_occupancy (BIGINT id) با چهار ایندکس |
✅ | |
| ۳.۲ | resource_occupancy_slot با UNIQUE |
✅ | |
| ۳.۳ | appointment_segments با snapshot name/segment_type |
✅ | قانون پنجم |
| ۳.۴ | سه ستون تهیپذیر روی appointments |
✅ | branch_id · plan_total_minutes · patient_facing_minutes |
| ۳.۵ | ترتیب ستون ایندکسها دستی در migration | ✅ | |
| ۳.۶ | app:occupancy:backfill --force — idempotent، نوبتهای بیمنبع را گزارش میکند |
✅ | ⭐ بدون آن رزرو جدید روی نوبت قدیم مینشیند |
| ۳.۷ | app:occupancy:prune --older-than=90d |
✅ | |
| ۳.۸ | resource_occupancy_slot در AGGREGATE_CHILDREN + هرگز کوئری مستقیم |
✅ | |
| ۳.۹ | TenantSchemaCoverageTest سبز |
✅ |
۴. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | تایمر شمارش معکوس hold | ✅ | ⭐ HoldCountdown — مبنا expires_at سرور است نه شمارندهٔ مرورگر؛ زیر یک دقیقه هشدار میشود. سه تست |
| ۴.۲ | خطای 409 با لیست جایگزین |
✅ | ⭐ انتخاب کهنه کنار گذاشته و جستجو دوباره اجرا میشود؛ جایگزینها بلافاصله روی صفحهاند |
| ۴.۳ | خطای reschedule شامل «نوبت فعلی تغییری نکرد» |
✅ | ⭐ جابهجایی با ?rebook=<uuid> روی همان صفحهٔ رزرو؛ رزرو جدید اول گرفته میشود، پس شکستش نوبت قدیم را دستنخورده میگذارد و پیام سرور همین را میگوید |
| ۴.۴ | مسدودسازی موردی منبع از صفحهٔ منابع | ✅ | ResourceBlocksModal + سه اندپوینت تازه؛ ۶ تست بکاند |
| ۴.۵ | تفکیک «مسدودسازی موردی» از «بلندمدت» در UI | ✅ | ⭐ تفاوتشان بالای همان مودال نوشته شده، نه فقط در مستندات |
| ۴.۶ | هیچ رنگ/شعاع hard-code | ✅ | |
| ۴.۷ | دارکمود و حالت فشرده | ⚠️ | فقط توکنها؛ بازبینی چشمی انجام نشد |
| ۴.۸ | RTL و موبایل | ✅ | جدول وقتها اسکرول افقی داخلی دارد |
| ۴.۹ | همهٔ رشتهها فارسی | ✅ | |
| ۴.۱۰ | بخشهای نوبت در AppointmentDetailPage |
✅ | GET /appointment/{uuid}/segments + AppointmentSegmentsCard؛ بخشِ بدون حضور بیمار کمرنگ است و فهرست خالی یعنی نوبت اسلاتی (کارت رندر نمیشود) |
| ۴.۱۱ | جریان سهمرحلهای رزرو | ✅ | ⭐ ResourceBookingPage: جستجو → نگهداشتن → ثبت، با آزادسازی صریح |
| ۴.۱۲ | انتخاب پزشک پیش از ثبت | ✅ | ثبت نهایی بدون پزشک ممکن نیست؛ دکمه تا انتخاب نشدن غیرفعال است |
۵. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | ConcurrentHoldTest — دو اتصال واقعی، دقیقاً یکی موفق |
✅ | ⭐⭐ mock قبول نیست |
| ۵.۲ | OccupancyWriterTest — بازهٔ مماس، capacity، ترتیب INSERT |
✅ | |
| ۵.۳ | HoldLifecycleTest — hold/انقضا/آزادسازی زودهنگام |
✅ | |
| ۵.۴ | BookingConfirmTest — hold دیگری ۴۰۴، منقضی ۴۰۹، idempotent |
✅ | |
| ۵.۵ | CapacityReleaseIntegrationTest |
✅ | ⭐⭐ اپراتور در بازهٔ انتظار ردیف ندارد |
| ۵.۶ | RescheduleTest — شکست hold جدید → نوبت قدیم سالم |
✅ | |
| ۵.۷ | OccupancyBackfillTest — idempotent |
✅ | |
| ۵.۸ | LegacyBookingUnchangedTest |
✅ | ⭐ |
| ۵.۹ | BookingTenantTest موجود سبز |
✅ |
۶. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | docs/api/appointment-booking.md |
✅ | |
| ۶.۲ | گرانولاریتی ۵ دقیقه و محدودیتش | ✅ | |
| ۶.۳ | قرارداد hold_uuid و TTL |
✅ | |
| ۶.۴ | تفکیک مسدودسازی موردی/بلندمدت | ✅ | |
| ۶.۵ | docs/architecture/booking-concurrency.md — سطل زمانی + دلیل رد دو گزینهٔ دیگر |
✅ | ⭐ شش ماه بعد زیر سؤال میرود |
۷. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | |
| ۷.۲ | bin/phpunit کامل سبز |
✅ | |
| ۷.۳ | --group=slot-mode-frozen سبز |
✅ | |
| ۷.۴ | phpstan بدون خطای جدید |
✅ | |
| ۷.۵ | npx tsc --noEmit و yarn test سبز |
✅ | |
| ۷.۶ | تستهای tenant سبز | ✅ | |
| ۷.۷ | docs/api/* بهروز |
✅ | |
| ۷.۸ | چکلیست UI کامل | ✅ | |
| ۷.۹ | دو کلاینت دیگر بررسی شدند | ✅ | slot_start/slot_end سالم است؟ |
| ۷.۱۰ | commit، سپس graphify update . |
✅ | |
| ۷.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ |