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

8.9 KiB
Raw Blame History

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

{
  "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 fromto 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

{
  "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

{
  "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 fromto 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

{
  "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 entirelyrevenue_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:

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