- 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.
12 KiB
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 thefrom–toperiodtotal_patients— distinct patients ever (no period filter)revenue_period_rials— sum offinal_price_rialsfrom all patient sessions in the periodtoday_payments_rials/week_payments_rials— revenue for today / the last 7 dayscharts.appointments_by_day— «نمودار تعداد بیماران»: one entry per day of the requested Jalali month (28–31 entries), label = Jalali day number in Persian digits, empty days arecount: 0charts.revenue_by_month— «میزان درآمد»: exactly 12 entries, one per Jalali month ofrevenue_year, label = Persian month namecharts.revenue_by_day— last 7 days; kept only as the source oftoday_payments_rials/week_payments_rials, not drawn by the dashboard chartscharts_period— the effective (post-clamp) chart period, so the UI can reflect what was actually renderedtoday_appointments— up to 5 records, ordered byslot_start ASC; each row carriespatient_mobile,doctor_name(personnel),service_name(nullable — booked service item), andslot_endfor the «لیست نوبتهای جدید» dashboard table (that table renders read-only —statusis 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 byslot_start ASC; each row carriespatient_mobile,doctor_name,service_name(nullable), andslot_endfor the dashboard appointments tableavg_rating— rounded to 1 decimal;nullif no ratings yetclinics— all clinics the doctor belongs tosms_wallet_balance,unique_patients_count,revenue_period_rials— same semantics as clinic dashboardcharts.*,charts_period— same Jalali-period semantics as the clinic dashboard;revenue_by_day/revenue_by_monthare 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 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
{
"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 ascount: 0/amount: 0appointment_status— all-time counts, not filtered by periodtop_specialties— top 8 by appointment volume, all-timesubscription_sales_by_plan— subscriptions created in period, grouped by plan;revenuesums only payments with statusreceived
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_appointmentsonly include appointments whoseaddress_idbelongs 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, andcharts.revenue_by_day, andcharts.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 holdingpayments.view; clinicscomes 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.