Files
clinicpro/docs/api/dashboard.md
T
hamed f1258d206d feat(migrations): add clinic_id context to weekly_schedules, date_overrides, and holidays
- Introduced clinic_id to weekly_schedules, date_overrides, and holidays to differentiate between personal and clinic schedules.
- Updated unique constraints and indexes to accommodate the new clinic context.

feat(command): create AssignScheduleClinicCommand to move schedules

- Added a command to move a doctor's personal weekly schedule into a clinic context.
- Implemented checks to ensure sessions align with the target clinic.

feat(context): implement EntityContext and EntityContextResolver

- Created EntityContext to represent the effective working environment of a request (doctor or clinic).
- Developed EntityContextResolver to determine the execution context based on user roles and active contexts.

test: add ServiceModeContextTest for appointment scheduling

- Implemented tests to ensure service booking respects clinic and personal contexts.
- Verified that financial data is omitted in clinic contexts in InvitedDoctorDashboardScopeTest.
2026-07-18 13:32:56 +03:30

300 lines
8.9 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.
# 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 |
### 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 }
],
"appointments_by_day": [
{ "label": "۷ خرداد", "count": 9 }
]
},
"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.revenue_by_day` / `charts.appointments_by_day` — last 7 days series (Jalali day label); revenue drives the «میزان درآمد» area chart, appointments the «نمودار تعداد مراجعین» bar chart
- `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
- `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 |
### 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 } ],
"appointments_by_day": [ { "label": "۷ خرداد", "count": 4 } ]
},
"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
### 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`
```json
{
"success": true,
"data": {
"doctor": {
"uuid": "string",
"name": "string",
"degree": "string | null"
},
"permissions": {
"resources": {
"appointments": {
"view": true,
"edit": false
}
}
},
"stats": {
"today_appointments": 8,
"tomorrow_appointments": 5
},
"today_appointments": [
{
"uuid": "string",
"patient_name": "string | null",
"patient_mobile": "string",
"slot_start": 1700000000,
"status": "reserved"
}
]
}
}
```
`today_appointments` — only populated when `permissions.resources.appointments.view === true`; up to 10 records when visible.
### 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`. 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.