# پاک‌سازی نوبت‌دهی سرویسی: حذف شعبه، تعطیلات سراسری، تنظیمات منابع، تایم‌لاین یکپارچه

## پروژه

`clinicpro` (بک‌اند Symfony + پنل ادمین React).

یک وظیفه **cross-repo** است و علامت‌گذاری شده: حذف شعبه به `nobat724_front` می‌رسد
(`nobat724_front/services/response.js:165` اندپوینت `doctor-address/{id}` را صدا می‌زند).

## زمینه

مدل Resource-First پیاده شده است: منبع، سرویس، گزینهٔ سرویس، دستهٔ سراسری. حالا مالک
محصول می‌خواهد لایه‌هایی که در این مدل مصرف‌کننده ندارند برداشته شوند (شعبه/اتاق، گروه‌های
انتخاب، تب بخش‌های نوبت)، تعطیلات یک بار سراسری تعریف شود، و تنظیمات نوبت‌دهی منابع
هم‌شکل پزشکان شود.

## مشکل / هدف

پنج تغییر مستقل، به همین ترتیب:

1. **حذف شعبه و اتاق** از محصول — بدون تغییر منطق نوبت‌دهی.
2. **تعطیلات سراسری**: مدیر سیستم تعطیلات رسمی سال را ثبت کند؛ هر محیط بتواند
   غیرفعالشان کند؛ پزشک و منبع تعطیلی اختصاصی خودشان را داشته باشند.
3. **تب منابع** در `/admin/settings/appointment-settings`، هم‌شکل تب پزشک.
4. **حذف تب‌های «گروه‌ها و آیتم‌ها» و «بخش‌های نوبت»** از صفحهٔ سرویس.
5. **تایم‌لاین یکپارچه**: پزشکانِ سرویسی و منابعِ قابل‌رزرو در یک نما، با ظرفیت و وقت آزاد.

---

## ⚠️ نقد پرامپت — قبل از شروع بخوان

خواستهٔ «همه‌چیز شعبه حذف شود، شامل `DoctorAddress`» با «منطق نوبت‌دهی بدون تغییر بماند»
قابل جمع نیست. شواهد از خود کد و دیتابیس:

| شاهد | یعنی |
|---|---|
| `appointments.address_id` — **۷۵ از ۷۵ نوبت مقدار دارد** | آدرس، محلِ خودِ نوبت است نه یک بخش تنظیمات |
| `nobat724_front/services/response.js:165` → `api/v1/clinic-pro/doctor-address/{id}` | سایت عمومی روی همین قرارداد رزرو می‌گیرد |
| `ClinicResource::__construct()` → `assignTenantPair($address->tenantEntityType(), …)` | جفت محیط هر منبع از آدرس مشتق می‌شود |
| `price_lists.address_id` · `resource_pools.address_id` · `service_branch_overrides.address_id` | سه زیرسیستم دیگر هم به آن گره خورده‌اند |

پس **`DoctorAddress` در این پرامپت حذف نمی‌شود**؛ به یک لنگرِ نامرئی تنزل می‌کند: هیچ
صفحه، منو یا مفهومی به کاربر نشان نمی‌دهد، ولی جدولش سر جایش می‌ماند. آنچه واقعاً حذف
می‌شود، دامنهٔ `Branch` است (اتاق، ساعت کاری شعبه، صفحه‌ها، اندپوینت‌ها).

حذف کامل `DoctorAddress` یک پرامپت جداست و بازنویسی جریان رزرو در **دو ریپو** را می‌خواهد؛
قبل از شروع باید مالک محصول هزینه‌اش را ببیند. اگر پس از دیدن این ارقام باز هم حذف کامل
خواسته شد، همان‌جا توقف کن و تکلیف را بپرس — با این پرامپت انجامش نده.

---

## معیار پذیرش

### قابلیت ۱ — حذف شعبه و اتاق

- ✅ موفق: `/admin/branches` و زیرصفحه‌هایش ۴۰۴ می‌دهند، آیتم «شعبه‌ها و اتاق‌ها» از منوی
  تنظیمات رفته، و `ddev exec php bin/phpunit` کامل سبز است — یعنی جستجوی آزاد و رزرو
  دقیقاً همان نتایج قبلی را می‌دهد.
