# رفع دو باگ Modal و تقویم شمسی در پنل ادمین

## پروژه

`clinicpro` (Admin SPA — React 19 داخل Symfony/Encore).

## زمینه

در پنل ادمین دو باگ UI مستقل وجود دارد که هر دو در `/admin/profile` و سایر صفحات دیده می‌شوند:

1. **مودال وسط صفحه باز نمی‌شود و به پایین صفحه می‌چسبد** — در همهٔ صفحاتی که از کامپوننت مشترک `Modal` استفاده می‌کنند.
2. **تقویم شمسی هنگام کار با دکمه‌های ناوبری بسته می‌شود** — مثلاً در فیلد «تاریخ شروع فعالیت» وقتی وارد نمای انتخاب سال می‌شوی و روی دکمه‌های `<` / `>` (تغییر بازهٔ سال) کلیک می‌کنی، به‌جای جابه‌جایی بازه، کل فرم submit و مودال بسته می‌شود.

## مشکل / هدف

### باگ ۱ — علت
کامپوننت `Modal` محتوای خود را **inline در همان‌جای درخت DOM** رندر می‌کند (بدون Portal). کلاس `.overlay` از `position: fixed; inset: 0; display: grid; place-items: center` استفاده می‌کند که باید نسبت به viewport وسط‌چین کند؛ اما وقتی یکی از عناصر والد یک containing-block برای `position: fixed` بسازد (هر عنصری با `transform` / `filter` / `perspective` / `contain: paint` / `will-change`)، مبنای `fixed` از viewport به آن والد تغییر می‌کند و overlay داخل جعبهٔ بلندِ آن والد کشیده می‌شود؛ در نتیجه `place-items: center` مودال را در وسط آن جعبهٔ بلند (که پایین‌تر از دید کاربر است) قرار می‌دهد، نه وسط صفحه. راه‌حل قطعی و مستقل از اینکه کدام والد مقصر است: رندر Modal با **React Portal روی `document.body`**.

### باگ ۲ — علت
در `PersianCalendar.tsx` هیچ‌کدام از `<button>`ها `type` ندارند. طبق HTML، `<button>` بدون `type` داخل یک `<form>` مقدار پیش‌فرض **`type="submit"`** می‌گیرد. این تقویم داخل فرم ویرایش پروفایل/پزشک رندر می‌شود (`<form id="edit-doctor-form" onSubmit={handleSubmit(...)}>` در `DoctorDetailPage.tsx`)، پس هر کلیک روی دکمه‌های ناوبری (`<` / `>`) یا سلول‌های روز/ماه/سال، فرم را submit می‌کند → mutation ذخیره اجرا می‌شود و در `onSuccess` فرم/مودال بسته می‌شود. راه‌حل: افزودن `type="button"` به **همهٔ** `<button>`های داخل `PersianCalendar`.

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

| فایل | نقش |
|------|-----|
| `clinicpro/assets/admin/components/ui/Modal.tsx` | کامپوننت مشترک مودال — نیاز به Portal |
| `clinicpro/assets/admin/components/ui/PersianCalendar.tsx` | تقویم شمسی popup — buttonها بدون `type` |
| `clinicpro/assets/admin/pages/DoctorDetailPage.tsx` | مصرف‌کننده؛ تقویم داخل `<form id="edit-doctor-form">` (فقط برای درک زمینه — تغییر لازم ندارد) |
| `clinicpro/assets/admin/styles.css` | کلاس‌های `.overlay` / `.modal` (خط ۶۱۴ به بعد — تغییر لازم ندارد) |

## وضعیت فعلی

### `Modal.tsx` (بدون Portal)

```tsx
import React, { useEffect } from 'react';
import { XMarkIcon } from '@heroicons/react/24/outline';
// ...
export default function Modal({ open, title, size = 'md', onClose, children, footer }: Props) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open, onClose]);

  if (!open) return null;

  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal" style={{ maxWidth: sizeMap[size] }} onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h2>{title}</h2>
          <button className="mini-btn" onClick={onClose}>
            <XMarkIcon style={{ width: 18, height: 18 }} />
          </button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-foot">{footer}</div>}
      </div>
    </div>
  );
}
```

### `PersianCalendar.tsx` (buttonها بدون `type`) — نمونه‌ها

```tsx
// دکمه‌های ناوبری هدر
<button onClick={mode === 'year' ? () => setYearRangeStart(s => s - 12) : nextMonth} style={{ ...navBtnStyle, ... }}>
  <ChevronRightIcon style={{ width: 16, height: 16 }} />
</button>
// ...
<button onClick={mode === 'year' ? () => setYearRangeStart(s => s + 12) : prevMonth} style={{ ...navBtnStyle, ... }}>
  <ChevronLeftIcon style={{ width: 16, height: 16 }} />
</button>

// سلول روز
<button key={i} onClick={() => selectDay(day)} style={{ ... }}> {day.toLocaleString('fa-IR')} </button>

// سلول ماه
<button key={m} onClick={() => { setViewMonth(m); setMode('day'); }} style={{ ... }}> {mName} </button>

// سلول سال
<button key={y} onClick={() => { setViewYear(y); setMode('month'); }} style={{ ... }}> {faYear(y)} </button>
```

