diff --git a/assets/admin/components/TreatmentCaseEditModal.tsx b/assets/admin/components/TreatmentCaseEditModal.tsx
index a499eac2..86d065a2 100644
--- a/assets/admin/components/TreatmentCaseEditModal.tsx
+++ b/assets/admin/components/TreatmentCaseEditModal.tsx
@@ -67,6 +67,7 @@ export default function TreatmentCaseEditModal({ caseUuid, onClose }: {
doctors={doctorsQ.data?.data?.data ?? []}
doctorsLoading={doctorsQ.isLoading}
staff={staffQ.data?.data ?? []}
+ staffLoading={staffQ.isLoading}
onSaved={() => {
qc.invalidateQueries({ queryKey: ['treatment-cases'] });
qc.invalidateQueries({ queryKey: ['treatment-case', caseUuid] });
@@ -79,11 +80,12 @@ export default function TreatmentCaseEditModal({ caseUuid, onClose }: {
);
}
-function EditForm({ detail, doctors, doctorsLoading, staff, onSaved, onClose }: {
+function EditForm({ detail, doctors, doctorsLoading, staff, staffLoading, onSaved, onClose }: {
detail: TreatmentCaseDetail;
doctors: DoctorRow[];
doctorsLoading: boolean;
staff: StaffRow[];
+ staffLoading: boolean;
onSaved: () => void;
onClose: () => void;
}) {
@@ -208,7 +210,11 @@ function EditForm({ detail, doctors, doctorsLoading, staff, onSaved, onClose }:
- {staff.length === 0 ? (
+ {/* «در حال خواندن» با «تعریف نشده» یکی نیست؛ بدون این تفکیک، فهرستِ هنوز
+ نیامده بهصورت «پرسنلی ندارید» خوانده میشد. */}
+ {staffLoading ? (
+
در حال خواندن فهرست پرسنل…
+ ) : staff.length === 0 ? (
پرسنلی در این محیط تعریف نشده است.
) : (
diff --git a/assets/admin/pages/TreatmentCasesPage.test.tsx b/assets/admin/pages/TreatmentCasesPage.test.tsx
index 4479b209..c5765f71 100644
--- a/assets/admin/pages/TreatmentCasesPage.test.tsx
+++ b/assets/admin/pages/TreatmentCasesPage.test.tsx
@@ -47,6 +47,8 @@ describe('صفحهٔ پروندههای درمان', () => {
mockList([caseRow()]);
renderWithProviders();
+ await screen.findByText('محمد رسولی');
+ fireEvent.click(screen.getByLabelText('نمایش کارتی'));
const name = await screen.findByText('محمد رسولی');
expect(name.tagName).toBe('STRONG');
@@ -120,6 +122,8 @@ describe('صفحهٔ پروندههای درمان', () => {
mockList([caseRow({ opened_at: 1_786_000_000 })]);
renderWithProviders();
+ await screen.findByText('محمد رسولی');
+ fireEvent.click(screen.getByLabelText('نمایش کارتی'));
// formatDateTime ساعت را با «:» میآورد؛ formatDate نمیآورد.
const start = await screen.findByText(/^شروع:/);
@@ -131,6 +135,8 @@ describe('صفحهٔ پروندههای درمان', () => {
mockList([caseRow({ performed_by: [{ uuid: 'st-1', name: 'پرسنل۱' }] })]);
renderWithProviders();
+ await screen.findByText('محمد رسولی');
+ fireEvent.click(screen.getByLabelText('نمایش کارتی'));
expect(await screen.findByText(/انجامدهنده: پرسنل۱/)).toBeInTheDocument();
});
@@ -140,10 +146,37 @@ describe('صفحهٔ پروندههای درمان', () => {
mockList([caseRow({ assigned_staff: [{ uuid: 'st-2', name: 'پرسنل۲' }] })]);
renderWithProviders();
+ await screen.findByText('محمد رسولی');
+ fireEvent.click(screen.getByLabelText('نمایش کارتی'));
expect(await screen.findByText(/اپراتور: پرسنل۲/)).toBeInTheDocument();
});
+ /** نمای پیشفرض جدول است — همان الگوی صفحهٔ پروندهها، با سوییچ به کارتی. */
+ it('پیشفرض جدولی است و به کارتی سوییچ میشود', async () => {
+ mockList([caseRow()]);
+
+ renderWithProviders();
+ await screen.findByText('محمد رسولی');
+
+ expect(screen.getByRole('columnheader', { name: 'پرسنل' })).toBeInTheDocument();
+
+ fireEvent.click(screen.getByLabelText('نمایش کارتی'));
+ expect(screen.queryByRole('columnheader', { name: 'پرسنل' })).not.toBeInTheDocument();
+
+ fireEvent.click(screen.getByLabelText('نمایش جدولی'));
+ expect(screen.getByRole('columnheader', { name: 'پرسنل' })).toBeInTheDocument();
+ });
+
+ it('ستون پرسنل در جدول پر میشود', async () => {
+ mockList([caseRow({ performed_by: [{ uuid: 'st-1', name: 'پرسنل۱' }] })]);
+
+ renderWithProviders();
+
+ const cell = await screen.findByRole('cell', { name: 'پرسنل۱' });
+ expect(cell).toBeInTheDocument();
+ });
+
it('دکمهٔ ویرایش مودال را باز میکند', async () => {
mockList([caseRow()]);
diff --git a/assets/admin/pages/TreatmentCasesPage.tsx b/assets/admin/pages/TreatmentCasesPage.tsx
index 23d2729c..a89f57a1 100644
--- a/assets/admin/pages/TreatmentCasesPage.tsx
+++ b/assets/admin/pages/TreatmentCasesPage.tsx
@@ -10,6 +10,7 @@ import { formatDate, formatDateTime, formatNumber } from '../lib/utils';
import { useUrlState } from '../hooks/useUrlState';
import PersianDateInput from '../components/ui/PersianDateInput';
import TreatmentCaseEditModal from '../components/TreatmentCaseEditModal';
+import { PatientsGridView, PatientsCategoryView } from '../components/icons/FilesToolbarIcons';
import type { TreatmentCaseSummary, StaffTreatmentSession, SlotSuggestionResponse } from '../types';
const TABS = [
@@ -32,7 +33,7 @@ const CASE_STATUS_LABEL: Record = {
* را جواب میدهند — «کدام بیمار در چه مرحلهای است و چه کاری مانده».
*/
export default function TreatmentCasesPage() {
- const [urlState, setUrlState] = useUrlState({ tab: 'cases', status: '', q: '', from: '', to: '' });
+ const [urlState, setUrlState] = useUrlState({ tab: 'cases', status: '', q: '', from: '', to: '', view: 'table' });
const tab = (TABS.some((t) => t.id === urlState.tab) ? urlState.tab : 'cases') as TabId;
return (
@@ -62,6 +63,8 @@ export default function TreatmentCasesPage() {
onFrom={(from) => setUrlState({ from })}
to={urlState.to}
onTo={(to) => setUrlState({ to })}
+ view={urlState.view === 'card' ? 'card' : 'table'}
+ onView={(v) => setUrlState({ view: v })}
/>
: }
>
@@ -75,7 +78,7 @@ const STATUS_FILTERS = [
['abandoned', 'رها شده'],
] as const;
-function CasesTab({ status, onStatus, search, onSearch, from, onFrom, to, onTo }: {
+function CasesTab({ status, onStatus, search, onSearch, from, onFrom, to, onTo, view, onView }: {
status: string;
onStatus: (s: string) => void;
search: string;
@@ -85,6 +88,9 @@ function CasesTab({ status, onStatus, search, onSearch, from, onFrom, to, onTo }
onFrom: (s: string) => void;
to: string;
onTo: (s: string) => void;
+ /** جدولی یا کارتی — همان الگوی صفحهٔ پروندهها، در URL تا «بازگشت» نما را نپراند. */
+ view: 'table' | 'card';
+ onView: (v: 'table' | 'card') => void;
}) {
// فیلد جستجو محلی میماند و فقط مقدار نهایی به URL میرود؛ وگرنه هر حرف یک ورودی
// تاریخچه میسازد و «بازگشت» بیمعنی میشود.
@@ -134,6 +140,29 @@ function CasesTab({ status, onStatus, search, onSearch, from, onFrom, to, onTo }
)}
+
+ {([['table', 'نمایش جدولی', PatientsGridView], ['card', 'نمایش کارتی', PatientsCategoryView]] as const).map(
+ ([v, label, Icon]) => (
+
+ ),
+ )}
+
+
{STATUS_FILTERS.map(([v, label]) => (