The public doctor payload built `active`/`free_turn`/`hours_of_work` from the personal schedule alone, so a doctor bookable only at a clinic was reported as "نوبتدهی غیرفعال". Aggregate over every schedule instead: any schedule with online booking on and an active day makes the doctor bookable, and the disabled label only appears when all of them are off. Three admin-panel fixes for the same class of bug: - AppointmentsPage took the selected doctor from `dbUuid`, which is the clinic's uuid inside a clinic context — the slots request 404'd. Use `doctorUuid`. - TurnsTimeline rendered any error or unknown empty_reason as "این روز شیفت کاری ندارد". Errors now surface as errors and unknown reasons get a neutral message; the day-off wording is reserved for an explicit day_off from the backend. - Admins have no clinic context, so slots fell back to the personal schedule. They now pick a location from `appointment-booking-locations` and that choice drives the slot, service and create-appointment requests. Adds `app:schedule:normalize-format` for legacy rows stored as a bare JSON list covering only Saturday, which read as day-off for the rest of the week. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
71 lines
3.8 KiB
TypeScript
71 lines
3.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { screen, fireEvent } from '@testing-library/react';
|
|
import { renderWithProviders } from '../../test/utils';
|
|
|
|
vi.mock('../../lib/api', () => ({
|
|
api: { get: vi.fn(() => Promise.resolve({ success: true, data: [] })), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
|
ApiError: class extends Error {},
|
|
}));
|
|
|
|
import TurnsTimeline from './TurnsTimeline';
|
|
import type { TimelineSlot } from './types';
|
|
import type { Appointment } from '../../types';
|
|
|
|
const appt = (over: Partial<Appointment> = {}): Appointment => ({
|
|
uuid: 'ap1', patient_name: 'ساغر صابری', patient_mobile: '09356619438',
|
|
doctor_uuid: 'doc1', doctor_name: 'دکتر محمدی',
|
|
slot_start: 1000, slot_end: 2000, appointment_date: '2024-12-31',
|
|
appointment_time: '08:00', end_time: '08:35', status: 'completed',
|
|
version: 1, created_at: '', service_item: { uuid: 's1', name: 'ویزیت عمومی' },
|
|
} as unknown as Appointment);
|
|
|
|
const occupiedSlot: TimelineSlot = {
|
|
start: 1000, end: 2000, start_time: '08:00', end_time: '08:35',
|
|
is_available: false, appointment: appt(), cancelled_appointment: null,
|
|
};
|
|
const emptySlot: TimelineSlot = {
|
|
start: Math.floor(Date.now() / 1000) + 3600, end: Math.floor(Date.now() / 1000) + 5400,
|
|
start_time: '09:10', end_time: '10:30', is_available: true, appointment: null, cancelled_appointment: null,
|
|
};
|
|
|
|
describe('TurnsTimeline', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('renders an occupied slot card with patient name + service', () => {
|
|
renderWithProviders(<TurnsTimeline slots={[occupiedSlot]} loading={false} queryKey={['x']} onView={vi.fn()} onBook={vi.fn()} />);
|
|
expect(screen.getByText('ساغر صابری')).toBeInTheDocument();
|
|
expect(screen.getByText(/ویزیت عمومی/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders an empty slot as «افزودن نوبت» and fires onBook on click', () => {
|
|
const onBook = vi.fn();
|
|
renderWithProviders(<TurnsTimeline slots={[emptySlot]} loading={false} queryKey={['x']} onView={vi.fn()} onBook={onBook} />);
|
|
const add = screen.getByText('افزودن نوبت سریع');
|
|
fireEvent.click(add);
|
|
expect(onBook).toHaveBeenCalledWith(emptySlot);
|
|
});
|
|
|
|
it('empty_reason=day_off → «این روز شیفت کاری ندارد»', () => {
|
|
renderWithProviders(<TurnsTimeline slots={[]} emptyReason="day_off" loading={false} queryKey={['x']} onView={vi.fn()} onBook={vi.fn()} />);
|
|
expect(screen.getByText('این روز شیفت کاری ندارد')).toBeInTheDocument();
|
|
});
|
|
|
|
it('empty_reason=holiday → «این روز تعطیل است»', () => {
|
|
renderWithProviders(<TurnsTimeline slots={[]} emptyReason="holiday" loading={false} queryKey={['x']} onView={vi.fn()} onBook={vi.fn()} />);
|
|
expect(screen.getByText('این روز تعطیل است')).toBeInTheDocument();
|
|
});
|
|
|
|
it('خطای API هرگز به «شیفت کاری ندارد» ترجمه نمیشود', () => {
|
|
renderWithProviders(<TurnsTimeline slots={[]} errorMessage="دکتر یافت نشد" loading={false} queryKey={['x']} onView={vi.fn()} onBook={vi.fn()} />);
|
|
expect(screen.getByText('خطا در دریافت برنامهٔ این روز')).toBeInTheDocument();
|
|
expect(screen.getByText('دکتر یافت نشد')).toBeInTheDocument();
|
|
expect(screen.queryByText('این روز شیفت کاری ندارد')).toBeNull();
|
|
});
|
|
|
|
it('دلیل ناشناخته/غایب → پیام خنثی، نه day_off', () => {
|
|
renderWithProviders(<TurnsTimeline slots={[]} loading={false} queryKey={['x']} onView={vi.fn()} onBook={vi.fn()} />);
|
|
expect(screen.getByText('برنامهٔ این روز در دسترس نیست')).toBeInTheDocument();
|
|
expect(screen.queryByText('این روز شیفت کاری ندارد')).toBeNull();
|
|
});
|
|
});
|