refactor(admin): split the secretary form from its permissions into two modals

Adding a secretary meant deciding all 17 permission resources in the same
dialog. The full-height capture showed the form running past 1300px with the
save button below 16 accordions, and the dynamic registry makes that worse: every
page added in future lengthens this one modal.

The add/edit modal now carries only doctors, profile and address, and fits on
screen with its footer visible. Permissions move to SecretaryPermissionsModal,
reachable from a row action and opened automatically right after a successful
add, since a new secretary starts on the role defaults and the owner usually
wants to set them.

Neither create nor update sends permissions any more — the backend seeds the role
defaults on create, and the permissions modal owns the writes, fanning out over
every link row so a secretary shared across doctors stays consistent.

PermissionAccordions moves to components/ui as a shared component. Sections now
start collapsed with a granted/total badge on each header, so the panel opens at
a fixed height and still says which sections are on.

Two design-system slips caught by re-screenshotting rather than by the audit:
- a text button as a third row action pushed the name column out of the table, so
  the desktop row uses an icon with a title and the mobile card keeps the label
- .btn.secondary is not defined in styles.css (variants are primary/ghost/soft/
  danger/accent), so it renders as a bare .btn. Used ghost here. 25 other files
  have the same dead class; left alone as a separate sweep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 19:55:32 +03:30
co-authored by Claude Opus 5
parent a6eee1ce9d
commit 7c3407b0b3
4 changed files with 339 additions and 116 deletions
+36 -4
View File
@@ -113,14 +113,28 @@ describe("MySecretariesPage", () => {
expect(await screen.findByText("هنوز منشی فعالی اضافه نشده است")).toBeInTheDocument();
});
it("opens the add modal with permission sections from the catalog", async () => {
// ── جداسازی فرمِ منشی از دسترسی‌ها ──────────────────────────────────────
// مودالِ افزودن دیگر مجوز ندارد؛ مجوزها مودالِ خودشان را دارند تا افزودنِ یک
// منشی به تصمیم‌گیری دربارهٔ همهٔ منابع گره نخورد.
it("keeps permissions out of the add form", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getByText("اضافه کردن منشی"));
expect(await screen.findByText("اضافه کردن منشی جدید")).toBeInTheDocument();
expect(screen.getByText(جوزهای دسترسی")).toBeInTheDocument();
expect(screen.queryByText(دیریت نوبت‌ها")).not.toBeInTheDocument();
expect(screen.queryByText("پرونده بیماران")).not.toBeInTheDocument();
});
it("opens a separate permissions modal from the row action", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getAllByRole("button", { name: "دسترسی‌ها" })[0]);
expect(await screen.findByText("دسترسی‌های سارا احمدی")).toBeInTheDocument();
expect(await screen.findByText("مدیریت نوبت‌ها")).toBeInTheDocument();
expect(screen.getByText("پرونده بیماران")).toBeInTheDocument();
});
@@ -137,7 +151,7 @@ describe("MySecretariesPage", () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getByText("اضافه کردن منشی"));
fireEvent.click(screen.getAllByRole("button", { name: "دسترسی‌ها" })[0]);
expect(await screen.findByText("صفحهٔ کاملاً تازه")).toBeInTheDocument();
});
@@ -147,9 +161,27 @@ describe("MySecretariesPage", () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getByText("اضافه کردن منشی"));
fireEvent.click(screen.getAllByRole("button", { name: "دسترسی‌ها" })[0]);
expect(await screen.findByText("مدیریت نوبت‌ها")).toBeInTheDocument();
expect(screen.queryByText("مدیریت پزشکان کلینیک")).not.toBeInTheDocument();
});
/** آکاردئون‌ها بسته باز می‌شوند و شمارِ روشن‌ها روی هدر دیده می‌شود. */
it("starts every section collapsed with a granted counter", async () => {
renderWithProviders(<MySecretariesPage />, { route: "/admin/my-secretaries" });
await screen.findAllByText("سارا احمدی");
fireEvent.click(screen.getAllByRole("button", { name: "دسترسی‌ها" })[0]);
await screen.findByText("مدیریت نوبت‌ها");
// appointments.view=true در fullPerms → «۱ از ۲» با ارقام فارسی
expect(screen.getByText("۱ از ۲")).toBeInTheDocument();
// بسته است، پس سوییچِ داخلش رندر نشده
expect(screen.queryByLabelText("مدیریت نوبت‌ها — مشاهده نوبت‌ها")).not.toBeInTheDocument();
fireEvent.click(screen.getByText("مدیریت نوبت‌ها"));
expect(await screen.findByLabelText("مدیریت نوبت‌ها — مشاهده نوبت‌ها")).toBeInTheDocument();
});
});