- ❌ خطا: `GET /api/v1/branches` → **۴۰۴** (روت وجود ندارد)، نه ۵۰۰.
- ⚠️ مرزی: منبعی که `subject_kind = 'room'` دارد باید همچنان کار کند — اتاق به‌عنوان
  *منبع* می‌ماند، فقط موجودیت `Room` می‌رود.

### قابلیت ۲ — تعطیلات سراسری

- ✅ موفق: با توکن `ROLE_ADMIN`، `POST /api/v1/admin/national-holidays` یک تعطیل می‌سازد و
  همان روز بلافاصله در `GET /api/v1/resource/{uuid}/availability` با دلیل
  `national_holiday` خالی برمی‌گردد.
- ❌ خطا: همان `POST` با توکن پزشک → **۴۰۳**.
- ⚠️ مرزی: محیطی که `TenantHolidayOverride(is_working = true)` دارد، همان روز **باز**
  است و ساعتش برمی‌گردد.

### قابلیت ۳ — تب منابع در تنظیمات نوبت‌دهی

- ✅ موفق: در `/admin/settings/appointment-settings` تب «منابع» ساعت کاری هفتگی، تاریخ‌های
  خاص و تعطیلات هر منبع را می‌دهد و ذخیره‌اش در `GET /api/v1/resource/{uuid}/calendar`
  دیده می‌شود.
- ❌ خطا: منشیِ بدون مجوز `appointment_settings.update` فیلدها را read-only می‌بیند و
  `PUT` سرور ۴۰۳ می‌دهد.
- ⚠️ مرزی: کلینیکِ بدون هیچ منبعی، حالت خالی با لینک «تنظیمات ← منابع» نشان دهد، نه صفحهٔ سفید.

### قابلیت ۴ — حذف تب‌های سرویس

- ✅ موفق: `/admin/service/{uuid}` پنج تب دارد (اطلاعات، تعرفه‌ها، بیمه‌ها، کالاها،
  دسته‌بندی‌ها، لاگ) و هیچ ورودی به گروه‌ها و بخش‌های نوبت ندارد.
- ❌ خطا: باز کردن مستقیم `?tab=segments` به تب اطلاعات برگردد، نه خطای رندر.
- ⚠️ مرزی: سرویسی که همین حالا `SegmentTemplate` دارد باید **دقیقاً مثل قبل** رزرو شود —
  تست‌های موجود `tests/Appointment` سبز بمانند.

### قابلیت ۵ — تایم‌لاین یکپارچه

- ✅ موفق: نمای «زمانبندی» هم ردیف پزشکانِ سرویسی و هم ردیف منابعِ قابل‌رزرو را نشان دهد،
  با بازهٔ اشغال و وقت آزاد و ظرفیت هر ردیف.
- ❌ خطا: روزی که هیچ ردیفی داده ندارد، پیام خالیِ صریح بدهد نه اسکلتِ همیشگی.
- ⚠️ مرزی: منبعی با `capacity = 3` و دو نوبت هم‌زمان، «۱ ظرفیت آزاد» نشان دهد نه «پر».

---

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `src/Branch/` | کل دامنه: `BranchController`, `Room`, `BranchWorkingHours`, `RoomService`, `WorkingHoursService`, `BranchResolver` |
| `src/Resource/Service/ResourceAvailabilityService.php` | تنها مصرف‌کنندهٔ `BranchWorkingHoursRepository` بیرون از `src/Branch` |
| `src/Resource/Entity/ClinicResource.php` | `subject_kind='room'` و `ResourceLinker` به `Room` وصل‌اند |
| `src/Resource/Controller/HolidayController.php` | `GET /national-holidays` + `POST/DELETE /holiday-overrides` — **POST برای national ندارد** |
| `src/Resource/Entity/NationalHoliday.php` · `TenantHolidayOverride.php` | مدل تعطیلات، از قبل درست است |
| `src/Appointment/Controller/AppointmentSettingsController.php` | تعطیلی اختصاصی پزشک (`Holiday`) |
| `assets/admin/pages/HolidaysSettingsPage.tsx` | صفحهٔ `/admin/holidays` |
| `assets/admin/pages/ClinicAppointmentSettingsPage.tsx` | تب‌بندی per پزشک با `.seg` |
| `assets/admin/pages/ServiceDetailPage.tsx` | `TABS` — گروه‌ها و بخش‌های نوبت اینجاست |
| `assets/admin/pages/AppointmentsPage.tsx` · `components/appointments/ResourceTimeline.tsx` · `TurnsTimeline.tsx` | سه نمای فعلی |
| `assets/admin/components/resources/ResourceWorkingHoursPanel.tsx` · `ResourceExceptionsPanel.tsx` | پنل‌های آمادهٔ منبع — در تب جدید همین‌ها مصرف می‌شوند |

