Files
clinicpro/.claude/prompt/multi-role-dashboard.md
T
hamed e7b90a6399 feat(api): add dashboard endpoints for clinic, doctor, and secretary roles
- Implemented GET /api/v1/dashboard/clinic to return clinic stats and today's schedule for clinic owners.
- Implemented GET /api/v1/dashboard/doctor to return doctor's stats and today's schedule for doctors.
- Implemented GET /api/v1/dashboard/secretary to return stats and conditional appointments for secretaries.

feat(migrations): create user_active_context and mobile_verification_otp tables

- Added migration to create user_active_context table for tracking active user sessions.
- Added migration to create mobile_verification_otp table for handling mobile number verification.

feat(migrations): create site_config table for application settings

- Added migration to create site_config table to store various site configuration settings.

feat(appointments): create MyAppointmentsController for user-specific appointments

- Added MyAppointmentsController to handle fetching user-specific appointments with pagination and filtering.

feat(auth): implement NotificationMobileController for mobile number verification

- Added NotificationMobileController to handle OTP requests and verification for mobile number changes.

feat(auth): create MobileVerificationOtp entity for OTP management

- Created MobileVerificationOtp entity to manage OTP records for mobile verification.

feat(auth): create UserActiveContext entity for user session management

- Created UserActiveContext entity to manage user active sessions.

feat(config): implement SiteConfigController for managing site settings

- Added SiteConfigController to handle fetching and updating site configuration settings.

feat(config): create SiteConfig entity and repository for configuration management

- Created SiteConfig entity and repository to manage site configuration data.
2026-06-11 12:20:12 +03:30

