Files
clinicpro/assets/admin/components/SecretaryPermissionsModal.tsx
hamedandClaude Opus 5 7c3407b0b3 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>
2026-08-07 19:55:32 +03:30

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>
);
}