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.
This commit is contained in:
hamed
2026-06-11 12:20:12 +03:30
parent 54c491c734
commit e7b90a6399
32 changed files with 3780 additions and 354 deletions
+564 -32
View File
@@ -39,6 +39,7 @@
| 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` | همان | داشبورد منشی |
@@ -48,6 +49,76 @@
---
## مفاهیم کلیدی معماری 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` بر اساس انتخاب کاربر به‌روز می‌شوند
---
## وضعیت فعلی کد (مهم — قبل از تغییر بخوان)
### بک‌اند
@@ -98,7 +169,7 @@ endpoint موجود `/oauth/userinfo` را گسترش بده — همان route
}
```
پاسخ بعد از تغییر (دو فیلد جدید اضافه):
پاسخ بعد از تغییر (فیلدهای جدید اضافه):
```json
{
"success": true,
@@ -110,14 +181,34 @@ endpoint موجود `/oauth/userinfo` را گسترش بده — همان route
"status": 1,
"roles": ["ROLE_USER", "ROLE_DOCTOR"],
"primary_role": "doctor",
"db_uuid": "clinic-uuid-currently-active",
"db_key": "hmac-sha256-hash...",
"context": {
"doctor_uuid": "...",
"doctor_name": "دکتر وحید درویشی"
}
"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"`
@@ -125,14 +216,78 @@ endpoint موجود `/oauth/userinfo` را گسترش بده — همان route
- `ROLE_SECRETARY` → `"secretary"`
- بقیه → `"user"`
**پر کردن `context`**:
- `ROLE_DOCTOR`: از `DoctorRepository::findByUser($user)` → `{doctor_uuid, doctor_name}`
- `ROLE_CLINIC`: از `ClinicRepository::findByUser($user)` → `{clinic_uuid, clinic_name, clinic_logo}`
- `ROLE_SECRETARY`: از `DoctorSecretaryRepository::findActiveBySecretary($user)` (متد جدید) → `{secretary_uuid, doctor_uuid, doctor_name, permissions}`
- اگر موجودیت پیدا نشد: `context: null`
**ساختن `available_contexts` در بک‌اند**:
برای هر کاربر، همه context های ممکن را جمع می‌کنیم:
**متد جدید در `DoctorSecretaryRepository`**:
```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]);
@@ -143,11 +298,87 @@ public function findActiveBySecretary(User $user): ?DoctorSecretary
---
## مرحله ۱.۵ — 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;
@@ -156,13 +387,68 @@ interface AuthState {
userUuid: string | null;
userName: string | null;
primaryRole: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'user' | null;
context: Record<string, any> | 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`)
---
@@ -472,6 +758,225 @@ const endpoint = primaryRole === 'admin'
> برای ادمین از 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
---
## نکات مهم پیاده‌سازی
@@ -496,43 +1001,70 @@ const endpoint = primaryRole === 'admin'
- نوبت‌های «امروز»: بازه `strtotime('today midnight')` تا `strtotime('tomorrow midnight') - 1`
### ترتیب اجرا (پیشنهادی)
1. `DoctorSecretaryRepository::findActiveBySecretary()` — متد جدید
2. `AuthController::userInfo()` — گسترش برای `primary_role` + `context`، تست با curl
3. `authStore.ts` — اضافه کردن fetchMe + فیلدهای جدید
4. `App.tsx` — fetchMe در mount
5. `DashboardController` — هر سه endpoint
6. `DashboardPage.tsx` — sub-dashboardها
7. `Sidebar.tsx` — پویا
8. `App.tsx` — RoleRoute
9. `MyClinicPage.tsx`
10. `MyAppointmentsController` — بک‌اند
11. `AppointmentsPage.tsx` — فیلتر endpoint
12. بعد از هر مرحله بک‌اند: `ddev exec php bin/console cache:clear`
13. بعد از هر مرحله فرانت‌اند: `ddev exec yarn dev`
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` و `context`
- `src/Secretary/Repository/DoctorSecretaryRepository.php` — اضافه: `findActiveBySecretary()`
- `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، context، fetchMe()
- `assets/admin/App.tsx` — اضافه: RoleRoute، fetchMe در mount، route های جدید
- `assets/admin/components/layout/Sidebar.tsx` — تبدیل به پویا
- `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/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` — اضافه: `GET /api/v1/me`
- `docs/api/appointment.md` — اضافه: `GET /api/v1/my/appointments`
- `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 داشبورد