1071 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# پرامپت: داشبورد چند-نقشه (Multi-Role Dashboard)
## هدف کلی
پنل `/admin/` باید علاوه بر ادمین، برای نقش‌های زیر نیز کار کند — هر نقش فقط بخش‌هایی می‌بیند که به آن دسترسی دارد:
| نقش | نام فارسی | ROLE در Symfony |
|-----|-----------|-----------------|
| ادمین سیستم | مدیر کل | `ROLE_ADMIN` |
| صاحب کلینیک | مالک کلینیک | `ROLE_CLINIC` |
| دکتر عضو کلینیک | پزشک | `ROLE_DOCTOR` |
| منشی | منشی | `ROLE_SECRETARY` |
---
## وضعیت فعلی API (مهم — قبل از پیاده‌سازی بخوان)
### API های موجود (استفاده کن، تغییر نده)
| Endpoint | داکیومنت | توضیح |
|----------|----------|-------|
| `GET /oauth/userinfo` | [auth.md](../docs/api/auth.md) | اطلاعات کاربر — باید `primary_role` و `context` به آن اضافه شود |
| `GET /api/v1/admin/dashboard/stats` | [admin.md](../docs/api/admin.md) | KPI های داشبورد ادمین |
| `GET /api/v1/admin/dashboard/charts` | [admin.md](../docs/api/admin.md) | نمودار ۳۰ روزه ادمین |
| `GET /api/v1/admin/dashboard/recent` | [admin.md](../docs/api/admin.md) | آخرین فعالیت‌ها برای ادمین |
| `GET /api/v1/admin/appointments` | [admin.md](../docs/api/admin.md) | لیست همه نوبت‌ها — فقط ROLE_ADMIN |
| `GET /api/v1/appointments/doctor/{doctorUuid}` | [appointment.md](../docs/api/appointment.md) | نوبت‌های یک دکتر خاص |
| `GET /api/v1/appointments/user` | [appointment.md](../docs/api/appointment.md) | نوبت‌های کاربر جاری |
| `GET /api/v1/clinics/{uuid}` | [clinic.md](../docs/api/clinic.md) | جزئیات کلینیک |
| `GET /api/v1/admin/clinics` | [admin.md](../docs/api/admin.md) | لیست کلینیک‌ها — فقط ROLE_ADMIN |
### تغییر روی API موجود
| Endpoint | فایل | توضیح |
|----------|------|-------|
| `GET /oauth/userinfo` | `src/Auth/Controller/AuthController.php` | اضافه کردن `primary_role` و `context` به پاسخ موجود |
### API های جدید که باید ساخته شوند
| Endpoint | فایل کنترلر | توضیح |
|----------|-------------|-------|
| `POST /api/v1/auth/switch-context` | `src/Auth/Controller/AuthController.php` | تغییر context فعال (انتخاب محیط کاری) |
| `GET /api/v1/dashboard/clinic` | `src/Dashboard/Controller/DashboardController.php` | داشبورد صاحب کلینیک |
| `GET /api/v1/dashboard/doctor` | همان | داشبورد دکتر |
| `GET /api/v1/dashboard/secretary` | همان | داشبورد منشی |
| `GET /api/v1/my/appointments` | `src/Appointment/Controller/MyAppointmentsController.php` | نوبت‌های فیلترشده بر اساس نقش |
> بعد از ساخت هر API جدید، فایل مستندات متناظر در `docs/api/` را نیز به‌روز کن.
---
## مفاهیم کلیدی معماری Multi-Context (مهم — قبل از پیاده‌سازی بخوان)
این سیستم از معماری **Multi-Tenant / Multi-Context** استفاده می‌کند. یعنی یک کاربر می‌تواند در چند محیط مختلف فعالیت کند و باید هنگام login محیط کاری خود را انتخاب کند.
### مفهوم `db_uuid`
`db_uuid` برابر UUID **موجودیت فعال فعلی** است — نه UUID کاربر:
| وضعیت | مقدار `db_uuid` |
|--------|-----------------|
| دکتر مستقل (مطب شخصی) | UUID خود دکتر |
| صاحب کلینیک | UUID کلینیک |
| دکتر فعال در یک کلینیک | UUID آن کلینیک |
| منشی یک دکتر | UUID دکتر |
| منشی یک کلینیک | UUID کلینیک |
### مفهوم `db_key`
مقدار `db_key` یک hash امنیتی است:
```
db_key = HMAC-SHA256(db_uuid, APP_SECRET)
```
هر بار که context فعال تغییر کند، باید `db_key` جدید تولید شود.
### مفهوم `available_contexts`
آرایه‌ای از همه محیط‌های کاری که کاربر می‌تواند در آن‌ها فعالیت کند:
```json
"available_contexts": [
{
"type": "doctor",
"db_uuid": "doctor-uuid-...",
"name": "مطب شخصی دکتر احمدی",
"role": "doctor"
},
{
"type": "clinic",
"db_uuid": "clinic-uuid-1",
"name": "کلینیک سلامت",
"role": "doctor"
},
{
"type": "clinic",
"db_uuid": "clinic-uuid-2",
"name": "کلینیک آریا",
"role": "secretary",
"permissions": { ... }
}
]
```
### سناریوهای چند-context
**دکتر چند-کلینیکی (Multi-Clinic Doctor):**
- دکتری که هم مطب شخصی دارد هم در چند کلینیک فعالیت می‌کند
- `available_contexts` شامل: مطب شخصی + هر کلینیکی که عضو است
- هر context مستقل: نوبت‌ها، تنظیمات و برنامه هفتگی جداگانه
**منشی چند-کلینیکی (Multi-Clinic Secretary):**
- منشی که هم برای دکتر A و هم کلینیک B کار می‌کند
- `available_contexts` شامل همه روابط فعال DoctorSecretary آن منشی
### قانون انتخاب context:
- اگر **یک context**: سیستم به‌صورت خودکار همان را فعال می‌کند
- اگر **بیش از یک context**: بعد از login، صفحه انتخاب محیط کاری نمایش داده می‌شود
- پس از انتخاب، `db_uuid` و `db_key` و `context` بر اساس انتخاب کاربر به‌روز می‌شوند
---
## وضعیت فعلی کد (مهم — قبل از تغییر بخوان)
### بک‌اند
- کلاس `AdminApiController` با `#[IsGranted('ROLE_ADMIN')]` روی کل کلاس — تمام endpoint های داشبورد فعلی فقط برای ادمین
- موجودیت‌ها:
- `User`: فیلد `roles: array` — مقادیر ممکن: `ROLE_USER`, `ROLE_ADMIN`, `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY`
- `Clinic`: فیلد `user` (ManyToOne به User) — صاحب کلینیک. رابطه ManyToMany با `Doctor` از طریق جدول `clinic_doctors`
- `Doctor`: فیلد `user` (OneToOne به User). دارای `mobileNumber` و رابطه با `Specialty`. متد `findByUser(User)` در `DoctorRepository` موجود است.
- `DoctorSecretary`: فیلد `doctor` (ManyToOne)، `secretary` (ManyToOne به User)، `permissions` (JSON):
```
{ version:1, resources: {
appointments: { view, create, cancel, update_status },
addresses: { view, create, update, delete },
clinic_info: { view, update },
insurances: { view, create, update, delete }
}}
```
- `ClinicRepository::findByUser(User $user)` موجود است.
- `DoctorRepository::findByUser(User $user)` موجود است.
- JWT: از LexikJWTBundle — payload شامل: `username` (mobile_number)، `roles` (آرایه)، `iat`، `exp`
### فرانت‌اند
- `authStore.ts` (Zustand + persist در `clinicpro-auth`): فقط `token`، `refreshToken`، `isAuthenticated`
- `Sidebar.tsx`: لیست ثابت — بدون هیچ فیلتر نقشی
- `App.tsx`: همه routes با `PrivateRoute` (فقط isAuthenticated بررسی می‌شود)
- `DashboardPage.tsx`: سه query به `/api/v1/admin/dashboard/*` + نمودار Recharts + mini lists
---
## مرحله ۱ — گسترش `/oauth/userinfo` (بک‌اند)
### فایل: `src/Auth/Controller/AuthController.php`
endpoint موجود `/oauth/userinfo` را گسترش بده — همان route، همان permission، فقط دو فیلد جدید به پاسخ اضافه می‌شود.
پاسخ فعلی:
```json
{
"success": true,
"data": {
"id": 4766,
"uuid": "...",
"mobile_number": "09...",
"realName": "دکتر وحید درویشی",
"status": 1,
"roles": ["ROLE_USER", "ROLE_DOCTOR"]
}
}
```
پاسخ بعد از تغییر (فیلدهای جدید اضافه):
```json
{
"success": true,
"data": {
"id": 4766,
"uuid": "...",
"mobile_number": "09...",
"realName": "دکتر وحید درویشی",
"status": 1,
"roles": ["ROLE_USER", "ROLE_DOCTOR"],
"primary_role": "doctor",
"db_uuid": "clinic-uuid-currently-active",
"db_key": "hmac-sha256-hash...",
"context": {
"type": "clinic",
"db_uuid": "clinic-uuid-currently-active",
"name": "کلینیک سلامت",
"role": "doctor"
},
"available_contexts": [
{
"type": "doctor",
"db_uuid": "doctor-uuid-...",
"name": "مطب شخصی دکتر وحید درویشی",
"role": "doctor"
},
{
"type": "clinic",
"db_uuid": "clinic-uuid-currently-active",
"name": "کلینیک سلامت",
"role": "doctor"
}
]
}
}
```
> اگر کاربر تنها یک context دارد، `available_contexts` آرایه‌ای با یک عنصر است و `db_uuid` همان را نشان می‌دهد. اگر **context فعال هنوز انتخاب نشده** (اولین login چند-context)، `db_uuid` و `db_key` باید `null` باشند — frontend صفحه انتخاب محیط کاری را نشان می‌دهد.
**قانون `primary_role`** (اولویت‌بندی):
- `ROLE_ADMIN` → `"admin"`
- `ROLE_CLINIC` → `"clinic"`
- `ROLE_DOCTOR` → `"doctor"`
- `ROLE_SECRETARY` → `"secretary"`
- بقیه → `"user"`
**ساختن `available_contexts` در بک‌اند**:
برای هر کاربر، همه context های ممکن را جمع می‌کنیم:
```php
$contexts = [];
// اگر دکتر است: مطب شخصی خودش
if ($doctor = $this->doctorRepo->findByUser($user)) {
$contexts[] = [
'type' => 'doctor',
'db_uuid' => $doctor->getUuid(),
'name' => 'مطب شخصی ' . $doctor->getName(),
'role' => 'doctor',
];
// کلینیک‌هایی که عضو است
foreach ($doctor->getClinics() as $clinic) {
$contexts[] = [
'type' => 'clinic',
'db_uuid' => $clinic->getUuid(),
'name' => $clinic->getName(),
'role' => 'doctor',
];
}
}
// اگر صاحب کلینیک است
if ($clinic = $this->clinicRepo->findByUser($user)) {
// اگر قبلاً از طریق عضویت دکتری اضافه نشده
$alreadyAdded = array_filter($contexts, fn($c) => $c['db_uuid'] === $clinic->getUuid());
if (empty($alreadyAdded)) {
$contexts[] = [
'type' => 'clinic',
'db_uuid' => $clinic->getUuid(),
'name' => $clinic->getName(),
'role' => 'clinic',
];
}
}
// اگر منشی است: همه روابط فعال DoctorSecretary
if ($user->hasRole('ROLE_SECRETARY')) {
$secretaryRelations = $this->doctorSecretaryRepo->findAllActiveBySecretary($user);
foreach ($secretaryRelations as $rel) {
$contexts[] = [
'type' => 'doctor',
'db_uuid' => $rel->getDoctor()->getUuid(),
'name' => 'مطب ' . $rel->getDoctor()->getName(),
'role' => 'secretary',
'permissions' => $rel->getPermissions(),
];
}
}
```
**تولید `db_uuid` و `db_key` در بک‌اند**:
- `db_uuid`: از session/cookie/JWT claim ذخیره‌شده — مقدار آخرین context انتخاب‌شده توسط کاربر
- اگر context هنوز انتخاب نشده (یا یک context وجود دارد): اولین آیتم `available_contexts` را به‌صورت خودکار فعال کن
- `db_key = hash_hmac('sha256', $dbUuid, $this->getParameter('app.secret'))`
**ذخیره context فعال**:
چون JWT بی‌حالت است، context انتخاب‌شده باید در **user session جدا** یا **درون JWT** ذخیره شود. ساده‌ترین روش: یک جدول `user_active_context` با فیلدهای `user_id`، `db_uuid`، `updated_at`. `/oauth/userinfo` از این جدول می‌خواند؛ `/api/v1/auth/switch-context` آن را به‌روز می‌کند.
**متدهای جدید در Repository ها**:
```php
// DoctorSecretaryRepository
public function findAllActiveBySecretary(User $user): array
{
return $this->findBy(['secretary' => $user, 'active' => true]);
}
// متد قبلی همچنان نگه داشته شود:
public function findActiveBySecretary(User $user): ?DoctorSecretary
{
return $this->findOneBy(['secretary' => $user, 'active' => true]);
}
```
**داکیومنت**: بعد از پیاده‌سازی، پاسخ `/oauth/userinfo` را در `docs/api/auth.md` به‌روز کن.
---
## مرحله ۱.۵ — switch-context API (بک‌اند) — API جدید
### فایل: `src/Auth/Controller/AuthController.php`
#### `POST /api/v1/auth/switch-context` `[IS_AUTHENTICATED_FULLY]`
کاربر یک `db_uuid` از لیست `available_contexts` خود انتخاب می‌کند.
**Request body:**
```json
{ "db_uuid": "clinic-uuid-..." }
```
**پیاده‌سازی**:
1. `available_contexts` کاربر را محاسبه کن (همان منطق مرحله ۱)
2. بررسی کن آیا `db_uuid` ارسال‌شده در لیست `available_contexts` کاربر هست — اگر نه: خطای `403`
3. در جدول `user_active_context` مقدار `db_uuid` را ذخیره/به‌روز کن
4. `db_key` جدید را محاسبه کن: `hash_hmac('sha256', $dbUuid, $appSecret)`
5. `context` فعال را بر اساس `db_uuid` انتخاب‌شده بساز
**Response `200`:**
```json
{
"success": true,
"data": {
"db_uuid": "clinic-uuid-...",
"db_key": "new-hmac-hash...",
"context": {
"type": "clinic",
"db_uuid": "clinic-uuid-...",
"name": "کلینیک سلامت",
"role": "doctor"
}
}
}
```
**Errors:**
| Code | HTTP | توضیح |
|------|------|-------|
| `ERR_AUTH_001` | 401 | توکن وجود ندارد |
| `ERR_AUTH_006` | 403 | `db_uuid` در لیست context های این کاربر نیست |
| `ERR_VALIDATION_001` | 422 | `db_uuid` ارسال نشده |
**موجودیت جدید مورد نیاز** — `src/Auth/Entity/UserActiveContext.php`:
```php
#[ORM\Entity]
#[ORM\Table(name: 'user_active_context')]
class UserActiveContext {
#[ORM\Id]
#[ORM\OneToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'user_id', onDelete: 'CASCADE')]
private User $user;
#[ORM\Column(name: 'db_uuid', type: 'string', length: 36)]
private string $dbUuid;
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
}
```
> **Migration**: بعد از ساخت entity، `doctrine:migrations:diff` و `migrate` اجرا کن.
**داکیومنت**: بعد از پیاده‌سازی، این endpoint را به `docs/api/auth.md` اضافه کن.
---
## مرحله ۲ — به‌روز کردن `authStore.ts` (فرانت‌اند)
```typescript
// assets/admin/stores/authStore.ts
interface ContextItem {
type: 'doctor' | 'clinic';
db_uuid: string;
name: string;
role: 'admin' | 'clinic' | 'doctor' | 'secretary';
permissions?: Record<string, any>;
}
interface AuthState {
token: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
// فیلدهای جدید:
userUuid: string | null;
userName: string | null;
primaryRole: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'user' | null;
dbUuid: string | null; // UUID موجودیت فعال
dbKey: string | null; // hash امنیتی context فعال
context: ContextItem | null; // context فعال انتخاب‌شده
availableContexts: ContextItem[]; // همه context های قابل انتخاب
}
```
- متد `login(token, refreshToken)`: بعد از ذخیره token، یک `GET /oauth/userinfo` بزند و نتیجه را ذخیره کند
- متد `logout()`: همه فیلدها را پاک کند
- متد جدید `fetchMe()`: `GET /oauth/userinfo` و update store — در `App.tsx` هنگام mount فراخوانی شود (اگر token موجود بود اما `primaryRole` خالی بود، تا بعد از reload صفحه role بازیابی شود)
- متد جدید `switchContext(dbUuid: string)`: `POST /api/v1/auth/switch-context` و update `dbUuid`، `dbKey`، `context` در store
---
## مرحله ۲.۵ — صفحه انتخاب محیط کاری (فرانت‌اند) — جدید
### فایل جدید: `assets/admin/pages/SelectContextPage.tsx`
این صفحه **فقط** هنگامی نمایش داده می‌شود که کاربر بیش از یک context دارد.
**شرط نمایش** (در `App.tsx`):
```tsx
// بعد از fetchMe، قبل از route های اصلی:
if (isAuthenticated && availableContexts.length > 1 && !dbUuid) {
return <Navigate to="/admin/select-context" replace />;
}
```
**UI صفحه**:
```tsx
export default function SelectContextPage() {
const { availableContexts, switchContext } = useAuthStore();
const navigate = useNavigate();
const handleSelect = async (dbUuid: string) => {
await switchContext(dbUuid); // POST /api/v1/auth/switch-context
navigate('/admin/dashboard', { replace: true });
};
return (
<div className="select-context-page">
<h2>محیط کاری خود را انتخاب کنید</h2>
<div className="context-list">
{availableContexts.map(ctx => (
<button
key={ctx.db_uuid}
className="context-card"
onClick={() => handleSelect(ctx.db_uuid)}
>
<span className="context-type-badge">{ctx.role}</span>
<span className="context-name">{ctx.name}</span>
</button>
))}
</div>
</div>
);
}
```
- هر کارت: نام محیط کاری + نقش (مطب شخصی / کلینیک / منشی)
- بعد از انتخاب: `switchContext` را صدا می‌زند → store به‌روز می‌شود → redirect به dashboard
- دکمه تغییر محیط کاری در Sidebar هم باید موجود باشد (کلیک → `/admin/select-context`)
---
## مرحله ۳ — محافظت route ها (فرانت‌اند)
### در `App.tsx`، کامپوننت `RoleRoute` اضافه کن:
```tsx
function RoleRoute({ roles, children }: { roles: string[]; children: ReactNode }) {
const primaryRole = useAuthStore(s => s.primaryRole);
if (!primaryRole) return <div style={{padding:40, textAlign:'center'}}>در حال بارگذاری...</div>;
if (!roles.includes(primaryRole)) return <Navigate to="/admin/dashboard" replace />;
return <>{children}</>;
}
```
**Route های فقط ادمین** (با `<RoleRoute roles={['admin']}>` بپوشان):
- `/admin/users`, `/admin/users/:uuid`
- `/admin/payments`
- `/admin/settlements`
- `/admin/representations`, `/admin/representations/:uuid`
- `/admin/comments`
- `/admin/ratings`
- `/admin/sms`
- `/admin/categories`
- `/admin/blogs`, `/admin/blogs/new`, `/admin/blogs/:uuid/edit`
- `/admin/secretaries`
- `/admin/clinics` (لیست کل کلینیک‌ها)
**Route های admin + clinic**:
- `/admin/clinics/:uuid` — ادمین همه را می‌بیند، clinic فقط کلینیک خودش را
- `/admin/doctors` — ادمین همه، clinic فقط پزشکان کلینیکش
**Route های مشترک همه نقش‌ها**:
- `/admin/dashboard`
- `/admin/appointments`, `/admin/appointments/:uuid`
**Route های جدید**:
- `/admin/my-clinic` → `<MyClinicPage />` — فقط `ROLE_CLINIC`
---
## مرحله ۴ — Sidebar پویا (فرانت‌اند)
### فایل `assets/admin/components/layout/Sidebar.tsx`
ساختار `sections` را به یک تابع تبدیل کن که `primaryRole` و `context` می‌گیرد:
```tsx
function buildSections(
primaryRole: string | null,
context: Record<string, any> | null
): Section[]
```
#### ادمین — همه لینک‌های فعلی (بدون تغییر)
#### صاحب کلینیک (`clinic`):
```
عمومی:
• داشبورد /admin/dashboard
• کلینیک من /admin/my-clinic
• پزشکان /admin/doctors
مدیریت:
• نوبت‌ها /admin/appointments
```
#### دکتر (`doctor`):
```
عمومی:
• داشبورد /admin/dashboard
مدیریت:
• نوبت‌های من /admin/appointments
```
#### منشی (`secretary`) — بر اساس `context.permissions.resources`:
```
عمومی:
• داشبورد /admin/dashboard
مدیریت (شرطی):
• نوبت‌ها /admin/appointments ← اگر appointments.view = true
```
در Sidebar، permissions را از `useAuthStore(s => s.context)` بخوان.
---
## مرحله ۵ — endpoint های داشبورد جدید (بک‌اند) — API های جدید
### فایل جدید: `src/Dashboard/Controller/DashboardController.php`
سه endpoint جداگانه — هر سه از `BaseController` extend می‌کنند.
پاسخ با `$this->success($data)` — یعنی فرانت با `data?.data` می‌خواند.
---
### `GET /api/v1/dashboard/clinic` `[ROLE_CLINIC]`
پاسخ:
```json
{
"success": true,
"data": {
"clinic": { "uuid":"...", "name":"...", "is_active": true, "logo":"..." },
"stats": {
"total_doctors": 5,
"today_appointments": 12,
"this_month_appointments": 87,
"pending_invitations": 2
},
"today_appointments": [
{ "uuid":"...", "patient_name":"...", "doctor_name":"...", "slot_start": 1234567890, "status":"reserved" }
],
"doctors": [
{ "uuid":"...", "name":"دکتر ...", "specialty":"...", "today_count": 3 }
]
}
}
```
پیاده‌سازی:
- کلینیک را از `ClinicRepository::findByUser($user)` بگیر
- اگر نبود: `return $this->error('ERR_NOT_FOUND_001', 'کلینیک یافت نشد', 404)`
- `today_appointments` و `this_month_appointments`: از جدول `appointments` با JOIN به `clinic_doctors` فیلتر کن
- `pending_invitations`: از `clinic_doctor_invitations` با `status='pending'` بشمار
- `today_appointments` لیست: ۵ نوبت اخیر امروز این کلینیک (از طریق JOIN `clinic_doctors`)
- `doctors`: لیست پزشکان کلینیک با شمارش نوبت امروز آن‌ها
- از DQL array hydration استفاده کن (`.getArrayResult()`)
---
### `GET /api/v1/dashboard/doctor` `[ROLE_DOCTOR]`
پاسخ:
```json
{
"success": true,
"data": {
"doctor": { "uuid":"...", "name":"...", "degree":"..." },
"stats": {
"today_appointments": 5,
"tomorrow_appointments": 3,
"this_month_appointments": 42,
"avg_rating": 4.7,
"total_ratings": 18
},
"today_appointments": [
{ "uuid":"...", "patient_name":"...", "patient_mobile":"...", "slot_start": 1234567890, "status":"reserved" }
],
"clinics": [
{ "uuid":"...", "name":"کلینیک ...", "logo":"..." }
]
}
}
```
پیاده‌سازی:
- دکتر از `DoctorRepository::findByUser($user)` — اگر نبود `return $this->error(..., 404)`
- `today_appointments`: نوبت‌های این دکتر با `slot_start` در بازه ابتدا تا انتهای امروز
- `tomorrow_appointments`: همان برای فردا
- `avg_rating`: AVG(overall) از جدول `ratings` برای این دکتر — با DQL
- `clinics`: کلینیک‌هایی که این دکتر در `clinic_doctors` آن‌هاست
- از DQL array hydration استفاده کن
---
### `GET /api/v1/dashboard/secretary` `[ROLE_SECRETARY]`
پاسخ:
```json
{
"success": true,
"data": {
"doctor": { "uuid":"...", "name":"...", "degree":"..." },
"permissions": { "version": 1, "resources": { ... } },
"stats": {
"today_appointments": 4,
"tomorrow_appointments": 2
},
"today_appointments": [
{ "uuid":"...", "patient_name":"...", "patient_mobile":"...", "slot_start": 1234567890, "status":"reserved" }
]
}
}
```
پیاده‌سازی:
- از `DoctorSecretaryRepository::findActiveBySecretary($user)` اولین رابطه فعال بگیر
- اگر نبود: `return $this->error('ERR_FORBIDDEN_001', 'دسترسی منشی تنظیم نشده', 403)`
- بررسی `appointments.view = true` در permissions — اگر false بود، `today_appointments` آرایه خالی برگردان
- نوبت‌های دکتر مربوطه را برگردان
**داکیومنت**: بعد از پیاده‌سازی، فایل `docs/api/dashboard.md` جدید بساز.
---
## مرحله ۶ — DashboardPage.tsx چند-نقشه (فرانت‌اند)
فایل `assets/admin/pages/DashboardPage.tsx` را به این شکل بازنویسی کن:
```tsx
export default function DashboardPage() {
const primaryRole = useAuthStore(s => s.primaryRole);
if (!primaryRole) return <LoadingSkeleton />;
if (primaryRole === 'admin') return <AdminDashboard />;
if (primaryRole === 'clinic') return <ClinicDashboard />;
if (primaryRole === 'doctor') return <DoctorDashboard />;
if (primaryRole === 'secretary') return <SecretaryDashboard />;
return <div className="card card-pad"><p className="muted">نقش شما برای داشبورد تعریف نشده</p></div>;
}
```
**AdminDashboard**: کد فعلی DashboardPage عیناً — فقط در یک تابع بپیچ. از endpointهای موجود `/api/v1/admin/dashboard/*` استفاده می‌کند.
**ClinicDashboard**:
- یک query به `GET /api/v1/dashboard/clinic` (جدید)
- استخراج: `data?.data` (چون `$this->success()` یک‌بار nest می‌کند)
- ۴ کارت KPI: تعداد پزشکان / نوبت امروز / نوبت این ماه / دعوتنامه در انتظار
- جدول نوبت‌های امروز (ستون: بیمار، پزشک، زمان، وضعیت)
- لیست پزشکان با تعداد نوبت امروز
- دکمه "مدیریت کلینیک" → navigate به `/admin/my-clinic`
**DoctorDashboard**:
- یک query به `GET /api/v1/dashboard/doctor` (جدید)
- استخراج: `data?.data`
- ۴ کارت KPI: نوبت امروز / فردا / این ماه / میانگین امتیاز (با ستاره)
- جدول نوبت‌های امروز (ستون: بیمار، موبایل `dir="ltr"`, زمان، وضعیت)
- لیست کلینیک‌های عضو به شکل badge
**SecretaryDashboard**:
- یک query به `GET /api/v1/dashboard/secretary` (جدید)
- استخراج: `data?.data`
- نام دکتر مربوطه در header کارت
- ۲ کارت KPI: نوبت امروز / فردا
- جدول نوبت‌های امروز
- لیست مجوزهای فعال با آیکون ✓
---
## مرحله ۷ — صفحه "کلینیک من" (فرانت‌اند)
### فایل جدید: `assets/admin/pages/MyClinicPage.tsx`
```tsx
export default function MyClinicPage() {
const context = useAuthStore(s => s.context);
const clinicUuid = context?.clinic_uuid;
if (!clinicUuid) return (
<div className="card card-pad">
<p>کلینیک شما هنوز ثبت نشده است.</p>
</div>
);
// از endpoint موجود GET /api/v1/clinics/{uuid} استفاده کن
// همان محتوای ClinicDetailPage — اما uuid از context
// دکمه "حذف کلینیک" نشان داده نشود
// بقیه همه فعال: ویرایش، تغییر وضعیت، آپلود لوگو، گالری، دعوت پزشک
}
```
بهترین رویکرد: کد مشترک را از `ClinicDetailPage.tsx` در یک کامپوننت `ClinicDetailView` جدا کن که `uuid` و `showDeleteButton` را به عنوان prop می‌گیرد. هر دو صفحه از آن استفاده کنند.
> `GET /api/v1/clinics/{uuid}` و `PATCH /api/v1/clinic/{uuid}` هر دو موجودند — نیازی به API جدید نیست.
---
## مرحله ۸ — نوبت‌های فیلترشده (بک‌اند + فرانت‌اند) — API جدید
### بک‌اند — endpoint جدید: `GET /api/v1/my/appointments` `[IS_AUTHENTICATED_FULLY]`
فایل جدید: `src/Appointment/Controller/MyAppointmentsController.php`
```
GET /api/v1/my/appointments?page=1&limit=15&status=...&search=...
```
بر اساس نقش فیلتر:
- `ROLE_ADMIN`: forward به همان query موجود در `AdminApiController::appointments()`
- `ROLE_CLINIC`: نوبت‌هایی که doctor آن در `clinic_doctors` این کلینیک است
- `ROLE_DOCTOR`: نوبت‌های این دکتر — از `DoctorRepository::findByUser($user)` uuid بگیر
- `ROLE_SECRETARY`: نوبت‌های دکتری که این منشی به آن وصل است (اگر `appointments.view = true`)
پاسخ: همان فرمت `$this->paginated()` موجود:
```json
{
"success": true,
"data": [ { "uuid":"...", "doctor_name":"...", "patient_name":"...", "slot_start":..., "status":"..." } ],
"meta": { "totalRecords": 100, "totalPages": 7, "currentPage": 1 }
}
```
**داکیومنت**: بعد از پیاده‌سازی، endpoint را به `docs/api/appointment.md` اضافه کن.
### فرانت‌اند — `AppointmentsPage.tsx`
```tsx
const primaryRole = useAuthStore(s => s.primaryRole);
const endpoint = primaryRole === 'admin'
? `/api/v1/admin/appointments`
: `/api/v1/my/appointments`;
```
> برای ادمین از endpoint موجود استفاده می‌شود؛ برای بقیه نقش‌ها از endpoint جدید.
صفحه نوبت‌ها باید **دو نمای قابل‌تعویض** داشته باشد — یک toggle بین «جدولی» و «زمانبندی» (مطابق تصاویر طراحی).
#### هدر صفحه (مشترک هر دو نما):
- ۴ کارت آمار: «کل نوبت‌های امروز» | «نوبت‌های انجام شده» | «مراجعین در انتظار» | «نوبت‌های لغو شده»
- دکمه «+ نوبت جدید»
- دکمه toggle نما (جدولی / زمانبندی)
- انتخابگر پرسنل/دکتر (dropdown)
- انتخابگر تاریخ با Jalali calendar (< روز > + آیکون calendar)
#### نمای جدولی (`TableView`):
- جدول با ستون‌ها: ردیف | شماره تماس | شروع | پایان | سرویس | پرسنل | وضعیت | عملیات
- در ستون «وضعیت»: dropdown تغییر وضعیت با رنگ‌بندی (ثبت شده=آبی، قطعی شده=سبز، در حال پیگیری=نارنجی، سالن=بنفش، ویزیت شده=سبز تیره، لغو شده=قرمز)
- ستون «عملیات»: دکمه `...` با منو
#### نمای زمانبندی (`TimelineView`):
- تب‌های افقی یک دکتر به ازای هر تب (نام دکتر)
- محور زمان عمودی در سمت راست (فارسی: `HH:MM`)
- هر اسلات زمانی یا:
- **پر**: کارت نوبت با رنگ پس‌زمینه بر اساس وضعیت + نام بیمار + شماره تماس + سرویس + دکمه عملیات + dropdown وضعیت
- **خالی**: کارت خالی با دکمه «+ نوبت جدید» (کلیک → مودال ثبت نوبت با slot از پیش پر شده)
- رنگ کارت‌ها: ویزیت شده=سبز روشن | لغو شده=قرمز روشن | در حال پیگیری=نارنجی روشن | سالن=بنفش روشن | ثبت شده / قطعی=آبی روشن | انتظار پرداخت=خاکستری
#### وضعیت‌های نوبت (باید در entity و frontend هر دو باشند):
| مقدار DB | نمایش فارسی | رنگ |
|-----------|-------------|-----|
| `waiting_for_payment` | انتظار پرداخت | خاکستری |
| `pending` | ثبت شده | آبی |
| `following` | در حال پیگیری | نارنجی |
| `in_salon` | سالن | بنفش |
| `visited` | ویزیت شده | سبز |
| `cancelled_by_doctor` | لغو شده (دکتر) | قرمز |
| `cancelled_by_user` | لغو شده (کاربر) | قرمز |
| `expired` | منقضی شده | خاکستری تیره |
| `no_show` | غایب | خاکستری تیره |
**Transition های مجاز (`ALLOWED_TRANSITIONS` در entity)**:
```
waiting_for_payment → [pending, expired, cancelled_by_user]
pending → [following, in_salon, visited, cancelled_by_doctor, cancelled_by_user, expired, no_show]
following → [in_salon, visited, cancelled_by_doctor, cancelled_by_user]
in_salon → [visited, cancelled_by_doctor]
```
> **داکیومنت**: بعد از پیاده‌سازی، وضعیت‌های جدید و فیلدهای جدید را در `docs/api/appointment.md` به‌روز کن.
---
## مرحله ۹ — سیستم کمیسیون نوبت‌دهی (بک‌اند + فرانت‌اند) — جدید
### منطق کسب‌وکار:
- کاربر عادی نوبت می‌گیرد → باید **کمیسیون سایت** (نه قیمت نوبت) پرداخت کند
- منشی / دکتر / کلینیک نوبت می‌دهد → بدون کمیسیون
- مقدار کمیسیون در پنل ادمین توسط مدیر تنظیم می‌شود (مثلاً ۱۰,۰۰۰ تومان به ازای هر نوبت)
### بک‌اند — موجودیت `SiteConfig` (جدید):
فایل جدید: `src/Admin/Entity/SiteConfig.php`
```php
#[ORM\Entity]
#[ORM\Table(name: 'site_config')]
class SiteConfig {
#[ORM\Id]
#[ORM\Column(type: 'string', length: 100)]
private string $configKey;
#[ORM\Column(name: 'config_value', type: 'text', nullable: true)]
private ?string $configValue;
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
}
```
فایل جدید: `src/Admin/Repository/SiteConfigRepository.php`
```php
public function get(string $key, mixed $default = null): mixed
public function set(string $key, mixed $value): void
public function all(): array
```
### بک‌اند — کنترلر تنظیمات ادمین (جدید):
فایل جدید: `src/Admin/Controller/SiteConfigController.php`
```
GET /api/v1/admin/settings [ROLE_ADMIN] → { booking_commission_rials: int }
PATCH /api/v1/admin/settings [ROLE_ADMIN] → body: { booking_commission_rials: int (>=0) }
```
### بک‌اند — تغییرات `Appointment` entity:
فایل: `src/Appointment/Entity/Appointment.php`
فیلدهای جدید:
```php
#[ORM\Column(name: 'booked_by', type: 'string', length: 20)]
private string $bookedBy = self::BOOKED_BY_USER; // 'user' | 'secretary'
#[ORM\Column(name: 'commission_rials', type: 'integer', nullable: true)]
private ?int $commissionRials = null;
```
ثابت‌های جدید:
```php
public const BOOKED_BY_USER = 'user';
public const BOOKED_BY_SECRETARY = 'secretary';
```
> **Migration**: بعد از تغییر entity اجرا کن: `ddev exec php bin/console doctrine:migrations:diff` و سپس `migrate`
### بک‌اند — تغییرات `AppointmentController::book()`:
فایل: `src/Appointment/Controller/AppointmentController.php`
منطق در `POST /api/v1/appointment`:
```
اگر caller دارای ROLE_SECRETARY یا ROLE_DOCTOR یا ROLE_CLINIC بود:
bookedBy = 'secretary'
status = 'pending'
commissionRials = null (بدون کمیسیون)
در غیر اینصورت (ROLE_USER):
bookedBy = 'user'
status = 'waiting_for_payment'
commissionRials = SiteConfigRepository::get('booking_commission_rials', 0)
→ مقدار commission_rials را در پاسخ برگردان تا frontend به درگاه هدایت کند
```
پاسخ برای کاربر عادی (اضافه به پاسخ معمول):
```json
{
"uuid": "...",
"status": "waiting_for_payment",
"booked_by": "user",
"commission_rials": 10000,
"payment_required": true
}
```
### فرانت‌اند — صفحه تنظیمات ادمین (جدید):
فایل جدید: `assets/admin/pages/SettingsPage.tsx`
- Route: `/admin/settings` — فقط `ROLE_ADMIN`
- یک فرم ساده:
- فیلد «کمیسیون نوبت (ریال)»: عدد، validation >= 0
- دکمه «ذخیره»
- `GET /api/v1/admin/settings` برای مقدار اولیه
- `PATCH /api/v1/admin/settings` برای ذخیره
- نمایش مقدار با `formatRial()` از `lib/utils.ts`
### فرانت‌اند — تغییرات `AppointmentsPage.tsx`:
- بعد از ثبت موفق نوبت توسط کاربر عادی (اگر `payment_required: true`)، کاربر را به صفحه درگاه پرداخت هدایت کن
> **داکیومنت**: بعد از پیاده‌سازی به‌روز کن:
> - `docs/api/appointment.md` — اضافه: فیلدهای `booked_by`، `commission_rials`، وضعیت‌های جدید
> - `docs/api/admin.md` — اضافه: `GET/PATCH /api/v1/admin/settings`
---
## مرحله ۱۰ — شماره موبایل اطلاع‌رسانی نوبت (بک‌اند + فرانت‌اند) — جدید
### منطق کسب‌وکار:
- دکتر یا کلینیک می‌تواند یک شماره موبایل برای **دریافت پیامک هنگام ثبت نوبت جدید** تنظیم کند
- این شماره می‌تواند: موبایل خود دکتر، موبایل منشی، یا یک شماره دیگر باشد
- شماره باید با OTP تأیید شود قبل از فعال شدن
- اگر شماره تنظیم نشده باشد، پیامک ارسال نمی‌شود
### بک‌اند — فیلد جدید روی موجودیت‌ها:
روی `Doctor` entity:
```php
#[ORM\Column(name: 'notification_mobile', type: 'string', length: 20, nullable: true)]
private ?string $notificationMobile = null;
#[ORM\Column(name: 'notification_mobile_verified', type: 'boolean')]
private bool $notificationMobileVerified = false;
```
روی `Clinic` entity (اگر کلینیک بخواهد):
```php
#[ORM\Column(name: 'notification_mobile', type: 'string', length: 20, nullable: true)]
private ?string $notificationMobile = null;
#[ORM\Column(name: 'notification_mobile_verified', type: 'boolean')]
private bool $notificationMobileVerified = false;
```
> **Migration**: بعد از تغییر entity اجرا کن.
### بک‌اند — endpoint های جدید:
فایل: `src/Doctor/Controller/DoctorNotificationController.php` (یا در کنترلر موجود دکتر)
```
PATCH /api/v1/doctor/notification-mobile [ROLE_DOCTOR | ROLE_CLINIC | ROLE_ADMIN]
body: { mobile: "09..." }
→ شماره را ذخیره کن (verified=false)، OTP ارسال کن
→ response: { message: "کد تأیید ارسال شد" }
POST /api/v1/doctor/notification-mobile/verify [ROLE_DOCTOR | ROLE_CLINIC | ROLE_ADMIN]
body: { mobile: "09...", code: "12345" }
→ کد OTP را بررسی کن → notification_mobile_verified = true
→ response: { message: "شماره تأیید شد" }
GET /api/v1/doctor/notification-mobile [ROLE_DOCTOR | ROLE_CLINIC | ROLE_ADMIN]
→ response: { mobile: "09...", verified: true }
```
**OTP**: از زیرساخت پیامک موجود (`src/Sms/`) استفاده کن — همان روشی که برای تأیید موبایل کاربر استفاده می‌شود.
**ارسال پیامک هنگام ثبت نوبت**:
در `AppointmentController::book()` بعد از ذخیره نوبت:
- اگر `doctor.notificationMobile` پر بود و `notificationMobileVerified = true`:
- یک پیامک با متن «نوبت جدید — بیمار: {نام} | زمان: {ساعت}» ارسال کن
### فرانت‌اند — تب/بخش تنظیمات اطلاع‌رسانی:
در صفحه اطلاعات دکتر (`DoctorDetailPage` یا پروفایل دکتر) یک بخش جدید اضافه کن:
**«شماره اطلاع‌رسانی نوبت»**:
- نمایش شماره فعلی (اگر موجود) + وضعیت تأیید (تأیید شده / تأیید نشده)
- دکمه «تغییر شماره»: باز می‌کند یک فرم یک‌فیلدی (ورودی موبایل) + دکمه «ارسال کد»
- بعد از ارسال: فیلد کد OTP ظاهر می‌شود + دکمه «تأیید»
- پیشنهاد سریع: «استفاده از موبایل دکتر» (موبایل دکتر را از context پر می‌کند)
> **داکیومنت**: بعد از پیاده‌سازی به‌روز کن:
> - `docs/api/doctor.md` — اضافه: سه endpoint notification-mobile
---
## نکات مهم پیاده‌سازی
### CSS / UI — فقط template CSS
- `.card`, `.card-pad`, `.badge.green/.blue/.amber/.violet/.gray`
- `.btn.primary/.ghost/.soft/.sm`
- `.skeleton` برای loading
- `.empty` برای حالت خالی
- گرادیان آواتار: `HUES_LIST = [256, 205, 162, 295, 272]` با OKLCH:
`background: \`linear-gradient(145deg, oklch(0.62 0.15 ${hue}), oklch(0.48 0.16 ${hue}))\``
- هیچ Tailwind نیست
### پاسخ‌های API
- `$this->success($data)` → `{ success, data: $data }` — فرانت با `data?.data` می‌خواند
- `$this->paginated($items, $total, $page, $limit)` → `{ success, data: $items[], meta: {...} }` — فرانت با `data?.data` و `data?.meta?.totalRecords`
- `$this->error(...)` → `{ success:false, errors:[...] }`
### بک‌اند — قوانین کلی
- همه Admin queries از DQL array hydration استفاده کنند (`.getArrayResult()`)
- Timestamps همه integer Unix هستند
- نوبت‌های «امروز»: بازه `strtotime('today midnight')` تا `strtotime('tomorrow midnight') - 1`
### ترتیب اجرا (پیشنهادی)
1. `UserActiveContext` entity جدید → migration
2. `DoctorSecretaryRepository` — اضافه: `findAllActiveBySecretary()` و `findActiveBySecretary()`
3. `AuthController::userInfo()` — گسترش: `primary_role`، `db_uuid`، `db_key`، `context`، `available_contexts`
4. `AuthController::switchContext()` — endpoint جدید `POST /api/v1/auth/switch-context`
5. `authStore.ts` — اضافه: `dbUuid`، `dbKey`، `availableContexts`، `fetchMe()`، `switchContext()`
6. `App.tsx` — fetchMe در mount + redirect به `/admin/select-context` اگر چند-context
7. `SelectContextPage.tsx` — صفحه انتخاب محیط کاری
8. `DashboardController` — هر سه endpoint
9. `DashboardPage.tsx` — sub-dashboardها
10. `Sidebar.tsx` — پویا + دکمه تغییر محیط کاری
11. `App.tsx` — RoleRoute
12. `MyClinicPage.tsx`
13. `MyAppointmentsController` — بک‌اند
14. `AppointmentsPage.tsx` — دو نما (جدولی/زمانبندی) + endpoint پویا + آمار هدر
15. `Appointment` entity — وضعیت‌های جدید + `booked_by` + `commission_rials` → migration
16. `SiteConfig` entity + repository → migration
17. `SiteConfigController` — `GET/PATCH /api/v1/admin/settings`
18. `SettingsPage.tsx` — پنل ادمین تنظیم کمیسیون
19. `AppointmentController::book()` — منطق کمیسیون + `booked_by`
20. `Doctor`/`Clinic` entity — فیلدهای `notification_mobile` → migration
21. `DoctorNotificationController` — سه endpoint OTP تأیید شماره
22. فرانت‌اند بخش اطلاع‌رسانی در صفحه پروفایل دکتر / کلینیک
23. بعد از هر مرحله بک‌اند: `ddev exec php bin/console cache:clear`
24. بعد از هر مرحله فرانت‌اند: `ddev exec yarn dev`
---
## خلاصه فایل‌های جدید/تغییریافته
### بک‌اند (تغییر)
- `src/Auth/Controller/AuthController.php` — گسترش `userInfo()`: اضافه کردن `primary_role`، `db_uuid`، `db_key`، `context`، `available_contexts` + endpoint جدید `switch-context`
- `src/Secretary/Repository/DoctorSecretaryRepository.php` — اضافه: `findActiveBySecretary()` و `findAllActiveBySecretary()`
- `src/Appointment/Entity/Appointment.php` — وضعیت‌های جدید + فیلدهای `booked_by`، `commission_rials`
- `src/Appointment/Controller/AppointmentController.php` — منطق کمیسیون + ارسال پیامک اطلاع‌رسانی
- `src/Doctor/Entity/Doctor.php` — فیلدهای `notification_mobile`، `notification_mobile_verified`
- `src/Clinic/Entity/Clinic.php` — فیلدهای `notification_mobile`، `notification_mobile_verified`
### بک‌اند (جدید)
- `src/Auth/Entity/UserActiveContext.php` — ذخیره context فعال کاربر
- `src/Dashboard/Controller/DashboardController.php`
- `src/Appointment/Controller/MyAppointmentsController.php`
- `src/Admin/Entity/SiteConfig.php` — موجودیت تنظیمات سایت (key-value)
- `src/Admin/Repository/SiteConfigRepository.php` — متدهای `get()`, `set()`, `all()`
- `src/Admin/Controller/SiteConfigController.php` — `GET/PATCH /api/v1/admin/settings`
- `src/Doctor/Controller/DoctorNotificationController.php` — سه endpoint شماره اطلاع‌رسانی
### فرانت‌اند (تغییر)
- `assets/admin/stores/authStore.ts` — اضافه: primaryRole، dbUuid، dbKey، context، availableContexts، fetchMe()، switchContext()
- `assets/admin/App.tsx` — اضافه: RoleRoute، fetchMe در mount، redirect به select-context اگر چند-context، route های جدید + `/admin/settings`
- `assets/admin/components/layout/Sidebar.tsx` — تبدیل به پویا + دکمه تغییر محیط کاری
- `assets/admin/pages/DashboardPage.tsx` — multi-role
- `assets/admin/pages/AppointmentsPage.tsx` — endpoint پویا + دو نما (جدولی/زمانبندی) + آمار هدر
### فرانت‌اند (جدید)
- `assets/admin/pages/SelectContextPage.tsx` — انتخاب محیط کاری برای کاربران چند-context
- `assets/admin/pages/MyClinicPage.tsx`
- `assets/admin/pages/SettingsPage.tsx` — تنظیم کمیسیون نوبت
### Migration
- `migrations/VersionXXX.php` — اضافه: ستون‌های `booked_by`، `commission_rials` به `appointments`؛ جدول `site_config`؛ ستون‌های `notification_mobile`، `notification_mobile_verified` به `doctors` و `clinics`
### داکیومنت (به‌روزرسانی/جدید)
- `docs/api/auth.md` — به‌روز: پاسخ `/oauth/userinfo` با `primary_role`، `db_uuid`، `db_key`، `context`، `available_contexts` + اضافه: `POST /api/v1/auth/switch-context`
- `docs/api/appointment.md` — اضافه: `GET /api/v1/my/appointments`، وضعیت‌های جدید، `booked_by`، `commission_rials`
- `docs/api/admin.md` — اضافه: `GET/PATCH /api/v1/admin/settings`
- `docs/api/doctor.md` — اضافه: endpoint های `notification-mobile`
- `docs/api/dashboard.md` — **فایل جدید** برای سه endpoint داشبورد