## وضعیت فعلی

`ResourceAvailabilityService` ساعت واقعی منبع را از تقاطع با ساعت شعبه می‌سازد:

```php
// src/Resource/Service/ResourceAvailabilityService.php
private readonly BranchWorkingHoursRepository    $branchHours,
…
$branchByDay = $this->branchHoursByDay($resource);
…
if ($branchByDay !== null) {
    $branchWindows = $branchByDay[$dayOfWeek] ?? [];
    if ($branchWindows === []) {
        // روز، بدون ساعت شعبه یعنی بسته
```

`HolidayController` فقط خواندن تعطیلات ملی را دارد؛ هیچ مسیری برای ساختنشان نیست:

```php
#[Route('/api/v1/national-holidays', name: 'national_holidays_list', methods: ['GET'])]
#[Route('/api/v1/holiday-overrides',  name: 'holiday_override_create', methods: ['POST'])]
#[Route('/api/v1/holiday-override/{uuid}', name: 'holiday_override_delete', methods: ['DELETE'])]
```

`ServiceDetailPage` هفت تب دارد:

```tsx
const TABS = [
  { id: 'info',      label: 'اطلاعات سرویس' },
  { id: 'tariffs',   label: 'تعرفه‌ها' },
  { id: 'insurance', label: 'بیمه‌ها' },
  { id: 'groups',    label: 'گروه‌ها و آیتم‌ها' },
  { id: 'segments',  label: 'بخش‌های نوبت' },
  { id: 'categories',label: 'دسته‌بندی‌ها' },
  { id: 'goods',     label: 'کالاهای مرتبط' },
  { id: 'history',   label: 'لاگ تغییرات' },
] as const;
```

---

## وظایف

### ۱. حذف دامنهٔ شعبه و اتاق

**دامنهٔ حذف:** `src/Branch/` کامل، سه صفحهٔ پنل (`BranchesPage`, `BranchRoomsPage`,
`BranchWorkingHoursPage`)، آیتم `branches` در `settingsMenu.ts`، و روت‌هایشان در `App.tsx`.

**دامنهٔ نگه‌داشتن:** `DoctorAddress` (لنگر محیط و محل نوبت — بالا را بخوان).

قبل از حذف، مهاجرت منابعِ نوع اتاق:

```php
// ClinicResource.subject_kind === 'room' امروز به rooms.id اشاره می‌کند.
// یک migration، آن ردیف‌ها را به منبع بی‌subject تبدیل می‌کند (نامشان می‌ماند):
UPDATE clinic_resources SET subject_kind = NULL, room_id = NULL WHERE subject_kind = 'room';
```

سپس لایهٔ شعبه از موتور دسترس‌پذیری برداشته می‌شود. **این تنها جای منطق است که واقعاً
تغییر می‌کند**، پس صریح بنویسش:

```php
// ResourceAvailabilityService: تزریق BranchWorkingHoursRepository حذف، و
// $branchByDay همه‌جا null می‌شود → لایهٔ «ساعت شعبه» از کسر بیرون می‌رود.
// دلیل معماری: با حذف شعبه، تنها مرجع ساعت کاری، شیفت خودِ منبع است.
```

دلیلِ `outside_branch_hours` و `branch_closed` و `branch_inactive` از
`REASON_LABELS` فرانت هم برداشته شوند (`ResourceExceptionsPanel.tsx`).

**نحوه تست:**
```bash
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/phpunit                      # همه سبز — مخصوصاً tests/Appointment و tests/Resource
ddev exec php bin/console debug:router | grep -c "branch\|room"   # باید 0 باشد
npx vitest run                                  # خط پایه: ۱۰۰ فایل / ۶۶۰ تست
```
و یک رزرو واقعی از مسیر عمومی بگیر (`POST /api/v1/appointment-availability` سپس
hold → confirm) تا ثابت شود همان اسلات‌های قبلی برمی‌گردند.

