- Removed the "دکتر" prefix from doctor names in various components and API responses to ensure consistency and clarity. - Updated the AppointmentDetailPage, CommentsPage, DashboardPage, RatingsPage, SecretariesPage, and other relevant files to reflect the changes in doctor name formatting. - Adjusted API documentation to align with the new naming conventions. - Implemented validation to prevent the creation of clinics without a name and restricted users to a single clinic. - Added tests to verify that doctor names are stored without titles and that clinic creation adheres to the new validation rules.
271 lines
9.9 KiB
TypeScript
271 lines
9.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { TrashIcon, PencilIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse, PaginatedResponse } from '../lib/api';
|
|
import type { Secretary, SecretaryPermissions } from '../types';
|
|
import { formatDate, maskMobile } from '../lib/utils';
|
|
import DataTable, { Column } from '../components/ui/DataTable';
|
|
import { ActiveBadge } from '../components/ui/StatusBadge';
|
|
import Pagination from '../components/ui/Pagination';
|
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
import Modal from '../components/ui/Modal';
|
|
|
|
const DEFAULT_PERMISSIONS: SecretaryPermissions = {
|
|
appointments: { view: true, create: false, cancel: false, update_status: false },
|
|
patients: { view: true, create: false, update: false, delete: false },
|
|
payments: { view: true, create: false, update: false, delete: false },
|
|
insurances: { view: true, create: false, update: false, delete: false },
|
|
addresses: { view: true, create: false, update: false, delete: false },
|
|
clinic_info: { view: true, update: false },
|
|
};
|
|
|
|
type PermSection = keyof SecretaryPermissions;
|
|
|
|
const PERMISSION_LABELS: Record<PermSection, { label: string; actions: { key: string; label: string }[] }> = {
|
|
appointments: {
|
|
label: 'نوبتها',
|
|
actions: [
|
|
{ key: 'view', label: 'مشاهده' },
|
|
{ key: 'create', label: 'ایجاد' },
|
|
{ key: 'cancel', label: 'لغو' },
|
|
{ key: 'update_status', label: 'تغییر وضعیت' },
|
|
],
|
|
},
|
|
patients: {
|
|
label: 'پرونده بیماران',
|
|
actions: [
|
|
{ key: 'view', label: 'مشاهده' },
|
|
{ key: 'create', label: 'ایجاد' },
|
|
{ key: 'update', label: 'ویرایش' },
|
|
{ key: 'delete', label: 'حذف' },
|
|
],
|
|
},
|
|
payments: {
|
|
label: 'پرداختها',
|
|
actions: [
|
|
{ key: 'view', label: 'مشاهده' },
|
|
{ key: 'create', label: 'ایجاد' },
|
|
{ key: 'update', label: 'ویرایش' },
|
|
{ key: 'delete', label: 'حذف' },
|
|
],
|
|
},
|
|
addresses: {
|
|
label: 'آدرسها',
|
|
actions: [
|
|
{ key: 'view', label: 'مشاهده' },
|
|
{ key: 'create', label: 'ایجاد' },
|
|
{ key: 'update', label: 'ویرایش' },
|
|
{ key: 'delete', label: 'حذف' },
|
|
],
|
|
},
|
|
clinic_info: {
|
|
label: 'اطلاعات کلینیک',
|
|
actions: [
|
|
{ key: 'view', label: 'مشاهده' },
|
|
{ key: 'update', label: 'ویرایش' },
|
|
],
|
|
},
|
|
insurances: {
|
|
label: 'بیمهها',
|
|
actions: [
|
|
{ key: 'view', label: 'مشاهده' },
|
|
{ key: 'create', label: 'ایجاد' },
|
|
{ key: 'update', label: 'ویرایش' },
|
|
{ key: 'delete', label: 'حذف' },
|
|
],
|
|
},
|
|
};
|
|
|
|
function PermissionsMatrix({
|
|
permissions,
|
|
onChange,
|
|
}: {
|
|
permissions: SecretaryPermissions;
|
|
onChange: (p: SecretaryPermissions) => void;
|
|
}) {
|
|
const toggle = (section: PermSection, action: string) => {
|
|
const current = (permissions[section] as Record<string, boolean>)[action];
|
|
onChange({
|
|
...permissions,
|
|
[section]: { ...(permissions[section] as Record<string, boolean>), [action]: !current },
|
|
});
|
|
};
|
|
|
|
const allActions = ['view', 'create', 'update', 'delete', 'cancel', 'update_status'];
|
|
const actionHeaders = ['مشاهده', 'ایجاد', 'ویرایش', 'حذف', 'لغو', 'تغییر وضعیت'];
|
|
|
|
return (
|
|
<div style={{ overflowX: 'auto' }}>
|
|
<table className="t">
|
|
<thead>
|
|
<tr>
|
|
<th>بخش</th>
|
|
{actionHeaders.map((h) => (
|
|
<th key={h} style={{ textAlign: 'center', fontSize: 12 }}>{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(Object.keys(PERMISSION_LABELS) as PermSection[]).map((section) => {
|
|
const config = PERMISSION_LABELS[section];
|
|
const sectionPerms = permissions[section] as Record<string, boolean>;
|
|
return (
|
|
<tr key={section}>
|
|
<td><b>{config.label}</b></td>
|
|
{allActions.map((action) => {
|
|
const actionConfig = config.actions.find((a) => a.key === action);
|
|
if (!actionConfig) {
|
|
return <td key={action} style={{ textAlign: 'center', color: 'var(--border)' }}>—</td>;
|
|
}
|
|
return (
|
|
<td key={action} style={{ textAlign: 'center' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={sectionPerms[action] ?? false}
|
|
onChange={() => toggle(section, action)}
|
|
style={{ width: 16, height: 16, accentColor: 'var(--primary)', cursor: 'pointer' }}
|
|
/>
|
|
</td>
|
|
);
|
|
})}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function SecretariesPage() {
|
|
const qc = useQueryClient();
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearch] = useState('');
|
|
const [editTarget, setEditTarget] = useState<Secretary | null>(null);
|
|
const [editPerms, setEditPerms] = useState<SecretaryPermissions>(DEFAULT_PERMISSIONS);
|
|
const [deleteTarget, setDeleteTarget] = useState<Secretary | null>(null);
|
|
const limit = 15;
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['secretaries', page, search],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
|
if (search) params.set('search', search);
|
|
return api.get<PaginatedResponse<Secretary>>(`/api/v1/admin/secretaries?${params}`);
|
|
},
|
|
});
|
|
|
|
const updatePermsMutation = useMutation({
|
|
mutationFn: ({ uuid, permissions }: { uuid: string; permissions: SecretaryPermissions }) =>
|
|
api.patch<ApiResponse<null>>(`/api/v1/secretary/${uuid}`, { permissions }),
|
|
onSuccess: () => {
|
|
toast.success('دسترسیها بروزرسانی شد');
|
|
setEditTarget(null);
|
|
qc.invalidateQueries({ queryKey: ['secretaries'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (s: Secretary) => api.delete<ApiResponse<null>>(`/api/v1/secretary/${s.uuid}`),
|
|
onSuccess: () => {
|
|
toast.success('منشی حذف شد');
|
|
setDeleteTarget(null);
|
|
qc.invalidateQueries({ queryKey: ['secretaries'] });
|
|
},
|
|
onError: (err: Error) => toast.error(err.message),
|
|
});
|
|
|
|
const openEdit = (s: Secretary) => {
|
|
setEditTarget(s);
|
|
setEditPerms(s.permissions ?? DEFAULT_PERMISSIONS);
|
|
};
|
|
|
|
const columns: Column<Secretary>[] = [
|
|
{ key: 'user_name', header: 'نام', render: (s) => <b>{s.user_name}</b> },
|
|
{ key: 'mobile_number', header: 'موبایل', render: (s) => <span dir="ltr">{maskMobile(s.mobile_number)}</span> },
|
|
{ key: 'doctor_name', header: 'پزشک', render: (s) => s.doctor_name },
|
|
{ key: 'is_active', header: 'وضعیت', render: (s) => <ActiveBadge active={s.is_active} /> },
|
|
{ key: 'created_at', header: 'تاریخ ثبت', render: (s) => formatDate(s.created_at) },
|
|
];
|
|
|
|
const items = data?.data ?? [];
|
|
const total = data?.meta?.totalRecords ?? 0;
|
|
|
|
return (
|
|
<div className="fade-in">
|
|
<div className="card-title-row" style={{ marginBottom: 'var(--gap)' }}>
|
|
<div>
|
|
<h1 className="section-title">منشیها</h1>
|
|
<div className="muted">{total} منشی ثبتشده</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<div className="card-pad" style={{ paddingBottom: 0 }}>
|
|
<div className="toolbar">
|
|
<div className="field" style={{ minWidth: 240 }}>
|
|
<MagnifyingGlassIcon style={{ width: 17, height: 17 }} />
|
|
<input
|
|
value={search}
|
|
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
|
placeholder="جستجو بر اساس نام یا پزشک..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DataTable<Secretary>
|
|
columns={columns}
|
|
data={items}
|
|
loading={isLoading}
|
|
emptyMessage="هیچ منشیای یافت نشد"
|
|
actions={(sec) => (
|
|
<>
|
|
<button onClick={() => openEdit(sec)} className="mini-btn" title="ویرایش دسترسیها">
|
|
<PencilIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
<button onClick={() => setDeleteTarget(sec)} className="mini-btn danger" title="حذف">
|
|
<TrashIcon style={{ width: 15, height: 15 }} />
|
|
</button>
|
|
</>
|
|
)}
|
|
/>
|
|
<Pagination page={page} total={total} limit={limit} onPageChange={setPage} />
|
|
</div>
|
|
|
|
<Modal
|
|
open={!!editTarget}
|
|
title={`دسترسیهای ${editTarget?.user_name}`}
|
|
size="lg"
|
|
onClose={() => setEditTarget(null)}
|
|
footer={
|
|
<>
|
|
<button onClick={() => setEditTarget(null)} className="btn ghost sm">لغو</button>
|
|
<button
|
|
onClick={() => editTarget && updatePermsMutation.mutate({ uuid: editTarget.uuid, permissions: editPerms })}
|
|
disabled={updatePermsMutation.isPending}
|
|
className="btn primary sm">
|
|
{updatePermsMutation.isPending ? 'در حال ذخیره...' : 'ذخیره دسترسیها'}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<PermissionsMatrix permissions={editPerms} onChange={setEditPerms} />
|
|
</Modal>
|
|
|
|
<ConfirmDialog
|
|
open={!!deleteTarget}
|
|
title="حذف منشی"
|
|
message={`آیا از حذف منشی "${deleteTarget?.user_name}" اطمینان دارید؟`}
|
|
confirmLabel="حذف"
|
|
danger
|
|
loading={deleteMutation.isPending}
|
|
onConfirm={() => deleteTarget && deleteMutation.mutate(deleteTarget)}
|
|
onCancel={() => setDeleteTarget(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|