The list had no way to tell two cases apart. TreatmentCase::toArray() carried
no patient, so four cases of the same service rendered as four identical
cards — same service, same supervisor, same date, same areas. Search would have
been meaningless without fixing that first, so the payload now carries the
patient (name, mobile, record number) and the card leads with the name.
Search: `?q=` on the list endpoint, matching patient name, mobile, national
code, record number and service name — the same keys a secretary already types
into the booking form. It lives in the URL via useUrlState, debounced, so back
and refresh keep the view.
Edit: PATCH /api/v1/treatment-case/{uuid} covering status, supervising doctor,
areas and session count, driven from a modal on the list. Rules live in
TreatmentCaseEditor, not the controller, around one boundary: no edit may
overwrite work already done. An area with session records cannot be removed, and
the session count cannot drop below the sessions that are booked or finished —
both 409, both tested. Reopening a closed case clears closed_at.
`areas[]` now also exposes `category_uuid`; the edit form selects catalog
categories, while `uuid` identifies the snapshot row.
Page fixes from the redesign checklist: the status filter was a hand-rolled
primary/secondary button pair, now `.seg` with `.on`; the raw `<progress>` bar
took the browser's own appearance and ignored the theme tokens, now a token-
styled bar with an explicit progressbar role; session counts go through
formatNumber; a failed request rendered as "no cases found", which reads as an
empty clinic rather than a broken one, and an empty search now says so in its
own words.
Adds the test files neither the page nor the case editor had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.2 KiB
TypeScript
114 lines
4.2 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' }],
|
|
...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('دکمهٔ ویرایش مودال را باز میکند', async () => {
|
|
mockList([caseRow()]);
|
|
|
|
renderWithProviders(<TreatmentCasesPage />);
|
|
fireEvent.click(await screen.findByRole('button', { name: /ویرایش/ }));
|
|
|
|
expect(await screen.findByText('ویرایش پروندهٔ درمان')).toBeInTheDocument();
|
|
});
|
|
});
|