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:
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../lib/api';
|
||||
import Modal from './ui/Modal';
|
||||
import PermissionAccordions from './ui/PermissionAccordions';
|
||||
import { usePermissionCatalog, alignPermissions } from '../hooks/usePermissionCatalog';
|
||||
import type { Secretary, SecretaryPermissions } from '../types';
|
||||
|
||||
/**
|
||||
* دسترسیهای یک منشی — جدا از فرمِ پروفایل.
|
||||
*
|
||||
* تا پیش از این هر دو در یک مودال بودند و افزودنِ یک منشی یعنی تصمیمگیری دربارهٔ
|
||||
* همهٔ منابع در همان لحظه. با رجیستریِ داینامیک تعداد منابع با هر صفحهٔ تازه بیشتر
|
||||
* میشود، پس آن مودال ذاتاً بلندتر میشد.
|
||||
*
|
||||
* ذخیره روی **همهٔ** ردیفهای رابطه اجرا میشود: یک منشی به ازای هر پزشک یک ردیف
|
||||
* دارد و مجوزها باید در همه یکسان بمانند — همان قاعدهای که ویرایش پروفایل دارد.
|
||||
*/
|
||||
export default function SecretaryPermissionsModal({
|
||||
open,
|
||||
secretary,
|
||||
links,
|
||||
isClinic,
|
||||
readOnly,
|
||||
onClose,
|
||||
onSaved,
|
||||
}: {
|
||||
open: boolean;
|
||||
secretary: Secretary | null;
|
||||
/** uuid هر رابطهٔ پزشک-منشی. */
|
||||
links: string[];
|
||||
isClinic: boolean;
|
||||
readOnly?: boolean;
|
||||
onClose: () => void;
|
||||
onSaved?: () => void;
|
||||
}) {
|
||||
const catalog = usePermissionCatalog();
|
||||
const [permissions, setPermissions] = useState<SecretaryPermissions>({});
|
||||
|
||||
// شکل را کاتالوگ میدهد؛ تا نیامده مقداردهی نمیشود وگرنه سوییچها از
|
||||
// uncontrolled به controlled میپرند و مقدارِ ذخیرهشده پاک میشود.
|
||||
useEffect(() => {
|
||||
if (!open || catalog.isLoading) return;
|
||||
setPermissions(alignPermissions(secretary?.permissions, catalog.resources));
|
||||
}, [open, secretary, catalog.isLoading, catalog.resources]);
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () =>
|
||||
Promise.all(
|
||||
links.map((uuid) =>
|
||||
api.patch(`/api/v1/secretary/${uuid}`, {
|
||||
permissions: { version: 1, resources: permissions },
|
||||
}),
|
||||
),
|
||||
),
|
||||
onSuccess: () => {
|
||||
toast.success('دسترسیهای منشی ذخیره شد');
|
||||
onSaved?.();
|
||||
onClose();
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const setPermission = (section: string, item: string, value: boolean) =>
|
||||
setPermissions((prev) => ({
|
||||
...prev,
|
||||
[section]: { ...prev[section], [item]: value },
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
size="lg"
|
||||
title={`دسترسیهای ${secretary?.user_name ?? 'منشی'}`}
|
||||
onClose={onClose}
|
||||
footer={
|
||||
<>
|
||||
<button type="button" className="btn ghost sm" onClick={onClose}>
|
||||
{readOnly ? 'بستن' : 'انصراف'}
|
||||
</button>
|
||||
{!readOnly && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary sm"
|
||||
disabled={save.isPending || catalog.isLoading}
|
||||
onClick={() => save.mutate()}
|
||||
>
|
||||
{save.isPending ? 'در حال ذخیره...' : 'ذخیره دسترسیها'}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{catalog.isLoading ? (
|
||||
<p className="muted">در حال بارگذاری فهرست دسترسیها...</p>
|
||||
) : catalog.isError ? (
|
||||
<p className="muted">فهرست دسترسیها خوانده نشد. مودال را دوباره باز کنید.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="muted" style={{ fontSize: 12.5, marginBottom: 14, lineHeight: 1.9 }}>
|
||||
هر بخش را باز کنید تا اجزایش را ببینید. عددِ کنار هر بخش میگوید چند مورد از
|
||||
آن روشن است.
|
||||
</p>
|
||||
<PermissionAccordions
|
||||
permissions={permissions}
|
||||
onChange={setPermission}
|
||||
disabled={readOnly}
|
||||
isClinic={isClinic}
|
||||
resources={catalog.resources}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user