Files
clinicpro/docs/api/dashboard.md
T
hamed 5c4976d65f feat: Implement secretary permissions enforcement across multiple resources
- Added SecretaryAccessChecker to manage resource access for secretaries.
- Integrated permission checks for payments, inventory, and tags in relevant controllers.
- Updated PaymentController and PaymentMethodController to enforce secretary permissions.
- Enhanced TenantTagController to check permissions for tag management actions.
- Introduced tests for secretary resource enforcement, ensuring proper access control.
- Updated DoctorSecretary entity to include inventory and tags permissions.
- Created a comprehensive audit document for secretary permissions coverage and enforcement.
- Fixed potential crashes in SecretaryDashboard when rendering without doctor data.
2026-07-23 16:36:35 +03:30

12 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
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

{
  "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 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.appointments_by_day — «نمودار تعداد بیماران»: one entry per day of the requested Jalali month (2831 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

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

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

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

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