# Dashboard API Role-specific dashboard endpoints. Each endpoint requires the corresponding role JWT. --- ## GET /api/v1/dashboard/clinic Returns stats and today's schedule for the authenticated clinic owner. **Auth:** `ROLE_CLINIC` required ### Query params | Param | Type | Default | Description | |-------|------|---------|-------------| | `from` | int (unix) | start of current month | Period start for patient/revenue stats | | `to` | int (unix) | now | Period end for patient/revenue stats | | `patients_year` | int (Jalali) | current Jalali year | Year of the month drawn by `charts.appointments_by_day` | | `patients_month` | int `1..12` | current Jalali month | Jalali month drawn by `charts.appointments_by_day` | | `revenue_year` | int (Jalali) | current Jalali year | Jalali year drawn by `charts.revenue_by_month` | `patients_year` / `revenue_year` are clamped to `1300..1500` and `patients_month` to `1..12`. ### Response `200` ```json { "success": true, "data": { "clinic": { "uuid": "string", "name": "string", "is_active": true, "logo": "string | null" }, "stats": { "total_doctors": 5, "today_appointments": 12, "this_month_appointments": 87, "pending_invitations": 2, "sms_wallet_balance": 50000, "unique_patients_count": 34, "total_patients": 210, "revenue_period_rials": 12500000, "today_payments_rials": 5225000, "week_payments_rials": 560000200 }, "charts": { "revenue_by_day": [ { "label": "۷ خرداد", "amount_rials": 3200000 } ], "revenue_by_month": [ { "label": "فروردین", "amount_rials": 3200000 } ], "appointments_by_day": [ { "label": "۱", "count": 9 } ] }, "charts_period": { "patients_year": 1405, "patients_month": 4, "revenue_year": 1405 }, "period": { "from": 1717200000, "to": 1719792000 }, "today_appointments": [ { "uuid": "string", "patient_name": "string | null", "patient_mobile": "string | null", "doctor_name": "string", "service_name": "string | null", "slot_start": 1700000000, "slot_end": 1700001800, "status": "reserved" } ], "doctors": [ { "uuid": "string", "name": "string", "today_count": 3 } ] } } ``` **Field notes:** - `sms_wallet_balance` — current SMS wallet balance in Rials (0 if wallet not yet created) - `unique_patients_count` — distinct patients with at least one session in the `from`–`to` period - `total_patients` — distinct patients ever (no period filter) - `revenue_period_rials` — sum of `final_price_rials` from all patient sessions in the period - `today_payments_rials` / `week_payments_rials` — revenue for today / the last 7 days - `charts.appointments_by_day` — «نمودار تعداد بیماران»: one entry per day of the requested Jalali month (28–31 entries), label = Jalali day number in Persian digits, empty days are `count: 0` - `charts.revenue_by_month` — «میزان درآمد»: exactly 12 entries, one per Jalali month of `revenue_year`, label = Persian month name - `charts.revenue_by_day` — last 7 days; kept only as the source of `today_payments_rials` / `week_payments_rials`, not drawn by the dashboard charts - `charts_period` — the effective (post-clamp) chart period, so the UI can reflect what was actually rendered - `today_appointments` — up to 5 records, ordered by `slot_start ASC`; each row carries `patient_mobile`, `doctor_name` (personnel), `service_name` (nullable — booked service item), and `slot_end` for the «لیست نوبت‌های جدید» dashboard table (that table renders read-only — `status` is still returned but no longer shown) - `doctors` — all doctors belonging to this clinic; each includes their appointment count for today ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_NOT_FOUND_001` | 404 | Clinic not found for this user | --- ## GET /api/v1/dashboard/doctor Returns stats and today's schedule for the authenticated doctor. **Auth:** `ROLE_DOCTOR` required ### Query params | Param | Type | Default | Description | |-------|------|---------|-------------| | `from` | int (unix) | start of current month | Period start for patient/revenue stats | | `to` | int (unix) | now | Period end for patient/revenue stats | | `patients_year` | int (Jalali) | current Jalali year | Year of the month drawn by `charts.appointments_by_day` | | `patients_month` | int `1..12` | current Jalali month | Jalali month drawn by `charts.appointments_by_day` | | `revenue_year` | int (Jalali) | current Jalali year | Jalali year drawn by `charts.revenue_by_month` | ### Response `200` ```json { "success": true, "data": { "doctor": { "uuid": "string", "name": "string", "degree": "string | null" }, "stats": { "today_appointments": 8, "tomorrow_appointments": 5, "this_month_appointments": 62, "avg_rating": 4.6, "total_ratings": 34, "sms_wallet_balance": 25000, "unique_patients_count": 18, "total_patients": 140, "revenue_period_rials": 6800000, "today_payments_rials": 5225000, "week_payments_rials": 42000000 }, "charts": { "revenue_by_day": [ { "label": "۷ خرداد", "amount_rials": 3200000 } ], "revenue_by_month": [ { "label": "فروردین", "amount_rials": 3200000 } ], "appointments_by_day": [ { "label": "۱", "count": 4 } ] }, "charts_period": { "patients_year": 1405, "patients_month": 4, "revenue_year": 1405 }, "period": { "from": 1717200000, "to": 1719792000 }, "today_appointments": [ { "uuid": "string", "patient_name": "string | null", "patient_mobile": "string", "doctor_name": "string", "service_name": "string | null", "slot_start": 1700000000, "slot_end": 1700001800, "status": "reserved" } ], "clinics": [ { "uuid": "string", "name": "string", "logo": "string | null" } ] } } ``` **Field notes:** - `today_appointments` — up to 10 records, ordered by `slot_start ASC`; each row carries `patient_mobile`, `doctor_name`, `service_name` (nullable), and `slot_end` for the dashboard appointments table - `avg_rating` — rounded to 1 decimal; `null` if no ratings yet - `clinics` — all clinics the doctor belongs to - `sms_wallet_balance`, `unique_patients_count`, `revenue_period_rials` — same semantics as clinic dashboard - `charts.*`, `charts_period` — same Jalali-period semantics as the clinic dashboard; `revenue_by_day` / `revenue_by_month` are omitted entirely when the caller may not see financials (see the note at the end of this file) ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_NOT_FOUND_001` | 404 | Doctor profile not found for this user | --- ## GET /api/v1/dashboard/secretary Returns stats for the authenticated secretary and (conditionally) today's appointments. **Auth:** `ROLE_SECRETARY` required ### Response `200` پاسخ بر اساس `scope` محیطِ فعالِ منشی دو شکل دارد. **کلید `scope` تمایزدهنده است**: در `scope=doctor` فیلد `doctor` هست (نه `clinic`) و در `scope=clinic` فیلد `clinic` (نه `doctor`). کلاینت باید هر دو را مدیریت کند و به‌صورت مستقیم به `data.doctor.name` دسترسی نگیرد. **منشیِ مطبِ شخصی (`scope=doctor`):** ```json { "success": true, "data": { "scope": "doctor", "doctor": { "uuid": "string", "name": "string", "degree": "string | null" }, "permissions": { "resources": { "appointments": { "view": true, "update_status": false } } }, "stats": { "today_appointments": 8, "tomorrow_appointments": 5 }, "today_appointments": [ { "uuid": "string", "patient_name": "string | null", "patient_mobile": "string", "slot_start": 1700000000, "status": "reserved" } ] } } ``` **منشیِ کلینیک (`scope=clinic`) — بدون کلید `doctor`:** ```json { "success": true, "data": { "scope": "clinic", "clinic": { "uuid": "string", "name": "string" }, "permissions": { "resources": { "appointments": { "view": true } } }, "stats": { "today_appointments": 12, "tomorrow_appointments": 7 }, "today_appointments": [ { "uuid": "string", "patient_name": "string | null", "patient_mobile": "string", "slot_start": 1700000000, "status": "reserved", "doctor_name": "string" } ] } } ``` `today_appointments` — only populated when `permissions.resources.appointments.view === true`; up to 10 records (doctor scope) / 20 (clinic scope) when visible. در scope کلینیک هر ردیف `doctor_name` هم دارد. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_FORBIDDEN_001` | 403 | Secretary relation not configured or inactive | --- ## GET /api/v1/admin/dashboard/charts Returns time-series chart data for admin dashboard. All series are filtered to the given `from`–`to` window. **Auth:** `ROLE_ADMIN` required ### Query params | Param | Type | Default | Description | |-------|------|---------|-------------| | `from` | int (unix) | 30 days ago | Period start | | `to` | int (unix) | now | Period end | ### Response `200` ```json { "success": true, "data": { "appointments_by_day": [ { "date": "06/01", "count": 12 } ], "revenue_by_day": [ { "date": "06/01", "amount": 3500000 } ], "appointment_status": [ { "status": "confirmed", "count": 320 } ], "top_specialties": [ { "name": "قلب و عروق", "count": 85 } ], "subscription_sales_by_plan": [ { "plan": "basic", "count": 14, "revenue": 4060000 } ], "period": { "from": 1717200000, "to": 1719792000 } } } ``` **Field notes:** - `appointments_by_day` / `revenue_by_day` — one entry per calendar day in the period; days with no data appear as `count: 0` / `amount: 0` - `appointment_status` — all-time counts, not filtered by period - `top_specialties` — top 8 by appointment volume, all-time - `subscription_sales_by_plan` — subscriptions created in period, grouped by plan; `revenue` sums only payments with status `received` --- ## Doctor dashboard is context-scoped (2026-07) `GET /api/v1/dashboard/doctor` now accepts an optional **`clinic_uuid`**. When absent it falls back to the caller's stored active context (`user_active_context`), then to their role. In a **clinic context** the response is restricted to that clinic: * appointment counts and `today_appointments` only include appointments whose `address_id` belongs to that clinic; * the financial fields are **omitted entirely** — `revenue_period_rials`, `today_payments_rials`, `week_payments_rials`, `sms_wallet_balance`, `unique_patients_count`, `total_patients`, and `charts.revenue_by_day`, and `charts.revenue_by_month`. They describe the doctor's personal practice and have no meaning inside someone else's clinic. They return only in the personal context, or for the clinic's own owner holding `payments.view`; * `clinics` comes back as `[]` — the "کلینیک‌های من" list belongs to the personal dashboard. A `clinic_uuid` the caller has no access to is ignored and the personal context is used. New response field: ```json "context": { "type": "personal" | "clinic", "clinic_uuid": "…|null", "clinic_name": "…|null" } ``` The admin SPA dispatches on this: `primaryRole === 'doctor' && context.scope === 'clinic'` renders `InvitedDoctorDashboard` (appointment tiles + today's list only) instead of the full doctor dashboard. Hiding the cards client-side was not enough — the endpoint is directly callable.