feat: add doctor profile page with sidebar integration and routing
This commit is contained in:
@@ -0,0 +1,202 @@
|
|||||||
|
# صفحه پروفایل پزشک (Doctor Profile Page)
|
||||||
|
|
||||||
|
## هدف
|
||||||
|
|
||||||
|
پزشک باید صفحهای داشته باشد که بتواند تمام اطلاعات مربوط به خودش را مشاهده و ویرایش کند — دقیقاً مشابه `/admin/doctors/{uuid}` که ادمین میبیند، اما:
|
||||||
|
- به صورت خودکار UUID پزشک لاگینشده از `authStore.dbUuid` خوانده میشود
|
||||||
|
- آدرس صفحه: `/admin/profile`
|
||||||
|
- در سایدبار دکتر یک آیتم "پروفایل من" اضافه میشود
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## وضعیت فعلی (چه داریم)
|
||||||
|
|
||||||
|
### صفحه موجود
|
||||||
|
- `DoctorDetailPage.tsx` در مسیر `/admin/doctors/:uuid` برای **ادمین + دکتر** قابل دسترسی است
|
||||||
|
- صفحه کامل است و شامل: اطلاعات پروفایل، آدرس مطب، تنظیمات نوبتدهی، تعطیلات، تاریخهای خاص
|
||||||
|
- دکتر با UUID خودش میتواند این صفحه را ببیند اما **لینکی در منوی دکتر وجود ندارد**
|
||||||
|
|
||||||
|
### سایدبار دکتر (فعلی)
|
||||||
|
```
|
||||||
|
عمومی:
|
||||||
|
- داشبورد
|
||||||
|
|
||||||
|
مدیریت:
|
||||||
|
- نوبتهای من
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Store
|
||||||
|
- `authStore.dbUuid` = UUID دکتر لاگینشده
|
||||||
|
- `authStore.primaryRole` = 'doctor'
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## APIهای موجود (بررسیشده)
|
||||||
|
|
||||||
|
| Endpoint | وضعیت | کاربرد |
|
||||||
|
|----------|-------|---------|
|
||||||
|
| `GET /api/v1/doctor/{uuid}` | ✅ PUBLIC | خواندن پروفایل (double-nested: `data?.data?.data`) |
|
||||||
|
| `PATCH /api/v1/doctor/{uuid}` | ✅ AUTH | ویرایش پروفایل |
|
||||||
|
| `POST /api/v1/clinic-pro/doctor-address` | ✅ AUTH | افزودن آدرس مطب |
|
||||||
|
| `PATCH /api/v1/clinic-pro/doctor-address/{id}` | ✅ AUTH | ویرایش آدرس |
|
||||||
|
| `DELETE /api/v1/clinic-pro/doctor-address/{id}` | ✅ AUTH | حذف آدرس |
|
||||||
|
| `GET /api/v1/clinic-pro/doctor-addresses/{doctorId}` | ✅ AUTH | لیست آدرسها |
|
||||||
|
| `GET /api/v1/appointment-settings/weekly-schedule/{doctorUuid}` | ✅ AUTH | برنامه هفتگی |
|
||||||
|
| `POST /api/v1/appointment-settings/weekly-schedule` | ✅ AUTH | ساخت برنامه هفتگی |
|
||||||
|
| `PATCH /api/v1/appointment-settings/weekly-schedule/{uuid}` | ✅ AUTH | ویرایش برنامه هفتگی |
|
||||||
|
| `GET /api/v1/appointment-settings/date-override/list/{doctorUuid}` | ✅ AUTH | تاریخهای خاص |
|
||||||
|
| `GET /api/v1/appointment-settings/holidays/list/{doctorUuid}` | ✅ AUTH | تعطیلات |
|
||||||
|
| `GET /api/v1/my/appointments/today-stats?date=...` | ✅ AUTH | آمار نوبتها |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## قابلیتهایی که باید ساخته شود
|
||||||
|
|
||||||
|
### قابلیت ۱ — Route + Sidebar
|
||||||
|
|
||||||
|
**فایلها:**
|
||||||
|
- `assets/admin/App.tsx` — افزودن route `/admin/profile`
|
||||||
|
- `assets/admin/components/layout/Sidebar.tsx` — افزودن "پروفایل من" به منوی دکتر
|
||||||
|
|
||||||
|
**Route:**
|
||||||
|
```tsx
|
||||||
|
<Route path="profile" element={<DoctorProfilePage />} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sidebar (doctor):**
|
||||||
|
```
|
||||||
|
عمومی:
|
||||||
|
- داشبورد
|
||||||
|
|
||||||
|
پروفایل:
|
||||||
|
- پروفایل من ← /admin/profile
|
||||||
|
|
||||||
|
مدیریت:
|
||||||
|
- نوبتهای من
|
||||||
|
- تنظیمات نوبتدهی ← /admin/appointment-settings (اختیاری)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### قابلیت ۲ — `DoctorProfilePage.tsx`
|
||||||
|
|
||||||
|
**ساختار صفحه** (با tab برای سازماندهی):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ [عکس] دکتر [نام] ویرایش پروفایل │
|
||||||
|
│ [تخصص] [درجه] │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
[ اطلاعات عمومی ] [ آدرس مطب ] [ تنظیمات نوبتدهی ]
|
||||||
|
↑ تبها
|
||||||
|
|
||||||
|
──── محتوای هر تب ────
|
||||||
|
```
|
||||||
|
|
||||||
|
**تب ۱ — اطلاعات عمومی** (ویرایش inline):
|
||||||
|
- نام کامل
|
||||||
|
- جنسیت (مرد/زن)
|
||||||
|
- درجه علمی (عمومی / متخصص / فوقتخصص / فلوشیپ)
|
||||||
|
- کد نظام پزشکی
|
||||||
|
- تخصصها (multi-select از `GET /api/v1/categorys/specially_doctor`)
|
||||||
|
- سرویسها (multi-select از `GET /api/v1/categorys/doctor_services` یا `GET /api/v1/doctor-services`)
|
||||||
|
- بیوگرافی (textarea)
|
||||||
|
- بارگذاری عکس پروفایل
|
||||||
|
|
||||||
|
فرم: React Hook Form + Zod
|
||||||
|
Submit: `PATCH /api/v1/doctor/{uuid}`
|
||||||
|
|
||||||
|
**تب ۲ — آدرس مطب** (CRUD):
|
||||||
|
- لیست آدرسها با نام + آدرس + تلفن
|
||||||
|
- دکمه "افزودن آدرس" → Modal با فرم:
|
||||||
|
- نام مطب
|
||||||
|
- آدرس
|
||||||
|
- تلفن
|
||||||
|
- استان + شهر (از category API)
|
||||||
|
- نقشه (Leaflet — اختیاری اگر پیچیده باشد)
|
||||||
|
- ویرایش + حذف هر آدرس
|
||||||
|
|
||||||
|
**تب ۳ — تنظیمات نوبتدهی** (مشابه آنچه در DoctorDetailPage وجود دارد):
|
||||||
|
- برنامه هفتگی (weekly schedule) — مشاهده + ویرایش
|
||||||
|
- تعطیلات (holidays) — لیست + افزودن + حذف
|
||||||
|
- تاریخهای خاص (date overrides) — لیست + مدیریت
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نکات پیادهسازی
|
||||||
|
|
||||||
|
### گرفتن UUID دکتر
|
||||||
|
```tsx
|
||||||
|
const uuid = useAuthStore(s => s.dbUuid);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Double-nested response برای GET doctor
|
||||||
|
```tsx
|
||||||
|
// پاسخ: { success: true, data: { data: { uuid, name, ... } } }
|
||||||
|
const doctor = data?.data?.data;
|
||||||
|
```
|
||||||
|
|
||||||
|
### استفاده مجدد از DoctorDetailPage
|
||||||
|
اگر `DoctorDetailPage.tsx` قابل reuse است:
|
||||||
|
- میتوان یک wrapper ساده ساخت که فقط `uuid` را از `authStore.dbUuid` میگیرد و پاس میدهد
|
||||||
|
- اما اگر رفتار صفحه برای ادمین و دکتر تفاوت دارد (مثلاً ادمین میتواند دکتر را غیرفعال کند ولی دکتر نمیتواند) باید برخی بخشها را با `isAdmin` شرطی کرد
|
||||||
|
|
||||||
|
### بخشهایی که فقط ادمین میبیند (باید پنهان شوند):
|
||||||
|
- دکمه "غیرفعال کردن حساب دکتر"
|
||||||
|
- دکمه "حذف دکتر"
|
||||||
|
- لیست کلینیکهایی که دکتر عضو است (فقط نمایش — بدون حذف از کلینیک)
|
||||||
|
|
||||||
|
### بخشهایی که دکتر اضافه میبیند (اختیاری):
|
||||||
|
- آمار خلاصه نوبتهای امروز (از `GET /api/v1/my/appointments/today-stats`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## فایلهای تأثیرپذیر
|
||||||
|
|
||||||
|
| فایل | تغییر |
|
||||||
|
|------|-------|
|
||||||
|
| `assets/admin/pages/DoctorProfilePage.tsx` | **جدید** — صفحه پروفایل پزشک |
|
||||||
|
| `assets/admin/App.tsx` | افزودن route `/admin/profile` |
|
||||||
|
| `assets/admin/components/layout/Sidebar.tsx` | افزودن "پروفایل من" به منوی دکتر |
|
||||||
|
| `assets/admin/pages/DoctorDetailPage.tsx` | اختیاری — اگر نیاز به تغییر رفتار ادمین/دکتر |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## رویکرد پیشنهادی
|
||||||
|
|
||||||
|
**سادهترین راه** (بدون duplicate کد):
|
||||||
|
|
||||||
|
1. در `App.tsx` route جدید:
|
||||||
|
```tsx
|
||||||
|
<Route path="profile" element={<Navigate to={`/admin/doctors/${dbUuid}`} replace />} />
|
||||||
|
```
|
||||||
|
|
||||||
|
اما این روش نیاز به دسترسی به `dbUuid` در سطح App دارد.
|
||||||
|
|
||||||
|
**روش بهتر:**
|
||||||
|
```tsx
|
||||||
|
// DoctorProfilePage.tsx — wrapper
|
||||||
|
export default function DoctorProfilePage() {
|
||||||
|
const dbUuid = useAuthStore(s => s.dbUuid);
|
||||||
|
if (!dbUuid) return <div>در حال بارگذاری...</div>;
|
||||||
|
return <DoctorDetailPage overrideUuid={dbUuid} isOwnProfile={true} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
و در `DoctorDetailPage.tsx` یک prop `isOwnProfile` اضافه کن که:
|
||||||
|
- دکمههای ادمین-only را پنهان کند
|
||||||
|
- عنوان صفحه را تغییر دهد ("پروفایل من" به جای "جزئیات پزشک")
|
||||||
|
|
||||||
|
**یا سادهتر:** فقط لینک sidebar را به `/admin/doctors/{dbUuid}` تنظیم کن و DoctorDetailPage را بدون تغییر reuse کن — چون دکتر از طریق `RoleRoute roles={['admin', 'doctor']}` به آن صفحه دسترسی دارد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## الزامات
|
||||||
|
|
||||||
|
1. وقتی دکتر وارد پنل میشود، از سایدبار به "پروفایل من" دسترسی داشته باشد
|
||||||
|
2. پروفایل خودش را ببیند و ویرایش کند (نه پروفایل دکتر دیگری)
|
||||||
|
3. تنظیمات نوبتدهیاش را مدیریت کند
|
||||||
|
4. آدرس مطبهایش را مدیریت کند
|
||||||
|
5. دکمههای ادمین-only (حذف/غیرفعال کردن) نمایش داده نشوند
|
||||||
|
6. همه تاریخها شمسی نمایش داده شوند
|
||||||
@@ -10,13 +10,14 @@ description: اجرای یک فایل پرامپت .md به صورت گامب
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## قبل از شروع — تحلیل پرامپت
|
## قبل از شروع — تحلیل پرامپت و ساخت Todo
|
||||||
|
|
||||||
۱. فایل پرامپت را کامل بخوان
|
۱. فایل پرامپت را کامل بخوان
|
||||||
۲. مستندات موجود را بررسی کن: `docs/api/`
|
۲. مستندات موجود را بررسی کن: `docs/api/`
|
||||||
۳. کد مرتبط در `src/` و `assets/admin/` را مرور کن
|
۳. کد مرتبط در `src/` و `assets/admin/` را مرور کن
|
||||||
۴. لیست قابلیتها را از پرامپت استخراج کن
|
۴. لیست قابلیتها را از پرامپت استخراج کن
|
||||||
۵. به کاربر نمایش بده:
|
۵. **با `TodoWrite` یک todo کامل بساز** — یک آیتم به ازای هر قابلیت
|
||||||
|
۶. به کاربر نمایش بده:
|
||||||
|
|
||||||
```
|
```
|
||||||
📋 قابلیتهای شناساییشده:
|
📋 قابلیتهای شناساییشده:
|
||||||
@@ -31,23 +32,44 @@ description: اجرای یک فایل پرامپت .md به صورت گامب
|
|||||||
|
|
||||||
## قوانین اجرا (اجباری — هیچ استثنایی ندارد)
|
## قوانین اجرا (اجباری — هیچ استثنایی ندارد)
|
||||||
|
|
||||||
### ۱. یک قابلیت در هر مرحله
|
### ۱. Todo — ستون فقرات اجرا
|
||||||
|
|
||||||
|
**در ابتدای هر اجرا** یک todo کامل با `TodoWrite` بساز:
|
||||||
|
- یک آیتم به ازای هر قابلیت با وضعیت `pending`
|
||||||
|
- آیتمهای زیر-مجموعه اضافه کن اگر قابلیت چند بخش مستقل دارد
|
||||||
|
|
||||||
|
**در طول اجرا:**
|
||||||
|
- هر قابلیت را که شروع میکنی → وضعیتش را `in_progress` کن
|
||||||
|
- هر قابلیت را که تست و تأیید شد → فوری `completed` کن
|
||||||
|
- هرگز دو آیتم را همزمان `in_progress` نگذار
|
||||||
|
- اگر خطا داری که قابلیت را block کرده → یک آیتم `in_progress` باقی بماند تا رفع شود
|
||||||
|
|
||||||
|
**نمونه todo اولیه:**
|
||||||
|
```
|
||||||
|
[ ] قابلیت ۱: Stats Bar backend endpoint
|
||||||
|
[ ] قابلیت ۲: Stats Bar frontend component
|
||||||
|
[ ] قابلیت ۳: PersianCalendar popup
|
||||||
|
[ ] قابلیت ۴: AppointmentStatusDropdown
|
||||||
|
[ ] قابلیت ۵: بازطراحی AppointmentsPage
|
||||||
|
```
|
||||||
|
|
||||||
|
### ۲. یک قابلیت در هر مرحله
|
||||||
- هرگز دو قابلیت را همزمان پیادهسازی نکن
|
- هرگز دو قابلیت را همزمان پیادهسازی نکن
|
||||||
- هرگز بدون تأیید موفقیت مرحله قبل به مرحله بعد نرو
|
- هرگز بدون تأیید موفقیت مرحله قبل به مرحله بعد نرو
|
||||||
|
|
||||||
### ۲. ترتیب اجرای هر قابلیت
|
### ۳. ترتیب اجرای هر قابلیت
|
||||||
|
|
||||||
```
|
```
|
||||||
① تحلیل ② طراحی ③ پیادهسازی ④ تست ⑤ رفع خطا ⑥ مستندسازی ⑦ گزارش
|
① تحلیل ② طراحی ③ پیادهسازی ④ تست ⑤ رفع خطا ⑥ مستندسازی ⑦ Todo + گزارش
|
||||||
```
|
```
|
||||||
|
|
||||||
### ۳. سازگاری با پروژه
|
### ۴. سازگاری با پروژه
|
||||||
- همه تغییرات باید با Symfony 7 + React 19 سازگار باشند
|
- همه تغییرات باید با Symfony 7 + React 19 سازگار باشند
|
||||||
- از الگوهای موجود پروژه پیروی کن (BaseController، TanStack Query، Zod، ...)
|
- از الگوهای موجود پروژه پیروی کن (BaseController، TanStack Query، Zod، ...)
|
||||||
- اگر Entity تغییر کرد: `doctrine:migrations:diff` و `doctrine:migrations:migrate` اجرا کن
|
- اگر Entity تغییر کرد: `doctrine:migrations:diff` و `doctrine:migrations:migrate` اجرا کن
|
||||||
- اگر API تغییر کرد: همه بخشهای وابسته (frontend types، api calls، ...) را اصلاح کن
|
- اگر API تغییر کرد: همه بخشهای وابسته (frontend types، api calls، ...) را اصلاح کن
|
||||||
|
|
||||||
### ۴. اصول کدنویسی
|
### ۵. اصول کدنویسی
|
||||||
- SOLID و Clean Code
|
- SOLID و Clean Code
|
||||||
- هیچ کامنت غیرضروری اضافه نکن
|
- هیچ کامنت غیرضروری اضافه نکن
|
||||||
- نامگذاری معنادار
|
- نامگذاری معنادار
|
||||||
@@ -74,6 +96,7 @@ description: اجرای یک فایل پرامپت .md به صورت گامب
|
|||||||
|
|
||||||
### مرحله ③ — پیادهسازی
|
### مرحله ③ — پیادهسازی
|
||||||
|
|
||||||
|
- قابلیت را در todo روی `in_progress` بگذار
|
||||||
- **Backend اول**: Entity → Migration → Repository → Service → Controller
|
- **Backend اول**: Entity → Migration → Repository → Service → Controller
|
||||||
- **Frontend بعد**: Types → API call → Component → Route
|
- **Frontend بعد**: Types → API call → Component → Route
|
||||||
- یک فایل در هر Edit/Write — نه bulk
|
- یک فایل در هر Edit/Write — نه bulk
|
||||||
@@ -138,9 +161,12 @@ ddev exec npx tsc --noEmit --project tsconfig.json 2>&1 | head -30
|
|||||||
4. تمام status codes و error codes
|
4. تمام status codes و error codes
|
||||||
5. اگر پارامتر query داشت → همه را مستند کن
|
5. اگر پارامتر query داشت → همه را مستند کن
|
||||||
|
|
||||||
### مرحله ⑦ — گزارش
|
### مرحله ⑦ — Todo + گزارش
|
||||||
|
|
||||||
بعد از اتمام هر قابلیت، گزارش کوتاه:
|
بعد از موفقیت تست و مستندسازی:
|
||||||
|
|
||||||
|
۱. **آیتم todo را `completed` کن** با `TodoWrite`
|
||||||
|
۲. گزارش کوتاه نمایش بده:
|
||||||
|
|
||||||
```
|
```
|
||||||
✅ قابلیت [شماره]: [نام] — تکمیل شد
|
✅ قابلیت [شماره]: [نام] — تکمیل شد
|
||||||
@@ -150,6 +176,12 @@ ddev exec npx tsc --noEmit --project tsconfig.json 2>&1 | head -30
|
|||||||
• assets/admin/...
|
• assets/admin/...
|
||||||
• docs/api/...
|
• docs/api/...
|
||||||
|
|
||||||
|
📊 پیشرفت کلی:
|
||||||
|
✅ قابلیت ۱: [نام] — تکمیل
|
||||||
|
✅ قابلیت ۲: [نام] — تکمیل
|
||||||
|
⏳ قابلیت ۳: [نام] — در صف
|
||||||
|
...
|
||||||
|
|
||||||
─────────────────────────────────
|
─────────────────────────────────
|
||||||
▶ قابلیت بعدی: [شماره] — [نام]
|
▶ قابلیت بعدی: [شماره] — [نام]
|
||||||
آیا ادامه دهم؟
|
آیا ادامه دهم؟
|
||||||
|
|||||||
+33
-29
@@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
## ادمین
|
## ادمین
|
||||||
|
|
||||||
| فیلد | مقدار |
|
| فیلد | مقدار |
|
||||||
|------|-------|
|
| ------ | ------------- |
|
||||||
| موبایل | `09120671713` |
|
| موبایل | `09120671713` |
|
||||||
| پسورد | `admin123` |
|
| پسورد | `admin123` |
|
||||||
| نقش | `ROLE_ADMIN` |
|
| نقش | `ROLE_ADMIN` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,49 +18,49 @@
|
|||||||
|
|
||||||
### کاربر ۱ — دکتر در چند کلینیک
|
### کاربر ۱ — دکتر در چند کلینیک
|
||||||
|
|
||||||
| فیلد | مقدار |
|
| فیلد | مقدار |
|
||||||
|------|-------|
|
| -------- | -------------------------- |
|
||||||
| موبایل | `09100000011` |
|
| موبایل | `09100000011` |
|
||||||
| پسورد | `Test1234` |
|
| پسورد | `Test1234` |
|
||||||
| نقش | `ROLE_DOCTOR` |
|
| نقش | `ROLE_DOCTOR` |
|
||||||
| Contexts | مطب شخصی دکتر آرمان رضایی |
|
| Contexts | مطب شخصی دکتر آرمان رضایی |
|
||||||
| | کلینیک جامع حضرت علی (عضو) |
|
| | کلینیک جامع حضرت علی (عضو) |
|
||||||
| | بیمارستان ابن سینا (عضو) |
|
| | بیمارستان ابن سینا (عضو) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### کاربر ۲ — صاحب کلینیک
|
### کاربر ۲ — صاحب کلینیک
|
||||||
|
|
||||||
| فیلد | مقدار |
|
| فیلد | مقدار |
|
||||||
|------|-------|
|
| ----------- | -------------------------------------- |
|
||||||
| موبایل | `09100000022` |
|
| موبایل | `09100000022` |
|
||||||
| پسورد | `Test1234` |
|
| پسورد | `Test1234` |
|
||||||
| نقش | `ROLE_CLINIC` |
|
| نقش | `ROLE_CLINIC` |
|
||||||
| Contexts | کلینیک تست پگاه |
|
| Contexts | کلینیک تست پگاه |
|
||||||
| UUID کلینیک | `0730373f-7c0a-4610-8120-d58ecf4a9916` |
|
| UUID کلینیک | `0730373f-7c0a-4610-8120-d58ecf4a9916` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### کاربر ۳ — منشی
|
### کاربر ۳ — منشی
|
||||||
|
|
||||||
| فیلد | مقدار |
|
| فیلد | مقدار |
|
||||||
|------|-------|
|
| -------- | ---------------------- |
|
||||||
| موبایل | `09100000033` |
|
| موبایل | `09100000033` |
|
||||||
| پسورد | `Test1234` |
|
| پسورد | `Test1234` |
|
||||||
| نقش | `ROLE_SECRETARY` |
|
| نقش | `ROLE_SECRETARY` |
|
||||||
| Contexts | مطب دکتر مهناز سلیمانی |
|
| Contexts | مطب دکتر مهناز سلیمانی |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### کاربر ۴ — دکتر + صاحب کلینیک
|
### کاربر ۴ — دکتر + صاحب کلینیک
|
||||||
|
|
||||||
| فیلد | مقدار |
|
| فیلد | مقدار |
|
||||||
|------|-------|
|
| ----------- | -------------------------------------- |
|
||||||
| موبایل | `09100000044` |
|
| موبایل | `09100000044` |
|
||||||
| پسورد | `Test1234` |
|
| پسورد | `Test1234` |
|
||||||
| نقش | `ROLE_DOCTOR` + `ROLE_CLINIC` |
|
| نقش | `ROLE_DOCTOR` + `ROLE_CLINIC` |
|
||||||
| Contexts | مطب شخصی دکتر سامان علوی |
|
| Contexts | مطب شخصی دکتر سامان علوی |
|
||||||
| | کلینیک تخصصی علوی |
|
| | کلینیک تخصصی علوی |
|
||||||
| UUID کلینیک | `26a1ff95-32aa-4a96-b0fd-03e75e810be9` |
|
| UUID کلینیک | `26a1ff95-32aa-4a96-b0fd-03e75e810be9` |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -72,3 +72,7 @@
|
|||||||
```bash
|
```bash
|
||||||
ddev exec php create_test_users.php
|
ddev exec php create_test_users.php
|
||||||
```
|
```
|
||||||
|
|
||||||
|
09212908096
|
||||||
|
|
||||||
|
09212908096
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import BlogFormPage from './pages/BlogFormPage';
|
|||||||
import SecretariesPage from './pages/SecretariesPage';
|
import SecretariesPage from './pages/SecretariesPage';
|
||||||
import MyClinicPage from './pages/MyClinicPage';
|
import MyClinicPage from './pages/MyClinicPage';
|
||||||
import SettingsPage from './pages/SettingsPage';
|
import SettingsPage from './pages/SettingsPage';
|
||||||
|
import DoctorProfilePage from './pages/DoctorProfilePage';
|
||||||
|
|
||||||
// ── Guards ──────────────────────────────────────────────────────────────────
|
// ── Guards ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -132,6 +133,7 @@ export default function App() {
|
|||||||
<Route path="doctors" element={<RoleRoute roles={['admin']}><DoctorsPage /></RoleRoute>} />
|
<Route path="doctors" element={<RoleRoute roles={['admin']}><DoctorsPage /></RoleRoute>} />
|
||||||
<Route path="doctors/new" element={<RoleRoute roles={['admin']}><DoctorFormPage /></RoleRoute>} />
|
<Route path="doctors/new" element={<RoleRoute roles={['admin']}><DoctorFormPage /></RoleRoute>} />
|
||||||
<Route path="doctors/:uuid" element={<RoleRoute roles={['admin', 'doctor']}><DoctorDetailPage /></RoleRoute>} />
|
<Route path="doctors/:uuid" element={<RoleRoute roles={['admin', 'doctor']}><DoctorDetailPage /></RoleRoute>} />
|
||||||
|
<Route path="profile" element={<RoleRoute roles={['doctor']}><DoctorProfilePage /></RoleRoute>} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to="/admin/dashboard" replace />} />
|
<Route path="*" element={<Navigate to="/admin/dashboard" replace />} />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
KeyIcon,
|
KeyIcon,
|
||||||
StarIcon,
|
StarIcon,
|
||||||
TagIcon,
|
TagIcon,
|
||||||
|
UserCircleIcon,
|
||||||
UserGroupIcon,
|
UserGroupIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
@@ -92,6 +93,12 @@ function buildSections(primaryRole: string | null, dbUuid: string | null): Secti
|
|||||||
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
|
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'پروفایل',
|
||||||
|
items: [
|
||||||
|
{ to: '/admin/profile', icon: UserCircleIcon, label: 'پروفایل من' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'مدیریت',
|
label: 'مدیریت',
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -1776,12 +1776,14 @@ type EditForm = z.infer<typeof editSchema>;
|
|||||||
|
|
||||||
// ── Main Page ──────────────────────────────────────────────────────────────
|
// ── Main Page ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function DoctorDetailPage() {
|
export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfile?: boolean }) {
|
||||||
const { uuid } = useParams<{ uuid: string }>();
|
const { uuid: paramUuid } = useParams<{ uuid: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const primaryRole = useAuthStore(s => s.primaryRole);
|
const primaryRole = useAuthStore(s => s.primaryRole);
|
||||||
|
const dbUuid = useAuthStore(s => s.dbUuid);
|
||||||
|
const uuid = isOwnProfile ? (dbUuid ?? undefined) : paramUuid;
|
||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(searchParams.get('edit') === '1');
|
const [editOpen, setEditOpen] = useState(searchParams.get('edit') === '1');
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
@@ -1967,12 +1969,25 @@ export default function DoctorDetailPage() {
|
|||||||
|
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
|
<div className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
|
||||||
<button onClick={() => navigate('/admin/doctors')}
|
{isOwnProfile ? (
|
||||||
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
|
<>
|
||||||
<ArrowRightIcon className="w-4 h-4" />پزشکان
|
<button onClick={() => navigate('/admin/dashboard')}
|
||||||
</button>
|
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
|
||||||
<span>/</span>
|
<ArrowRightIcon className="w-4 h-4" />داشبورد
|
||||||
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل پزشک</span>
|
</button>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل من</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button onClick={() => navigate('/admin/doctors')}
|
||||||
|
className="flex items-center gap-1.5 hover:text-slate-800 dark:hover:text-slate-100 transition-colors">
|
||||||
|
<ArrowRightIcon className="w-4 h-4" />پزشکان
|
||||||
|
</button>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-slate-700 dark:text-slate-300 font-medium">پروفایل پزشک</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
@@ -2026,39 +2041,43 @@ export default function DoctorDetailPage() {
|
|||||||
<PencilIcon className="w-4 h-4" />ویرایش
|
<PencilIcon className="w-4 h-4" />ویرایش
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Toggle active button — styled by current state */}
|
{/* Toggle active / Delete — فقط ادمین */}
|
||||||
<button
|
{!isOwnProfile && (
|
||||||
onClick={() => setToggleConfirm(true)}
|
<>
|
||||||
disabled={toggleMut.isPending}
|
<button
|
||||||
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
|
onClick={() => setToggleConfirm(true)}
|
||||||
doctor.active
|
disabled={toggleMut.isPending}
|
||||||
? 'border-orange-300 dark:border-orange-500/40 text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-500/10 hover:bg-orange-100 dark:hover:bg-orange-500/20'
|
className={`inline-flex items-center gap-1.5 text-sm font-medium px-3 h-10 rounded-xl border transition-colors disabled:opacity-50 ${
|
||||||
: 'border-emerald-300 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
|
doctor.active
|
||||||
}`}
|
? 'border-orange-300 dark:border-orange-500/40 text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-500/10 hover:bg-orange-100 dark:hover:bg-orange-500/20'
|
||||||
>
|
: 'border-emerald-300 dark:border-emerald-500/40 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 hover:bg-emerald-100 dark:hover:bg-emerald-500/20'
|
||||||
{toggleMut.isPending
|
}`}
|
||||||
? <span className="w-3.5 h-3.5 border-2 border-current/30 border-t-current rounded-full animate-spin" />
|
>
|
||||||
: doctor.active
|
{toggleMut.isPending
|
||||||
? <XCircleIcon className="w-4 h-4" />
|
? <span className="w-3.5 h-3.5 border-2 border-current/30 border-t-current rounded-full animate-spin" />
|
||||||
: <CheckCircleIcon className="w-4 h-4" />
|
: doctor.active
|
||||||
}
|
? <XCircleIcon className="w-4 h-4" />
|
||||||
{doctor.active ? 'غیرفعال کردن' : 'فعال کردن'}
|
: <CheckCircleIcon className="w-4 h-4" />
|
||||||
</button>
|
}
|
||||||
|
{doctor.active ? 'غیرفعال کردن' : 'فعال کردن'}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="relative" data-dmenu>
|
<div className="relative" data-dmenu>
|
||||||
<button data-dmenu onClick={() => setMenuOpen(!menuOpen)}
|
<button data-dmenu onClick={() => setMenuOpen(!menuOpen)}
|
||||||
className="cp-btn-secondary h-10 w-10 flex items-center justify-center px-0">
|
className="cp-btn-secondary h-10 w-10 flex items-center justify-center px-0">
|
||||||
<EllipsisVerticalIcon className="w-4 h-4" />
|
<EllipsisVerticalIcon className="w-4 h-4" />
|
||||||
</button>
|
|
||||||
{menuOpen && (
|
|
||||||
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-dmenu>
|
|
||||||
<button onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
|
||||||
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10">
|
|
||||||
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
|
|
||||||
</button>
|
</button>
|
||||||
|
{menuOpen && (
|
||||||
|
<div className="absolute left-0 top-12 z-30 w-52 cp-card shadow-2xl border border-slate-200 dark:border-gray-700 py-1.5 animate-scale-in" data-dmenu>
|
||||||
|
<button onClick={() => { setDeleteOpen(true); setMenuOpen(false); }}
|
||||||
|
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10">
|
||||||
|
<TrashIcon className="w-4 h-4 shrink-0" />حذف این پزشک
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import DoctorDetailPage from './DoctorDetailPage';
|
||||||
|
|
||||||
|
export default function DoctorProfilePage() {
|
||||||
|
return <DoctorDetailPage isOwnProfile />;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user