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:
@@ -50,8 +50,27 @@ const previousSecretary = {
|
||||
national_code: "9999999999",
|
||||
};
|
||||
|
||||
function mockData(rows = [activeSecretary, previousSecretary]) {
|
||||
/** فهرست بخشها از GET /api/v1/permission-catalog میآید، نه از کد کامپوننت. */
|
||||
const CATALOG = [
|
||||
{
|
||||
key: "appointments", label: "مدیریت نوبتها", clinic_only: false,
|
||||
actions: [{ key: "view", label: "مشاهده نوبتها" }, { key: "create", label: "ایجاد نوبت" }],
|
||||
},
|
||||
{
|
||||
key: "patients", label: "پرونده بیماران", clinic_only: false,
|
||||
actions: [{ key: "view", label: "مشاهده بیماران" }],
|
||||
},
|
||||
{
|
||||
key: "clinic_doctors", label: "مدیریت پزشکان کلینیک", clinic_only: true,
|
||||
actions: [{ key: "view", label: "مشاهده پزشکان" }],
|
||||
},
|
||||
];
|
||||
|
||||
function mockData(rows = [activeSecretary, previousSecretary], catalog: unknown[] = CATALOG) {
|
||||
get.mockImplementation((url: string) => {
|
||||
if (url.includes("permission-catalog")) {
|
||||
return Promise.resolve({ success: true, data: { version: 1, resources: catalog } });
|
||||
}
|
||||
if (url.includes("/secretaries/")) return Promise.resolve({ success: true, data: rows });
|
||||
return Promise.resolve({ success: true, data: [] });
|
||||
});
|
||||
@@ -94,7 +113,7 @@ describe("MySecretariesPage", () => {
|
||||
expect(await screen.findByText("هنوز منشی فعالی اضافه نشده است")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the add modal with permission sections based on existing pages", async () => {
|
||||
it("opens the add modal with permission sections from the catalog", async () => {
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
await screen.findAllByText("سارا احمدی");
|
||||
|
||||
@@ -102,9 +121,35 @@ describe("MySecretariesPage", () => {
|
||||
|
||||
expect(await screen.findByText("اضافه کردن منشی جدید")).toBeInTheDocument();
|
||||
expect(screen.getByText("مجوزهای دسترسی")).toBeInTheDocument();
|
||||
expect(screen.getByText("مدیریت نوبتها")).toBeInTheDocument();
|
||||
expect(await screen.findByText("مدیریت نوبتها")).toBeInTheDocument();
|
||||
expect(screen.getByText("پرونده بیماران")).toBeInTheDocument();
|
||||
expect(screen.getByText("مدیریت پرداختها")).toBeInTheDocument();
|
||||
expect(screen.getByText("مدیریت بیمهها")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/**
|
||||
* همان تضمینِ «داینامیک بودن»: منبع تازه فقط به mock اضافه میشود و هیچ خطی از
|
||||
* کد کامپوننت عوض نمیشود.
|
||||
*/
|
||||
it("shows a resource added to the catalog with no code change", async () => {
|
||||
mockData(undefined, [
|
||||
...CATALOG,
|
||||
{ key: "brand_new_page", label: "صفحهٔ کاملاً تازه", clinic_only: false, actions: [{ key: "view", label: "مشاهده" }] },
|
||||
]);
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
await screen.findAllByText("سارا احمدی");
|
||||
|
||||
fireEvent.click(screen.getByText("اضافه کردن منشی"));
|
||||
|
||||
expect(await screen.findByText("صفحهٔ کاملاً تازه")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/** منبعِ clinic_only برای پزشکِ مستقل (primaryRole=doctor) نباید دیده شود. */
|
||||
it("hides clinic-only resources for a standalone doctor", async () => {
|
||||
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
|
||||
await screen.findAllByText("سارا احمدی");
|
||||
|
||||
fireEvent.click(screen.getByText("اضافه کردن منشی"));
|
||||
|
||||
expect(await screen.findByText("مدیریت نوبتها")).toBeInTheDocument();
|
||||
expect(screen.queryByText("مدیریت پزشکان کلینیک")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user