`mini-btn` بستن در `Modal` هم بدون `type` است و باید اصلاح شود (اگر مودالی داخل فرم قرار گیرد).

## وظایف

### ۱. رندر `Modal` با Portal روی `document.body`

`createPortal` را از `react-dom` وارد کن و کل markup مودال را داخل آن بپیچ:

```tsx
import React, { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { XMarkIcon } from '@heroicons/react/24/outline';

// ... داخل کامپوننت، بعد از `if (!open) return null;`
  return createPortal(
    <div className="overlay" onClick={onClose}>
      <div className="modal" style={{ maxWidth: sizeMap[size] }} onClick={(e) => e.stopPropagation()}>
        {/* ... بدون تغییر ... */}
      </div>
    </div>,
    document.body
  );
```

نکته‌ها:
- Portal تضمین می‌کند overlay فرزندِ مستقیم `body` باشد، پس `position: fixed` همیشه نسبت به viewport محاسبه می‌شود و باگ چسبیدن به پایین در همهٔ صفحات رفع می‌شود.
- منطق Escape و `onClick` overlay و `stopPropagation` مودال بدون تغییر بماند.
- دکمهٔ بستن `mini-btn` را `type="button"` کن تا اگر مودالی داخل یک `<form>` قرار گرفت، submit ناخواسته رخ ندهد.

### ۲. افزودن `type="button"` به همهٔ `<button>`های `PersianCalendar.tsx`

به هر پنج نوع دکمه `type="button"` اضافه کن:
- دکمهٔ ناوبری راست (`ChevronRightIcon`)
- دکمهٔ ناوبری چپ (`ChevronLeftIcon`)
- سلول‌های روز (day view)
- سلول‌های ماه (month view)
- سلول‌های سال (year view)

نمونه:

```tsx
<button
  type="button"
  onClick={mode === 'year' ? () => setYearRangeStart(s => s - 12) : nextMonth}
  style={{ ...navBtnStyle, visibility: mode === 'month' ? 'hidden' : 'visible' }}
>
  <ChevronRightIcon style={{ width: 16, height: 16 }} />
</button>
```

این کار از submit ناخواستهٔ فرمِ دربرگیرنده جلوگیری می‌کند و تقویم هنگام کار با `<` / `>` و انتخاب سال/ماه باز می‌ماند؛ فقط انتخاب «روز» (که `onChange` + `onClose` را صدا می‌زند) آن را می‌بندد.

### ۳. (اختیاری، اگر جای دیگری هم مشکل مشابه بود) بررسی سریر سایر تقویم‌ها

`PersianDatePicker.tsx` (والدِ `PersianCalendar`) دکمهٔ باز/بستن‌اش `<div>` است نه `<button>`، پس مشکل submit ندارد؛ نیازی به تغییر نیست. فقط مطمئن شو `PersianCalendar` (که popup مشترک است) اصلاح شده.

## نکات مهم

- **علت دقیق باگ ۲ = نبود `type="button"` در فرم.** فقط با پوشش «همهٔ» دکمه‌های تقویم رفع می‌شود؛ اگر حتی یکی جا بماند، همان دکمه فرم را submit می‌کند.
- **Portal رفع ریشه‌ای باگ ۱ است** و مستقل از اینکه کدام والد containing-block می‌سازد کار می‌کند؛ نیازی به تغییر `styles.css` نیست.
- بعد از Portal، اطمینان حاصل کن `z-index: 1000` روی `.overlay` هنوز بالاتر از سایر عناصر است (چون حالا فرزند body است معمولاً بالاتر هم می‌آید — مشکلی نیست).
- هیچ کتابخانهٔ جدیدی اضافه نکن؛ `react-dom` از قبل موجود است.
- بعد از تغییر: `ddev exec npx tsc --noEmit --project tsconfig.json` و `ddev exec yarn dev` برای build. سپس دستی در `/admin/profile` تست کن: (الف) باز شدن مودال ویرایش در وسط صفحه؛ (ب) باز ماندن تقویم «تاریخ شروع فعالیت» هنگام کلیک روی `<` / `>` و انتخاب سال، و بسته شدن فقط با انتخاب روز.
- این تغییر فقط UI/رفتار کلاینت است؛ API و مستندات `docs/api/` تغییری لازم ندارند.
