Tasks 07 through 13 each changed something the rest of the system might want to know about, with no contract for saying so. And task 05 shipped a powerful segment editor with no feedback on whether a clinic defined its segments right. Events - A closed list of names, because a consumer branches on the string and a one-letter typo would produce an event nobody hears and no error either - Payloads carry uuids and scalars only; non-scalars are dropped, not serialised, so a consumer always fetches fresh rather than reading a stale detached entity - record() deliberately does not flush: the event row commits with the change it describes, so a rolled-back transaction leaves no event behind. A test pins exactly that - app:events:publish drains the outbox; five failed attempts park a row with its error rather than deleting it, because a silently dropped event is a loss with no trace. app:events:prune only ever removes published rows Reports - Resource utilisation separates available, occupied and active minutes. The gap between occupied and active is what exposes a bad segment definition, and available is multiplied by capacity so a three-chair room does not read as permanently over 100% - A resource with no calendar reports utilization: null, not zero — dividing by zero means something different from being idle - Plan accuracy compares planned against actual duration per service and flags both directions: running short wastes capacity that could have been sold. Its row links straight to editing that service's segments, because a report with no route to a fix does not get read Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } 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() } }));
|
|
|
|
import { api } from '../lib/api';
|
|
import PlanAccuracyPage from './PlanAccuracyPage';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
|
|
const rows = [
|
|
{
|
|
service_uuid: 's1',
|
|
service_name: 'لیزر فولبادی',
|
|
sample_size: 4,
|
|
planned_minutes: 60,
|
|
actual_minutes: 90,
|
|
deviation_percent: 50,
|
|
severity: 'high' as const,
|
|
},
|
|
{
|
|
service_uuid: 's2',
|
|
service_name: 'مشاوره',
|
|
sample_size: 5,
|
|
planned_minutes: 30,
|
|
actual_minutes: 29,
|
|
deviation_percent: -3,
|
|
severity: 'none' as const,
|
|
},
|
|
];
|
|
|
|
describe('PlanAccuracyPage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
get.mockResolvedValue({ success: true, data: { from: 1, to: 2, rows } });
|
|
});
|
|
|
|
it('shows planned against actual with a signed deviation', async () => {
|
|
renderWithProviders(<PlanAccuracyPage />, { route: '/admin/reports/plan-accuracy' });
|
|
|
|
await waitFor(() => expect(screen.getByText('لیزر فولبادی')).toBeInTheDocument());
|
|
|
|
expect(screen.getByText('+50٪')).toBeInTheDocument();
|
|
expect(screen.getByText('-3٪')).toBeInTheDocument();
|
|
expect(screen.getByText('زیاد')).toBeInTheDocument();
|
|
expect(screen.getByText('دقیق')).toBeInTheDocument();
|
|
});
|
|
|
|
/** نمونهٔ کوچک از گزارش حذف میشود؛ صفحه باید همان قاعده را بگوید. */
|
|
it('explains that small samples are excluded', async () => {
|
|
renderWithProviders(<PlanAccuracyPage />, { route: '/admin/reports/plan-accuracy' });
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText(/کمتر از سه نوبت انجامشده در گزارش نمیآیند/)).toBeInTheDocument(),
|
|
);
|
|
});
|
|
});
|