Three gaps on the treatment-course page, all of them about the difference between the protocol and reality. The sessions table listed each date but not the gap between them, leaving the operator to subtract two Jalali dates in their head. It now shows the real gap and colours it as a warning past the protocol maximum. A course cancelled mid-way stretches silently: the session goes back to planned and nobody is told. The suggestion endpoint does warn, but only once a branch is picked, so the warning could go unseen indefinitely. The page now derives "N days since the last session, past the protocol maximum" from the course itself, so it shows immediately. The course's preferred resource was applied by the engine but never named in the UI. The API now returns preferred_resource_name alongside the uuid, and the text says plainly that it is a preference — the engine moves it up the list, it does not hold the slot. Two backend tests that were owed: the stricter of the protocol spacing and a spacing policy wins (protocol 7 days, policy 21, effective 21 — otherwise a clinic's safety rule could be bypassed by writing a short protocol), and a session whose earliest possible date falls outside the 90-day horizon is skipped rather than failing book-all, leaving the course untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { screen, 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 {},
|
|
}));
|
|
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
|
|
vi.mock('react-router-dom', async () => ({
|
|
...(await vi.importActual<typeof import('react-router-dom')>('react-router-dom')),
|
|
useParams: () => ({ courseUuid: 'c-1' }),
|
|
}));
|
|
|
|
import { api } from '../lib/api';
|
|
import TreatmentCoursePage from './TreatmentCoursePage';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
|
|
const DAY = 86400;
|
|
const now = () => Math.floor(Date.now() / 1000);
|
|
|
|
function course(over: Record<string, unknown> = {}) {
|
|
return {
|
|
uuid: 'c-1',
|
|
service_name: 'لیزر',
|
|
status: 'active',
|
|
min_days: 20,
|
|
ideal_days: 28,
|
|
max_days: 40,
|
|
abandon_reason: null,
|
|
preferred_resource_uuid: null,
|
|
preferred_resource_name: null,
|
|
progress: { completed: 1, booked: 0, planned: 1, total: 2 },
|
|
sessions: [
|
|
{ session_number: 1, status: 'completed', slot_start: now() - 60 * DAY, completed_at: now() - 60 * DAY, params: {} },
|
|
{ session_number: 2, status: 'planned', slot_start: null, completed_at: null, params: {} },
|
|
],
|
|
...over,
|
|
};
|
|
}
|
|
|
|
function mockCourse(payload: Record<string, unknown>) {
|
|
get.mockImplementation((url: string) => {
|
|
if (url.includes('next-slot-suggestion')) return Promise.resolve({ data: null });
|
|
if (url.includes('/branch')) return Promise.resolve({ data: [] });
|
|
return Promise.resolve({ data: payload });
|
|
});
|
|
}
|
|
|
|
describe('TreatmentCoursePage', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
/** ⭐ دورهای که وسطش لغو شده بیصدا کِش میآید؛ هشدار نباید به انتخاب شعبه وابسته باشد. */
|
|
it('warns when the course has run past the protocol maximum', async () => {
|
|
mockCourse(course());
|
|
|
|
renderWithProviders(<TreatmentCoursePage />);
|
|
|
|
expect(await screen.findByText(/روز از آخرین جلسه گذشته/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('stays quiet while the course is still inside its window', async () => {
|
|
mockCourse(course({
|
|
sessions: [
|
|
{ session_number: 1, status: 'completed', slot_start: now() - 5 * DAY, completed_at: now() - 5 * DAY, params: {} },
|
|
{ session_number: 2, status: 'planned', slot_start: null, completed_at: null, params: {} },
|
|
],
|
|
}));
|
|
|
|
renderWithProviders(<TreatmentCoursePage />);
|
|
|
|
await waitFor(() => expect(get).toHaveBeenCalled());
|
|
expect(screen.queryByText(/روز از آخرین جلسه گذشته/)).toBeNull();
|
|
});
|
|
|
|
/** فاصلهٔ واقعی، نه فاصلهٔ پروتکل — تفاوتشان همان چیزی است که کِشآمدن را نشان میدهد. */
|
|
it('shows the real gap between two dated sessions', async () => {
|
|
mockCourse(course({
|
|
sessions: [
|
|
{ session_number: 1, status: 'completed', slot_start: now() - 40 * DAY, completed_at: now() - 40 * DAY, params: {} },
|
|
{ session_number: 2, status: 'completed', slot_start: now() - 10 * DAY, completed_at: now() - 10 * DAY, params: {} },
|
|
],
|
|
progress: { completed: 2, booked: 0, planned: 0, total: 2 },
|
|
}));
|
|
|
|
renderWithProviders(<TreatmentCoursePage />);
|
|
|
|
expect(await screen.findByText('۳۰ روز')).toBeInTheDocument();
|
|
});
|
|
});
|