- Implement DoctorPermissionsModal for managing doctor permissions in clinics. - Create usePermissions hook to handle user permissions context. - Add migration for clinic_doctor_permissions table with default permissions. - Develop ClinicDoctorPermissionController for handling permissions API. - Create ClinicDoctorPermission entity to manage permissions data. - Implement ClinicDoctorPermissionRepository for database interactions. - Add ClinicDoctorPermissionChecker for permission validation logic. - Write tests for clinic doctor permissions functionality.
178 lines
6.3 KiB
TypeScript
178 lines
6.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../../lib/api';
|
|
import type { ApiResponse } from '../../lib/api';
|
|
import Modal from './Modal';
|
|
|
|
/** envelope کامل — همان چیزی که بکاند برمیگرداند، بدون flatten. */
|
|
export interface PermissionEnvelope {
|
|
version: number;
|
|
resources: Record<string, Record<string, boolean>>;
|
|
}
|
|
|
|
export interface ClinicDoctorPermissionPayload {
|
|
uuid: string;
|
|
clinic_uuid: string;
|
|
doctor_uuid: string;
|
|
doctor_name: string;
|
|
active: boolean;
|
|
permissions: PermissionEnvelope;
|
|
}
|
|
|
|
const RESOURCE_LABELS: Record<string, { label: string; actions: Record<string, string> }> = {
|
|
appointments: {
|
|
label: 'نوبتها',
|
|
actions: { view: 'مشاهده', create: 'ایجاد', cancel: 'لغو', update_status: 'تغییر وضعیت' },
|
|
},
|
|
appointment_settings: {
|
|
label: 'تنظیمات نوبتدهی',
|
|
actions: { view: 'مشاهده', update: 'ویرایش' },
|
|
},
|
|
patients: {
|
|
label: 'پرونده بیماران',
|
|
actions: { view: 'مشاهده', create: 'ایجاد', update: 'ویرایش', delete: 'حذف' },
|
|
},
|
|
payments: {
|
|
label: 'پرداختها',
|
|
actions: { view: 'مشاهده', create: 'ایجاد', update: 'ویرایش', delete: 'حذف' },
|
|
},
|
|
services: {
|
|
label: 'خدمات',
|
|
actions: { view: 'مشاهده', update: 'ویرایش' },
|
|
},
|
|
clinic_info: {
|
|
label: 'اطلاعات کلینیک',
|
|
actions: { view: 'مشاهده', update: 'ویرایش' },
|
|
},
|
|
};
|
|
|
|
const ACTION_COLUMNS = ['view', 'create', 'update', 'delete', 'cancel', 'update_status'];
|
|
const ACTION_HEADERS = ['مشاهده', 'ایجاد', 'ویرایش', 'حذف', 'لغو', 'تغییر وضعیت'];
|
|
|
|
export default function DoctorPermissionsModal({ clinicUuid, doctorUuid, doctorName, onClose }: {
|
|
clinicUuid: string;
|
|
doctorUuid: string;
|
|
doctorName: string;
|
|
onClose: () => void;
|
|
}) {
|
|
const qc = useQueryClient();
|
|
const [resources, setResources] = useState<PermissionEnvelope['resources']>({});
|
|
const [active, setActive] = useState(true);
|
|
|
|
const permQ = useQuery({
|
|
queryKey: ['clinic-doctor-permissions', clinicUuid, doctorUuid],
|
|
queryFn: () => api.get<ApiResponse<ClinicDoctorPermissionPayload>>(
|
|
`/api/v1/admin/clinic/${clinicUuid}/doctor/${doctorUuid}/permissions`,
|
|
),
|
|
});
|
|
|
|
useEffect(() => {
|
|
const payload = permQ.data?.data;
|
|
if (!payload) return;
|
|
setResources(payload.permissions?.resources ?? {});
|
|
setActive(payload.active);
|
|
}, [permQ.data]);
|
|
|
|
const saveMut = useMutation({
|
|
mutationFn: () => api.patch<ApiResponse<ClinicDoctorPermissionPayload>>(
|
|
`/api/v1/admin/clinic/${clinicUuid}/doctor/${doctorUuid}/permissions`,
|
|
{ permissions: { resources }, active },
|
|
),
|
|
onSuccess: () => {
|
|
toast.success('دسترسیهای پزشک ذخیره شد');
|
|
qc.invalidateQueries({ queryKey: ['clinic-doctor-permissions', clinicUuid, doctorUuid] });
|
|
qc.invalidateQueries({ queryKey: ['clinic-doctors', clinicUuid] });
|
|
onClose();
|
|
},
|
|
onError: (e: Error) => toast.error(e.message),
|
|
});
|
|
|
|
const toggle = (resource: string, action: string) => {
|
|
setResources(prev => ({
|
|
...prev,
|
|
[resource]: { ...prev[resource], [action]: !prev[resource]?.[action] },
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
open
|
|
size="lg"
|
|
title={`دسترسیهای ${doctorName}`}
|
|
onClose={onClose}
|
|
footer={
|
|
<>
|
|
<button className="btn ghost sm" onClick={onClose}>انصراف</button>
|
|
<button
|
|
className="btn primary sm"
|
|
disabled={saveMut.isPending || permQ.isLoading}
|
|
onClick={() => saveMut.mutate()}
|
|
>
|
|
ذخیره
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
{permQ.isLoading ? (
|
|
<p className="muted">در حال بارگذاری...</p>
|
|
) : (
|
|
<>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={active}
|
|
onChange={() => setActive(v => !v)}
|
|
style={{ width: 16, height: 16, accentColor: 'var(--primary)', cursor: 'pointer' }}
|
|
/>
|
|
<span>دسترسی این پزشک به کلینیک فعال باشد</span>
|
|
</label>
|
|
|
|
<div style={{ overflowX: 'auto' }}>
|
|
<table className="t">
|
|
<thead>
|
|
<tr>
|
|
<th>بخش</th>
|
|
{ACTION_HEADERS.map(h => (
|
|
<th key={h} style={{ textAlign: 'center', fontSize: 12 }}>{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{Object.keys(RESOURCE_LABELS).map(resource => {
|
|
const config = RESOURCE_LABELS[resource];
|
|
return (
|
|
<tr key={resource}>
|
|
<td><b>{config.label}</b></td>
|
|
{ACTION_COLUMNS.map(action => {
|
|
if (!config.actions[action]) {
|
|
return <td key={action} style={{ textAlign: 'center', color: 'var(--border)' }}>—</td>;
|
|
}
|
|
return (
|
|
<td key={action} style={{ textAlign: 'center' }}>
|
|
<input
|
|
type="checkbox"
|
|
disabled={!active}
|
|
checked={resources[resource]?.[action] ?? false}
|
|
onChange={() => toggle(resource, action)}
|
|
style={{ width: 16, height: 16, accentColor: 'var(--primary)', cursor: 'pointer' }}
|
|
/>
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<p className="muted" style={{ fontSize: 12, marginTop: 12 }}>
|
|
این دسترسیها فقط داخل همین کلینیک اعمال میشوند؛ مطب شخصی پزشک تحت تأثیر قرار نمیگیرد.
|
|
</p>
|
|
</>
|
|
)}
|
|
</Modal>
|
|
);
|
|
}
|