Files
clinicpro/assets/admin/pages/TreatmentCasesPage.test.tsx
T
hamedandClaude Opus 5 b78f7311cf feat(treatment): per-case operators, shown on the list and searchable
A treatment case said which doctor supervised it but never who actually did the
work, so the list could not answer the first question a manager asks about a
course: who performed it.

Two separate things now travel with the case. `performed_by` is history —
derived from the sessions' performedBy, so it only ever reports what happened.
`assigned_staff` is plan — a new treatment_case_staff table, editable from the
modal, saying who is meant to handle this patient's course. The card shows the
first and falls back to the second while nothing has been performed yet.

Search matches both. A manager typing an operator's name wants that person's
work, and work already done is part of it.

Assignment also narrows the operator queue: a case with assigned staff shows its
sessions only to those people, because a patient who started a multi-session
course with one operator should keep them. An unassigned case keeps the existing
protocol rule, and an empty list means "anyone the protocol allows" rather than
"nobody" — the same "no rows is not a restriction" convention used elsewhere.

Unlike areas, removing an operator erases nothing: a finished session carries its
real operator on itself and never consults this list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:55:01 +03:30

156 lines
6.1 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, fireEvent, waitFor } from '@testing-library/react';
import { renderWithProviders } from '../test/utils';
vi.mock('../lib/api', () => ({
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
ApiError: class extends Error {},
}));
import { api } from '../lib/api';
import TreatmentCasesPage from './TreatmentCasesPage';
const get = api.get as ReturnType<typeof vi.fn>;
function caseRow(over: Record<string, unknown> = {}) {
return {
uuid: 'case-1',
status: 'active',
total_sessions: 3,
completed_sessions: 1,
opened_at: 1_786_000_000,
closed_at: null,
service: { uuid: 'svc-1', name: 'لیزر توتال' },
supervisor: { uuid: 'doc-1', name: 'پزشک مدیسا' },
patient: { record_uuid: 'rec-1', name: 'محمد رسولی', mobile: '09120001111', record_number: '۱۲' },
areas: [{ uuid: 'ca-1', name: 'دست', category_uuid: 'cat-1' }],
assigned_staff: [],
performed_by: [],
...over,
};
}
/** فقط اندپوینت فهرست را جواب می‌دهد؛ بقیه خالی. */
function mockList(rows: unknown[]) {
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/treatment-cases')) return Promise.resolve({ success: true, data: rows });
return Promise.resolve({ success: true, data: [] });
});
}
beforeEach(() => {
get.mockReset();
});
describe('صفحهٔ پرونده‌های درمان', () => {
it('نام بیمار سرتیتر کارت است، نه نام سرویس', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
const name = await screen.findByText('محمد رسولی');
expect(name.tagName).toBe('STRONG');
expect(screen.getByText('لیزر توتال')).toBeInTheDocument();
});
/** جستجو باید به سرور برود، نه اینکه فهرست را در مرورگر فیلتر کند. */
it('عبارت جستجو را به‌صورت پارامتر q می‌فرستد', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.change(screen.getByLabelText('جستجوی پرونده'), { target: { value: 'رسولی' } });
await waitFor(
() => expect(get.mock.calls.some(
(c: unknown[]) => typeof c[0] === 'string' && c[0].includes('q=%D8%B1%D8%B3%D9%88%D9%84%DB%8C'),
)).toBe(true),
{ timeout: 2000 },
);
});
it('فیلتر وضعیت به‌صورت status می‌رود', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.click(screen.getByRole('button', { name: 'رها شده' }));
await waitFor(() => expect(get.mock.calls.some(
(c: unknown[]) => typeof c[0] === 'string' && c[0].includes('status=abandoned'),
)).toBe(true));
});
/** نتیجهٔ خالیِ جستجو با «هنوز پرونده‌ای ساخته نشده» یکی نیست. */
it('خالیِ جستجو پیام خودش را دارد', async () => {
mockList([]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText(/پرونده‌ای یافت نشد/);
fireEvent.change(screen.getByLabelText('جستجوی پرونده'), { target: { value: 'هیچ' } });
expect(await screen.findByText(/برای «هیچ» پرونده‌ای پیدا نشد/, {}, { timeout: 2000 })).toBeInTheDocument();
});
it('خطا را از فهرست خالی جدا می‌کند', async () => {
get.mockRejectedValue(new Error('boom'));
renderWithProviders(<TreatmentCasesPage />);
await waitFor(() => expect(screen.getByText(/خواندن پرونده‌ها ناموفق بود/)).toBeInTheDocument());
expect(screen.queryByText(/پرونده‌ای یافت نشد/)).not.toBeInTheDocument();
});
it('بازهٔ تاریخ به‌صورت from و to می‌رود', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
// PersianDateInput تریگرش input نیست، پس با نام دسترس‌پذیرش پیدایش می‌کنیم.
fireEvent.click(screen.getByLabelText('شروع از تاریخ'));
expect(screen.getByLabelText('شروع تا تاریخ')).toBeInTheDocument();
});
it('تاریخ شروع ساعت هم دارد', async () => {
mockList([caseRow({ opened_at: 1_786_000_000 })]);
renderWithProviders(<TreatmentCasesPage />);
// formatDateTime ساعت را با «:» می‌آورد؛ formatDate نمی‌آورد.
const start = await screen.findByText(/^شروع:/);
expect(start.textContent).toMatch(/:/);
});
/** «چه کسی انجامش داد» سؤالِ اولِ مدیر است وقتی پرونده را در فهرست می‌بیند. */
it('نام پرسنل انجام‌دهنده را روی کارت می‌آورد', async () => {
mockList([caseRow({ performed_by: [{ uuid: 'st-1', name: 'پرسنل۱' }] })]);
renderWithProviders(<TreatmentCasesPage />);
expect(await screen.findByText(/انجام‌دهنده: پرسنل۱/)).toBeInTheDocument();
});
/** تا وقتی جلسه‌ای انجام نشده، برنامه را نشان می‌دهیم نه هیچ. */
it('اگر هنوز انجام نشده، اپراتورِ اختصاص‌یافته را نشان می‌دهد', async () => {
mockList([caseRow({ assigned_staff: [{ uuid: 'st-2', name: 'پرسنل۲' }] })]);
renderWithProviders(<TreatmentCasesPage />);
expect(await screen.findByText(/اپراتور: پرسنل۲/)).toBeInTheDocument();
});
it('دکمهٔ ویرایش مودال را باز می‌کند', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
fireEvent.click(await screen.findByRole('button', { name: /ویرایش/ }));
expect(await screen.findByText('ویرایش پروندهٔ درمان')).toBeInTheDocument();
});
});