diff --git a/.claude/prompt/appointment-settings-move-schedule-and-cancel-log.md b/.claude/prompt/appointment-settings-move-schedule-and-cancel-log.md
new file mode 100644
index 00000000..cab49e61
--- /dev/null
+++ b/.claude/prompt/appointment-settings-move-schedule-and-cancel-log.md
@@ -0,0 +1,212 @@
+# انتقال برنامه کاری، اصلاح فرم تنظیمات نوبت، فیلدهای عددی لاتین، رفع باگ هزینه ویزیت در مودال، و لاگ/تایملاین لغو نوبت
+
+## پروژه
+
+`clinicpro` (backend Symfony 7.4 + پنل ادمین React 19 داخل Webpack Encore). تک-ریپو، cross-repo نیست.
+
+> تست: پنل ادمین همیشه با `09390039833 / 09390039833`. اجرا داخل ddev (`ddev exec ...`, `https://clinic-pro.ddev.site`).
+
+## زمینه
+
+فیچر «الزامی کردن هزینه ویزیت» قبلاً پیاده شده (پرامپت `require-visit-price-setting.md`): فلگ روی `EntityInsurancePricing.require_visit_price` ذخیره میشود و کنترلر ایجاد نوبت با `VisitPriceRequirementResolver` آن را چک میکند. اما چند مشکل UX/باگ باقی مانده و همچنین دو تغییر ساختاری (انتقال برنامه کاری و لاگ لغو) لازم است. این پرامپت ۵ تسک مستقل ولی همحوزه را پوشش میدهد؛ **هر تسک را جدا پیادهسازی، تست و کامیت کن**.
+
+## فایلهای مرتبط
+
+| فایل | نقش |
+|------|-----|
+| `assets/admin/pages/AppointmentSettingsPage.tsx` | صفحه `/admin/appointment-settings` (۵۰ خط) — مقصد برنامه کاری، محل دکمه ذخیره |
+| `assets/admin/pages/DoctorProfilePage.tsx` | wrapper پروفایل → `` |
+| `assets/admin/pages/DoctorDetailPage.tsx` | تعریف `ScheduleSection` (L2058-2089)، رندر آن L2899، `WeeklyScheduleTab` (export L1252) |
+| `assets/admin/components/FreeVisitPrice.tsx` | کارت قیمت ویزیت + toggle الزامی + دکمه ذخیره (L66-68) |
+| `assets/admin/pages/AppointmentsPage.tsx` | صفحه `/admin/appointments` + `NewAppointmentModal` (L102، رندر L627) — **باگ هزینه ویزیت اینجاست** |
+| `assets/admin/pages/AppointmentCreatePage.tsx` | صفحه کامل ثبت نوبت (`/admin/appointments/new`) — مرجع درست هزینه ویزیت (L109-151, 477-488) |
+| `assets/admin/components/NewAppointmentDrawer.tsx` | drawer «افزودن نوبت» (L126-140) — همان باگ هزینه ویزیت |
+| `assets/admin/components/ui/Input.tsx` | input پایه design-system (`cp-input`) — نقطه تمرکز فیلد عددی سراسری |
+| `assets/admin/components/ui/DigitInput.tsx` / `PriceInput.tsx` / `MobileInput.tsx` | فیلدهای عددی موجود (همه `inputMode="numeric"` + `dir="ltr"`) |
+| `assets/admin/lib/utils.ts` | `toEnglishDigits` (L106-111)، `sanitizeMobileInput` (L114-116)، `tomanToRial`/`rialToToman` (L5-6) |
+| `src/Appointment/Controller/AppointmentController.php` | `updateStatus` (L634-669) و `update` (L677-776) — نقطه لغو نوبت |
+| `src/Appointment/Entity/Appointment.php` | ثابتهای وضعیت (L28-29)، جدول transition (L37-41)، `transitionTo()` (L299-314) |
+| `src/Shared/Logging/DbLogger.php` / `AppLog.php` | زیرساخت لاگ موجود (فقط WARNING به بالا persist) |
+| `docs/api/appointment.md`, `docs/api/insurance.md` | بهروزرسانی مستندات (Standing Rule) |
+
+---
+
+## تسک ۱ — انتقال کامل «برنامه کاری» از پروفایل به تنظیمات نوبتدهی
+
+### وضعیت فعلی
+
+- `AppointmentSettingsPage.tsx:45` همین الان فقط زیرتب `WeeklyScheduleTab` را دارد (بدون تبهای «تاریخهای خاص» و «تعطیلات»):
+```tsx
+
+```
+- `DoctorDetailPage.tsx:2899` کل `ScheduleSection` (سهتب: `weekly` / `overrides` / `holidays` + هدر «برنامه کاری») را رندر میکند و این برای **هم پروفایل و هم نمای ادمینِ جزئیات پزشک** اجرا میشود:
+```tsx
+{uuid && }
+```
+
+### هدف
+
+برنامه کاری فقط از صفحه تنظیمات نوبتدهی مدیریت شود؛ از پروفایل کاملاً حذف شود.
+
+### وظایف
+
+1. در `AppointmentSettingsPage.tsx`، بهجای `WeeklyScheduleTab` تنها، از **`ScheduleSection` کامل** استفاده کن (تا هر سه تب weekly/overrides/holidays در تنظیمات نوبتدهی باشد). `ScheduleSection` را از `DoctorDetailPage` export/import کن (اگر export نیست، `export function ScheduleSection` کن) و با همان props فعلی (`doctorUuid={uuid}`) بده. `addresses` را دیگر لازم نیست جدا بدهی چون `ScheduleSection` خودش `available-locations` را fetch میکند (L2061-2067).
+2. رندر `ScheduleSection` در پروفایل حذف شود. چون خط L2899 هم پروفایل و هم نمای ادمین را سرو میکند، آن را **مشروط** کن که فقط وقتی پروفایلِ خودِ کاربر **نیست** رندر شود — تا نمای ادمینِ جزئیات پزشک دستنخورده بماند:
+```tsx
+{uuid && !isOwnProfile && }
+```
+(نام دقیق prop تشخیص پروفایل را از خود کامپوننت بردار — `isOwnProfile` که `DoctorProfilePage` پاس میدهد.)
+
+### نکات
+
+- بعد از انتقال، مطمئن شو دکمههای ذخیره داخل تبها (`ذخیره برنامه هفتگی` L1608-1612 و مشابه در overrides/holidays) درست کار میکنند — آنها API خودشان را دارند و مستقل از دکمه ذخیره تسک ۲ هستند.
+- نمای ادمینِ «جزئیات پزشک» (وقتی ادمین پزشک دیگری را میبیند) باید همچنان برنامه کاری را نشان دهد؛ فقط پروفایلِ شخصی نباید.
+
+---
+
+## تسک ۲ — اصلاح چیدمان دکمه ذخیره در صفحه تنظیمات نوبتدهی
+
+### وضعیت فعلی
+
+در `AppointmentSettingsPage.tsx` ترتیب فعلی: عنوان → `` (شامل toggle «الزامی کردن هزینه ویزیت» و دکمه ذخیره خودش L66-68) → `ScheduleSection`. دکمه ذخیرهٔ کارت قیمت ویزیت داخل خود کارت است ولی از نظر بصری بعد از toggle در جای مناسبی قرار نمیگیرد.
+
+### هدف (بهترین UX انتخاب و پیادهسازی شود)
+
+راهکار توصیهشده: **دکمه ذخیرهٔ کارت `FreeVisitPrice` بلافاصله زیر فیلد/toggle «الزامی کردن هزینه ویزیت» و در انتهای همان کارت قرار گیرد** (نه شناور بالا). چون منطقاً دکمه ذخیره باید آخرین المان فرمِ آن کارت باشد.
+
+### وظایف
+
+1. در `FreeVisitPrice.tsx` ترتیب داخل کارت را طوری کن که: فیلد قیمت ویزیت آزاد → toggle «الزامی کردن هزینه ویزیت» (L77-93) → **سپس** دکمه ذخیره (L66-68) در انتهای کارت، تراز راست (`marginInlineStart: 'auto'`) با فاصله مناسب از toggle.
+2. اگر دکمه ذخیره فعلاً بالای toggle رندر میشود، آن را به انتهای JSX کارت منتقل کن.
+
+### نکات
+
+- منطق `save`/state دستنخورده بماند؛ فقط ترتیب رندر و استایل جای دکمه.
+- الگوی دکمه: `className="btn primary sm"`.
+
+---
+
+## تسک ۳ — اجبار ورودی لاتین در همه فیلدهای عددی سراسری
+
+### وضعیت فعلی
+
+- ابزار موجود: `toEnglishDigits` در `utils.ts:106-111` (فارسی/عربی → لاتین)، و `DigitInput`/`PriceInput`/`MobileInput` که همگی `inputMode="numeric"` + `dir="ltr"` دارند.
+- **مشکل**: خیلی از inputها المان خام `` هستند و از این کامپوننتها استفاده نمیکنند (مثلاً `NewAppointmentModal` L215-273، `NewAppointmentDrawer` L165-199/303-313). `Input.tsx` پایه design-system است ولی **هیچ** `inputMode`/`lang`/تبدیل رقم ندارد و adoption ناقص است. تبدیل رقم در سه جای تکراری است (`toEnglishDigits`، `PriceInput.toLatinDigits`، regex inline در AppointmentsPage L176-180).
+
+### هدف
+
+هر فیلدی که فقط عدد میگیرد، هنگام تایپ رقم لاتین وارد شود (نه فارسی)، بدون شکستن فیلدهای غیرعددی.
+
+### وظایف
+
+1. **`Input.tsx` را ارتقا بده** تا یک prop اختیاری `numeric?: boolean` بگیرد. وقتی `numeric` است:
+ - `inputMode="numeric"`, `dir="ltr"`, `lang="en"` روی input ست شود.
+ - در `onChange`، مقدار با `toEnglishDigits` نرمال شود قبل از فراخوانی `onChange` والد (رقم فارسی/عربی تایپشده بلافاصله به لاتین تبدیل شود). از همان `toEnglishDigits` مشترک `utils.ts` استفاده کن — تبدیلهای تکراری (`PriceInput.toLatinDigits`، regex inline) را با import از `utils.ts` یکدست کن.
+2. **حذف تکرار**: `PriceInput.tsx` و `onMobileChange` در `AppointmentsPage.tsx` (L176-180) بهجای map/regex محلی از `toEnglishDigits` مشترک استفاده کنند.
+3. **پوشش inputهای خام عددی**: فیلدهای عددیِ خام موجود در مودال/drawer نوبت و سایر فرمها (کدملی، موبایل، مبالغ، تعداد) که از `Input`/`DigitInput`/`MobileInput`/`PriceInput` استفاده نمیکنند را یا به این کامپوننتها مهاجرت بده یا حداقل `inputMode="numeric"` + `dir="ltr"` + نرمالسازی `toEnglishDigits` در onChange اضافه کن. حداقل این نقاط: `NewAppointmentModal` (کدملی/موبایل)، `NewAppointmentDrawer`.
+
+### نکات
+
+- فیلدهای متنی (نام، آدرس، توضیحات) نباید عددی شوند — فقط فیلدهایی که «فقط عدد» میگیرند.
+- `inputMode="numeric"` صفحهکلید موبایل را عددی میکند؛ `dir="ltr"` + نرمالسازی `toEnglishDigits` تضمین میکند رقم فارسی paste/تایپشده هم لاتین ذخیره شود. هر دو لازم است.
+- تبدیل باید در **onChange** انجام شود نه فقط onBlur، تا کاربر بلافاصله رقم لاتین ببیند.
+
+---
+
+## تسک ۴ — رفع باگ: ثبت نوبت هنگام الزامی بودن هزینه ویزیت (۴۲۲)
+
+### وضعیت فعلی
+
+- backend درست است: `MyAppointmentsController::createAppointment` (L132-135) وقتی `isRequiredForDoctor` و `visit_price_rials <= 0` → `422 "هزینه ویزیت الزامی است"`.
+- **باگ در frontend**: `NewAppointmentModal` (`AppointmentsPage.tsx:102`) — payload آن (L143-152) **اصلاً `visit_price_rials` ندارد**، هیچ فیلد قیمت ویزیت رندر نمیکند و تنظیم `insurance-pricing`/`require_visit_price` را نمیخواند:
+```tsx
+mutationFn: () => api.post(createEndpoint, {
+ doctor_uuid: slot.doctor_uuid,
+ slot_start: serviceMode ? pick.slot!.start : slot.start,
+ slot_end: serviceMode ? pick.slot!.end : slot.end,
+ patient_mobile: mobile,
+ patient_name: effectiveName,
+ patient_national_code: effectiveNationalCode,
+ ...(serviceMode ? { service_item_uuids: pick.serviceUuids } : {}),
+}),
+```
+- مرجع درست: `AppointmentCreatePage.tsx` که همین را دارد — خواندن تنظیم (L109-114)، state + prefill از `freeVisit` (L116-120)، گیت اعتبارسنجی (L129)، فیلد ورودی (L477-488)، و ارسال شرطی (L149):
+```tsx
+...(visitPriceToman > 0 ? { visit_price_rials: tomanToRial(visitPriceToman) } : {}),
+```
+- `NewAppointmentDrawer.tsx` (L126-140) هم همین باگ را دارد.
+
+### هدف
+
+مودال (و drawer) ثبت نوبت مثل `AppointmentCreatePage` هزینه ویزیت را بگیرد و ارسال کند تا ۴۲۲ رخ ندهد.
+
+### وظایف
+
+1. در `NewAppointmentModal`:
+ - تنظیم را بخوان: `useQuery(['insurance-pricing'])` → `requireVisit` و `freeVisit` (دقیقاً مثل `AppointmentCreatePage.tsx:109-114`). `doctor_uuid` مودال از `slot.doctor_uuid`.
+ - state `visitPriceToman` با prefill از `freeVisit` (مثل L116-120).
+ - یک فیلد ورودی «هزینه ویزیت (تومان)» با `` اضافه کن؛ اگر `requireVisit` است ستاره `*` روی label و پیام خطای «هزینه ویزیت الزامی است» زیر فیلد وقتی `visitPriceToman <= 0`.
+ - گیت submit: دکمه «ثبت نوبت» (L285) وقتی `requireVisit && visitPriceToman <= 0` غیرفعال شود.
+ - در payload (L143-152) خط شرطی اضافه کن: `...(visitPriceToman > 0 ? { visit_price_rials: tomanToRial(visitPriceToman) } : {})`.
+2. همین اصلاح را در `NewAppointmentDrawer.tsx` (L126-140) اعمال کن.
+
+### نکات
+
+- `visit_price_rials` بر حسب **ریال** ارسال میشود؛ ورودی UI تومان است → `tomanToRial()` از `utils.ts`.
+- وقتی `requireVisit` غیرفعال است رفتار فعلی حفظ شود (فیلد اختیاری، بدون مقدار → فیلد در payload نیاید).
+- فیلد قیمت باید عددی/لاتین باشد (با تسک ۳ سازگار — `PriceInput` این را دارد).
+
+---
+
+## تسک ۵ — ثبت لاگ و رویداد Timeline هنگام لغو نوبت
+
+### وضعیت فعلی (مهم — سیستم Timeline وجود ندارد)
+
+- لغو نوبت از طریق `AppointmentController::updateStatus` (L634-669) با گذار وضعیت به `cancelled_by_doctor` / `cancelled_by_user` انجام میشود (و نیز `update` L677-776, transition L752-764). `transitionTo()` (Entity L299-314) فقط status/updatedAt را ست میکند، **هیچ لاگ یا reason ندارد**.
+- **هیچ فیلد `cancel_reason`** در Entity یا بدنه request وجود ندارد (grep صفر).
+- **هیچ سیستم Timeline/ActivityLog/رویدادِ per-appointment** در backend یا پنل ادمین clinicpro وجود ندارد. `TurnsTimeline.tsx` صرفاً نمای روزانهٔ نوبتهاست، نه تاریخچهٔ رویدادهای یک نوبت. پس این تسک **اولین** سیستم رویداد نوبت را میسازد.
+- زیرساخت لاگ موجود: `DbLogger` → جدول `app_log`، اما فقط سطح WARNING به بالا persist میشود.
+
+### هدف
+
+هر بار یک نوبت لغو میشود: (الف) یک Log ثبت شود، (ب) یک رویداد جدید با عنوان «نوبت لغو شد» شامل زمان لغو، کاربرِ لغوکننده و دلیل لغو (در صورت وجود) در Timeline نوبت نمایش داده شود.
+
+### وظایف
+
+1. **Entity رویداد نوبت (جدید)** — `src/Appointment/Entity/AppointmentEvent.php`:
+ - ستونها: `id`, `uuid`, `appointment_id` (FK/int به نوبت), `type` string (مثل `cancelled`), `title` string («نوبت لغو شد»), `actor_user_id` (nullable int — کاربر لغوکننده), `actor_name` string (nullable — کش نام برای نمایش), `reason` text nullable, `created_at` int (Unix timestamp صحیح — نه DateTime).
+ - migration لازم است: `ddev exec php bin/console make:migration` سپس `ddev exec php bin/console doctrine:migrations:migrate -n`.
+2. **repository جدید** `AppointmentEventRepository` با متد لیستِ رویدادهای یک نوبت بهصورت **DQL array hydration** (`getArrayResult()`)، مرتب بر `created_at`.
+3. **ثبت رویداد در نقطه لغو** — در `AppointmentController::updateStatus` (بعد از `transitionTo`, حدود L656) و نیز مسیر `update` (L760): اگر `$newStatus` یکی از `STATUS_CANCELLED_BY_DOCTOR` / `STATUS_CANCELLED_BY_USER` بود:
+ - `reason` را از بدنه request بخوان: `$data['cancel_reason'] ?? null` (اختیاری).
+ - یک `AppointmentEvent` با `type='cancelled'`, `title='نوبت لغو شد'`, `actor_user_id`/`actor_name` از `$user`, `reason`, `created_at=time()` بساز و persist کن.
+ - همزمان `LoggerInterface` را با فرمت غنی پروژه (الگوی `project-logging`) صدا بزن، سطح `warning` تا در `app_log` هم persist شود:
+ ```php
+ $this->logger->warning(sprintf(
+ 'Appointment cancelled: uuid=%s status=%s by user=%d(%s) reason=%s',
+ $appointment->getUuid(), $newStatus, $user->getId(), $user->getName() ?? '-', $reason ?? '-'
+ ));
+ ```
+ - سرویس لاگ/EntityManager را در constructor کنترلر inject کن (الان هیچکدام inject نشده — L29-39).
+4. **خروجی رویدادها در API**: یک endpoint `GET /api/v1/appointment/{uuid}/events` (یا رویدادها را داخل پاسخ جزئیات نوبت `toArray()` اضافه کن) که آرایه رویدادها را برمیگرداند: `{ type, title, actor_name, reason, created_at }`. envelope با `$this->success()`.
+5. **نمایش Timeline در پنل ادمین**: در نمای جزئیات نوبت (مودال/بخش جزئیات که از `AppointmentsPage`/`TurnsTable` باز میشود) یک بخش «تاریخچه/Timeline» اضافه کن که رویدادها را از endpoint بالا میخواند و هر رویداد را نشان میدهد: عنوان («نوبت لغو شد»)، نامِ لغوکننده، زمان لغو (شمسی با `formatDate`)، و دلیل در صورت وجود. اگر نمای جزئیات نوبت مستقل وجود ندارد، یک بخش timeline ساده در همان مودال/سطر گسترشیافته اضافه کن.
+
+### نکات
+
+- تاریخها Unix timestamp صحیح ذخیره شوند؛ نمایش با `formatDate()` شمسی در فرانت.
+- لیستهای admin طبق قانون پروژه با DQL array hydration.
+- `cancel_reason` فیلد اختیاری است — اگر فرانت دلیل نفرستد، رویداد بدون reason ثبت شود ولی همچنان «نوبت لغو شد» ثبت گردد.
+- (اختیاری، بهبود) در UIِ لغو نوبت یک ورودی «دلیل لغو» اضافه کن تا `cancel_reason` پر شود؛ اگر خارج از scope است، backend همچنان باید null-safe باشد.
+- این ساختار قابلگسترش است: در آینده رویدادهای دیگر (ایجاد/تأیید/تغییر) هم میتوانند از همین `AppointmentEvent` استفاده کنند — ولی در این تسک فقط لغو کافی است.
+
+---
+
+## قوانین عمومی پروژه (برای همه تسکها)
+
+- کنترلرها از `BaseController` ارث میبرند؛ پاسخها با `$this->success()` / `$this->error()` / `$this->paginated()`.
+- تغییر Entity → migration لازم.
+- بعد از تغییر API، فایل مربوط در `docs/api/` همان session بهروز شود (`docs/api/appointment.md`, `docs/api/insurance.md`).
+- Admin frontend: JWT در `localStorage['clinicpro-auth']`؛ paginated → items از `data?.data`, total از `data?.meta?.totalRecords`؛ single → `data?.data`.
+- selectها: همیشه `SearchableSelect`، نه `