feat(permissions): render both permission forms from the catalog, fix borrowed gates

The three hardcoded resource lists in the admin panel are gone. MySecretariesPage,
SecretariesPage and DoctorPermissionsModal now render from
GET /api/v1/permission-catalog, so a resource added to the backend registry shows
up in all of them with no frontend change. Each has a test that proves exactly
that by adding a resource to the mock and asserting it renders.

SecretaryPermissions was an interface with a field per resource, which made
"dynamic" impossible in TypeScript — every new resource would have been a compile
error. It is now an open map. Only two files consumed it.

The borrowed gates are corrected:
- five resource pages moved off appointment_settings onto their own 'resources'
- treatment-cases moved off appointments onto 'treatment'
- service-categories moved onto 'services', which is what ServiceCatalogController
  actually manages (categories, item groups, service relations) — not resources

TreatmentCaseController had no permission gate at all, only IS_AUTHENTICATED_FULLY,
so any secretary could read and edit treatment cases. All seven of its actions are
now gated on treatment view/update.

ResourcePermissionTrait takes the resource from an overridable method instead of
hardcoding appointment_settings. HolidayController overrides it back, since the
holidays page really is appointment settings. The booking gate keeps its
appointments.view fallback so a secretary who may book is not blocked by a
resource-config permission.

Defaults were picked to preserve today's effective access, so no role gains or
loses a page from this move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 18:11:05 +03:30
co-authored by Claude Opus 5
parent dc40651308
commit ddd5f8f75a
17 changed files with 389 additions and 536 deletions
+8 -86
View File
@@ -512,92 +512,14 @@ export interface Secretary {
};
}
export interface SecretaryPermissions {
appointments: {
view: boolean;
create: boolean;
cancel: boolean;
update_status: boolean;
};
patients: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
payments: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
insurances: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
addresses: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
clinic_info: {
view: boolean;
update: boolean;
};
inventory: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
tags: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
services: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
staff: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
discounts: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
sms: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
appointment_settings: {
view: boolean;
update: boolean;
};
clinic_doctors: {
view: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
subscription: {
view: boolean;
create: boolean;
};
}
/**
* مجوزهای یک رابطه — نقشهٔ باز، نه interface با فیلدهای ثابت.
*
* فهرستِ منابع از GET /api/v1/permission-catalog می‌آید و ممکن است با افزودن
* صفحهٔ تازه رشد کند؛ تایپِ فیلد-به-فیلد یعنی هر منبع جدید یک خطای کامپایل.
* منبع/اکشنِ ناشناخته با ?? false خوانده می‌شود.
*/
export type SecretaryPermissions = Record<string, Record<string, boolean>>;
export interface Specialty {
id?: number;