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>
86 lines
3.2 KiB
TypeScript
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;
|
|
}
|