# 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.
