Files
clinicpro/assets/admin/components/appointments/TurnsTimeline.test.tsx
T
hamedandClaude Opus 4.8 a6ae6220cb feat: redesign appointments (نوبت‌ها) admin UI to match tauri turns + expandable sidebar
Rebuild the /admin/appointments page visual layer to match the tauri
clinic-pro-tauri "turns" design while keeping all existing data wiring and
backend endpoints unchanged (add/edit/move/transfer-reserve/replace already
supported via PATCH /api/v1/appointment/{uuid} and POST /api/v1/my/appointment).

Frontend (assets/admin):
- Sidebar: نوبت‌ها becomes an expandable parent with sub-items
  «نوبت های تایید شده» (/admin/appointments) and «افزودن نوبت»
  (/admin/appointments/new); auto-expands on active child. Applied to
  admin/clinic/doctor/secretary roles. Adds nav-subitem styling.
- New presentational components under components/appointments/: tauri status
  palette (turnStatus), TurnsStatInfo, TurnsViewToggle (sliding), DoctorTabs
  (underline), TurnsTimeline (marker rail + status cards, empty slot → افزودن
  نوبت), TurnsTable.
- AppointmentsPage recomposed with the new components (stats bar, doctor tabs,
  view toggle, timeline/table), preserving queries, filters, pagination,
  quick-book modal and the row actions menu.
- AppointmentCreatePage: full-page create form (CreateTurn layout) at
  /admin/appointments/new, reusing POST /api/v1/my|admin/appointment.

Tests: TurnsStatInfo, TurnsTimeline, Sidebar (expandable), AppointmentsPage,
AppointmentCreatePage. Backend move/reserve/replace verified green via existing
tests/Appointment/AppointmentUpdateTest + AppointmentWorkflowFieldsTest.

No API endpoints changed → no docs/api change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:33:03 +03:30

53 lines
2.5 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('shows the holiday/empty message when there are no slots', () => {
renderWithProviders(<TurnsTimeline slots={[]} loading={false} queryKey={['x']} onView={vi.fn()} onBook={vi.fn()} />);
expect(screen.getByText('این روز تعطیل است')).toBeInTheDocument();
});
});