**cross-repo:** بعد از حذف، در `nobat724_front` دنبال `doctor-address` بگرد و گزارش بده
کدام صفحه‌ها مصرفش می‌کنند. اگر اندپوینت عمومی `clinic-pro/doctor-address/{id}` را دست
نزدی (نباید بزنی)، سایت نمی‌شکند — همین را صریح در گزارش بنویس.

---

### ۲. تعطیلات سراسری، سه لایه

مدل از قبل درست است و ساخته نمی‌شود؛ فقط سه چیزِ کم اضافه می‌شود.

**الف) CRUD مدیر سیستم روی `NationalHoliday`:**

```php
// src/Resource/Controller/HolidayController.php
#[IsGranted('ROLE_ADMIN')]
#[Route('/api/v1/admin/national-holidays', methods: ['POST'])]     // {jalali_date, title}
#[Route('/api/v1/admin/national-holiday/{uuid}', methods: ['PATCH','DELETE'])]
```

`jalali_date` ورودی است و `date` (نیمه‌شب تهران) و `jalali_year` از آن مشتق می‌شوند —
مسئولیت تبدیل در یک Service بماند، نه Controller.

**ب) نمایش تعطیلات سراسری در تب تعطیلاتِ پزشک و منبع:** هر دو تب علاوه بر تعطیلی
اختصاصی، فهرست تعطیلات ملی سال را **فقط‌خواندنی** با یک سوییچ «این روز باز هستیم» نشان
دهند؛ سوییچ همان `POST /api/v1/holiday-overrides` موجود را صدا بزند.

**ج) `/admin/holidays`** جای مدیریت سراسری هر محیط بماند (همین حالا هست) و در توضیح
صفحه بنویسد که این‌ها پیش‌فرضِ همهٔ پزشکان و منابع‌اند.

**نحوه تست:**
```bash
# ✅ ادمین می‌سازد
curl -X POST .../api/v1/admin/national-holidays -H "Authorization: Bearer $ADMIN" \
     -d '{"jalali_date":"1405-01-13","title":"سیزده‌بدر"}'
# ✅ همان روز در دسترس‌پذیری منبع خالی است، با دلیل national_holiday
curl ".../api/v1/resource/$R/availability?from=…&to=…" -H "Authorization: Bearer $DOC"
# ❌ پزشک نمی‌سازد → 403
# ⚠️ بعد از POST /holiday-overrides با is_working=true همان روز باز می‌شود
```
تست PHPUnit: `tests/Resource/NationalHolidayEndpointTest.php` با هر سه سناریو.

---

### ۳. تب «منابع» در تنظیمات نوبت‌دهی کلینیک

در `ClinicAppointmentSettingsPage` یک سطح تب بالاتر اضافه کن: **پزشکان | منابع**. سطح
دوم برای منابع همان الگوی فعلی است (یک `.seg` با نام هر منبع).

کامپوننت جدید لازم نیست — پنل‌ها ساخته شده‌اند:

```tsx
{scope === 'resources' && selectedResource && (
  <div key={selectedResource}>
    <ResourceWorkingHoursPanel resourceUuid={selectedResource} canUpdate={canUpdate} />
    <ResourceExceptionsPanel  resourceUuid={selectedResource} canUpdate={canUpdate} />
  </div>
)}
```

`.seg` نکته دارد: کلاس فعالش `on` است (`active` هم alias شده) — تب بدون آن هیچ نشانه‌ای
ندارد.

**نحوه تست:** `npx vitest run assets/admin/pages/ClinicAppointmentSettingsPage.test.tsx`
با سه تست: تب منابع فهرست منابع را می‌دهد · انتخاب منبع پنل ساعت کاری را می‌آورد ·
کلینیک بدون منبع حالت خالی می‌دهد. سپس اسکرین‌شات:
`node .claude/skills/redesign-page/driver.mjs variants "https://clinic-pro.ddev.site/admin/settings/appointment-settings"`

---

### ۴. حذف دو تب از صفحهٔ سرویس

فقط **UI** حذف می‌شود: دو ورودی از `TABS` و رندرشان، به‌علاوهٔ کامپوننت‌های
`ServiceGroupsTab` و `ServiceSegmentsTab` و تست‌هایشان.

