Files
clinicpro/assets/admin/hooks/usePermissionCatalog.ts
T
hamedandClaude Opus 5 ddd5f8f75a 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>
2026-08-07 18:11:05 +03:30

86 lines
3.2 KiB
TypeScript

import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
export interface CatalogAction {
key: string;
label: string;
}
export interface CatalogResource {
key: string;
label: string;
/** فقط در محیط کلینیک معنا دارد — پزشک مستقل نباید ببیندش. */
clinic_only: boolean;
actions: CatalogAction[];
}
interface CatalogPayload {
version: number;
resources: CatalogResource[];
}
/**
* فهرستِ منابعِ قابل‌مجوزدهی — منبعِ واحدِ هر دو فرمِ مجوز (منشی و پزشکِ عضو کلینیک).
*
* تا پیش از این همین فهرست در سه فایل UI هاردکد بود و با بک‌اند واگرا می‌شد، پس
* صفحهٔ تازه مجوزِ صفحهٔ دیگری را قرض می‌گرفت. حالا افزودن یک ردیف به
* PermissionCatalog در بک‌اند کافی است.
*
* پاسخ به کاربر بستگی ندارد و تا وقتی رجیستری عوض نشود ثابت است.
*/
export function usePermissionCatalog() {
const query = useQuery({
queryKey: ['permission-catalog'],
queryFn: () => api.get<ApiResponse<CatalogPayload>>('/api/v1/permission-catalog'),
staleTime: Infinity,
});
// مرجعِ آرایه باید پایدار بماند: مصرف‌کننده‌ها آن را در وابستگیِ useEffect
// می‌گذارند و یک `?? []` تازه در هر رندر، حلقهٔ بی‌پایان می‌سازد.
const resources = useMemo(
() => query.data?.data?.resources ?? [],
[query.data],
);
return {
resources,
version: query.data?.data?.version ?? 1,
isLoading: query.isLoading,
isError: query.isError,
};
}
/** شکلِ کاملِ کاتالوگ با همهٔ اکشن‌ها خاموش — مبنای فرمِ «هیچ دسترسی». */
export function blankPermissions(resources: CatalogResource[]): Record<string, Record<string, boolean>> {
const out: Record<string, Record<string, boolean>> = {};
for (const resource of resources) {
out[resource.key] = {};
for (const action of resource.actions) {
out[resource.key][action.key] = false;
}
}
return out;
}
/**
* مقدارِ ذخیره‌شده را روی شکلِ کاتالوگ می‌نشاند.
*
* قرینهٔ PermissionCatalog::merge در بک‌اند: کلیدِ نبوده `false` می‌شود تا سوییچ
* از uncontrolled به controlled نپرد، و کلیدِ خارج از کاتالوگ نمایش داده نمی‌شود.
*/
export function alignPermissions(
stored: Record<string, Record<string, boolean>> | undefined | null,
resources: CatalogResource[],
): Record<string, Record<string, boolean>> {
const out: Record<string, Record<string, boolean>> = {};
for (const resource of resources) {
out[resource.key] = {};
for (const action of resource.actions) {
out[resource.key][action.key] = Boolean(stored?.[resource.key]?.[action.key]);
}
}
return out;
}