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>
117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
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>
|
|
);
|
|
}
|