`SegmentTemplate`، `ServiceSelectionGroup`، `ServiceItemRelation` و اندپوینت‌هایشان
**می‌مانند**: `AppointmentPlanBuilder` ورودی‌اش همین‌هاست و سرویسی که امروز اتاق و دستگاه
را با هم می‌گیرد، بدونشان می‌شکند. سرویسِ بدون template هم از قبل با `singleSegment()`
رزرو می‌شود، پس حذف تب هیچ رفتاری را عوض نمی‌کند.

`tab` در URL می‌نشیند؛ مقدار ناشناخته باید به `info` برگردد نه اینکه چیزی رندر نشود.

**نحوه تست:** `npx vitest run assets/admin/pages/ServiceDetailPage.test.tsx` +
`ddev exec php bin/phpunit tests/Appointment` (باید بدون تغییر سبز بماند) + باز کردن
`/admin/service/{uuid}?tab=segments` و دیدن تب اطلاعات.

---

### ۵. تایم‌لاین یکپارچه

سه نمای فعلی (`table` · `timeline` · `resources`) به دو نما می‌رسند: **جدولی** و
**زمانبندی**. نمای زمانبندی دو گروه ردیف دارد:

```
پزشکان   ← پزشکانی که حالت نوبت‌دهی‌شان service است
منابع    ← منابعی که برای سرویسی قابل رزروند (ResourceServiceOffering فعال دارند)
```

هر ردیف باید سه چیز بدهد: بازهٔ اشغال، وقت آزاد، و ظرفیت. برای منبع، «آزاد» یعنی
`capacity` منهای تعداد اشغال هم‌پوشان در آن لحظه — نه صفر و یک؛ منبعِ ظرفیت‌۳ با دو نوبت
هم‌زمان هنوز یک جا دارد.

سمت سرور، `GET /api/v1/resources/timeline` موجود را توسعه بده (ساخت اندپوینت جدید ممنوع
است تا وقتی این کافی است): فیلتر `only_bookable=1` و فیلد `free_slots` به هر ردیف اضافه
شود. ردیف پزشک از همان `slots` فعلی می‌آید و در فرانت با ردیف منابع در یک نما ادغام
می‌شود.

**نحوه تست:** تست PHPUnit برای `free_slots` روی منبع ظرفیت‌۳ با دو اشغال هم‌پوشان
(انتظار: ۱)؛ `npx vitest run assets/admin/components/appointments/`؛ و اسکرین‌شات نمای
زمانبندی در تاریخ `2026-08-05` که دادهٔ واقعی دارد.

---

## نکات مهم

- **ترتیب اجرا اجباری است.** قابلیت ۱ موتور دسترس‌پذیری را تغییر می‌دهد؛ اگر بعد از
  قابلیت ۵ انجام شود، تایم‌لاین را می‌شکند و علتش پیدا نیست.
- **خط پایهٔ تست را اول بگیر:** `ddev exec php bin/phpunit` و `npx vitest run`
  (۱۰۰ فایل / ۶۶۰ تست، همه سبز در ۲۰۲۶-۰۸-۰۲). هر شکستی بعد از این، مالِ همین کار است.
- **مسیر اسلاتی دست نخورد.** تست‌های `--group=slot-mode-frozen` نگهبانند؛ حتی یک شکست
  یعنی توقف.
- **`vitest` داخل ddev اجرا نمی‌شود** — روی هاست با `npx vitest run`.
- **دو صفحه، یک منوی تنظیمات:** `settingsMenu.ts` منبع واحد سایدبار دسکتاپ و فهرست
  موبایل است. حذف آیتم شعبه فقط همان‌جا انجام شود.
- **مستندات همین جلسه:** `docs/api/branch.md` حذف، `docs/api/resource.md` و
  `docs/api/resource-calendar.md` و `docs/api/clinic-services.md` به‌روز، و
  `docs/architecture/resource-first-model.md` باید بگوید شعبه از مدل بیرون رفته.
- **الگو:** برداشتن لایهٔ شعبه از `ResourceAvailabilityService` را به‌صورت حذف یک لایه از
  زنجیرهٔ کسر انجام بده (همان ساختار فعلی)، نه با `if` تازه — کلاس باید کوچک‌تر شود نه شاخه‌دارتر.
- **دادهٔ تست:** `ddev exec php bin/console app:seed-scenarios --reset -n` سه سناریو را
  می‌سازد؛ کاربرها در `TEST_USERS.md`. کاربر `0912000301` رمز ندارد — از `0912000201`
  استفاده کن.
