diff --git a/.claude/prompt/resource-first-appointments.md b/.claude/prompt/resource-first-appointments.md
new file mode 100644
index 00000000..eaf95149
--- /dev/null
+++ b/.claude/prompt/resource-first-appointments.md
@@ -0,0 +1,324 @@
+# نوبتدهی منبعمحور در صفحهٔ نوبتها — تب هر منبع + رزرو سرویسی + حذف تایملاین منابع
+
+## زمینه
+
+صفحهٔ `/admin/appointments` امروز کاملاً پزشکمحور است: تبها فقط پزشکاند
+(`AppointmentsPage.tsx:841`)، و منابع فقط یک نوار **فقطخواندنی** زیر زمانبندی دارند
+(`AppointmentsPage.tsx:892-903`) که هیچ اقدامی روی آن ممکن نیست.
+
+در مدل Resource-First، منبع واحد ظرفیت است: «لیزر CO2» و «لیزر NdYAG» سرویسهای خودشان
+(`ResourceServiceOffering`)، تقویم خودشان (`ResourceCalendar`) و استثناهای خودشان را دارند.
+ولی کاربر نمیتواند برای آنها نوبت ثبت کند، چون کل مسیر رزرو از پزشک عبور میکند.
+
+## مشکل / هدف
+
+**هدف:** هر منبع مثل پزشک تب خودش را داشته باشد؛ «افزودن نوبت» روی تب یک منبع، مودال
+نوبتدهی **سرویسی** را برای همان منبع باز کند (لیست سرویسهای همان منبع → انتخاب →
+زمان خالی → بیمار → ثبت)؛ و نوار «منابع» زیر زمانبندی حذف شود.
+
+**سه مانع واقعی در کد امروز:**
+
+۱. **نوبت بدون پزشک ممکن نیست.** `Appointment::$doctor` با `nullable: false` تعریف شده
+ (`Appointment.php:100-101`) و سازندهٔ entity هم `Doctor` میگیرد (`Appointment.php:256`).
+ `book()` بدون `doctor_uuid` خطای ۴۲۲ میدهد (`AppointmentController.php:484`) و منبع فقط
+ وقتی پزشک پیدا میکند که خودش پزشک باشد (`AppointmentController.php:471-473`). یعنی
+ دستگاهِ بدون پزشک اصلاً قابل رزرو نیست.
+
+۲. **اسلات سرویسی فقط پزشکمحور است.** `GET /api/v1/appointment-service-slots` پزشک
+ میخواهد و شرط میکند `booking_mode` همان پزشک `service` باشد
+ (`AppointmentController.php:193-209`). منبع `WeeklySchedule` ندارد.
+
+۳. تایملاین منابع باید حذف شود.
+
+**تصمیم گرفتهشده (توسط کاربر):** مسیر «نوبت بدون پزشک» — `doctor` تهیپذیر شود.
+
+### چرا این تصمیم آنقدر که بهنظر میرسد پرریسک نیست (شواهد از کد)
+
+- `Appointment::$resource` **از قبل وجود دارد** و تهیپذیر است (`Appointment.php:174-176`).
+- تداخل منابع **از قبل در سطح دیتابیس** تضمین شده، نه در کد: `OccupancyBucket` با
+ `UniqueConstraint('uniq_bucket_resource_seat', ['resource_id','bucket_at','seat'])`
+ (`OccupancyBucket.php:22`). پس `activeSlotKey` مسئول تداخل **منبع** نیست.
+- `activeSlotKey` فقط دوبارهرزروی **همان پزشک** را میگیرد:
+ `sprintf('%d:%d', $this->doctor->getId(), $this->slotStart)` (`Appointment.php:283`).
+ وقتی پزشکی وجود ندارد، «دوبارهرزروی پزشک» بیمعناست و `null` بودنِ کلید معنای درستی
+ است — نه یک حفرهٔ ایمنی.
+
+**ریسک واقعی و باقیمانده:** ۷۳ فراخوانی `getDoctor()` در ۲۱+ فایل `src/` که همه امروز
+`Doctor` غیرتهی فرض میکنند. این بخش سنگین کار است و باید تکتک بررسی شود.
+
+## معیار پذیرش
+
+- ✅ **موفق:** با توکن مالک کلینیک، `POST /api/v1/appointment` با بدنهٔ
+ `{resource_uuid, service_item_uuids[], slot_start, patient_national_code, patient_gender}`
+ و **بدون** `doctor_uuid`، برای منبعِ دستگاهی (`subject_kind = null`) → `200` و نوبت ذخیره
+ میشود با `doctor_id = NULL` و `resource_id` پرشده. در UI: تب «لیزر CO2» → «افزودن نوبت» →
+ انتخاب سرویس → انتخاب زمان → ثبت → نوبت در فهرست همان تب دیده میشود.
+- ✅ **موفق:** `GET /api/v1/appointment-service-slots?resource_uuid=…&date=…&service_item_uuids[]=…`
+ → `200` با همان شکل پاسخِ حالت پزشک (`start_times[]`, `total_duration_minutes`).
+- ❌ **خطا:** رزرو منبعی که آن سرویس را ارائه نمیدهد → `422` با پیام
+ «این منبع این سرویس را ارائه نمیدهد» و `field = resource_uuid` (این بررسی از قبل در
+ `AppointmentController.php:533-540` هست و باید در مسیر بدونپزشک هم اجرا شود).
+- ❌ **خطا:** رزرو منبعِ محیط دیگر → `422` «منبع یافت نشد» با `field = resource_uuid`.
+- ❌ **خطا:** نه `doctor_uuid` و نه `resource_uuid` → `422` با envelope خطا.
+- ⚠️ **مرزی:** دو رزروِ همزمان روی یک منبع با ظرفیت ۱ در یک بازه → دومی باید با
+ `409` رد شود (از قید یکتای `uniq_bucket_resource_seat`، نه از بررسی در کد).
+- ⚠️ **مرزی:** منبعی که آن روز شیفت ندارد → `start_times` خالی و پیام «زمان خالی کافی
+ نیست»، نه خطای ۵۰۰.
+- ⚠️ **مرزی:** نوبتهای قدیمیِ دارای پزشک باید بدون تغییر کار کنند (هم API، هم پنل، هم
+ سایت عمومی) — `doctor` تهیپذیر شده، حذف نشده.
+
+## فایلهای مرتبط
+
+| فایل | نقش |
+|------|-----|
+| `src/Appointment/Entity/Appointment.php` | `doctor` تهیپذیر، `activeSlotKey`، `toArray()` |
+| `src/Appointment/Controller/AppointmentController.php` | `book()` و `serviceSlots()` |
+| `src/Appointment/Booking/Entity/OccupancyBucket.php` | تضمین یکتاییِ اشغال منبع (فقط مرجع — تغییر نمیکند) |
+| `src/Resource/Entity/ResourceServiceOffering.php` | سرویسها و مدت مؤثرِ هر منبع |
+| `src/Resource/Entity/ResourceCalendar.php` | شیفت هفتگی منبع (مبنای اسلات) |
+| `migrations/` | migration تهیپذیر کردن `appointments.doctor_id` |
+| `assets/admin/pages/AppointmentsPage.tsx` | تبها، مودال ثبت، حذف بخش منابع |
+| `assets/admin/components/appointments/DoctorTabs.tsx` | تبها (باید عمومی شود) |
+| `assets/admin/components/appointments/ServiceSlotPicker.tsx` | انتخاب سرویس/زمان (باید منبع را هم بپذیرد) |
+| `assets/admin/components/appointments/ResourceTimeline.tsx` + `.test.tsx` | **حذف** |
+| `assets/admin/hooks/useResourceTimeline.ts` | **حذف** اگر مصرفکنندهٔ دیگری ندارد |
+| `docs/api/appointment-booking.md`, `docs/api/appointment.md`, `docs/api/resource.md` | مستندات |
+
+## وضعیت فعلی
+
+`Appointment.php` — پزشک اجباری و کلید یکتا بر پایهٔ پزشک:
+
+```php
+#[ORM\ManyToOne(targetEntity: Doctor::class)]
+#[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
+private Doctor $doctor;
+
+public function __construct(Doctor $doctor, User $user, int $slotStart, int $slotEnd)
+
+private function refreshActiveSlotKey(): void
+{
+ $this->activeSlotKey = !$this->isReserve && in_array($this->status, self::SLOT_OCCUPYING_STATUSES, true)
+ ? sprintf('%d:%d', $this->doctor->getId(), $this->slotStart)
+ : null;
+}
+
+public function getDoctor(): Doctor { return $this->doctor; }
+```
+
+`AppointmentController::book()` — بدون پزشک رد میشود:
+
+```php
+if ($doctorUuid === '' && $resource->subject() instanceof \App\Doctor\Entity\Doctor) {
+ $doctorUuid = $resource->subject()->getUuid();
+}
+…
+if ($doctorUuid === '' || $slotStart <= 0 || (!$hasServices && $slotEnd <= $slotStart)) {
+ return $this->error(ErrorCodes::ERR_VALIDATION_001, 'doctor_uuid یا resource_uuid بههمراه slot_start الزامی است', 422);
+}
+
+$doctor = $this->doctorRepo->findByUuid($doctorUuid);
+if ($doctor === null) {
+ return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
+}
+```
+
+`AppointmentController::serviceSlots()` — پزشکمحور و مقیّد به `booking_mode` پزشک:
+
+```php
+$doctor = $this->doctorRepo->findByUuid($doctorUuid);
+if ($doctor === null) {
+ return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
+}
+…
+$mode = ($schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META)['booking_mode'] ?? WeeklySchedule::MODE_SLOT;
+if ($mode !== WeeklySchedule::MODE_SERVICE) {
+ return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این پزشک در حالت نوبتدهی سرویسی نیست', 422);
+}
+```
+
+`AppointmentsPage.tsx` — تب فقط پزشک، و بخش منابع که باید حذف شود:
+
+```tsx
+{showDoctorTabs && (
+