Files
clinicpro/assets/admin/components/AppointmentActions.test.tsx
T
hamedandClaude Fable 5 7354c92e40 feat(appointments): row actions menu + info/move/transfer/replace modals — phase B
Frontend for the Figma عملیات menu on the confirmed-appointments table:

- AppointmentActions composite: six-item row menu (ویرایش، ثبت سرویس، مشاهده،
  جا به جایی، انتقال به لیست رزرو، جایگزینی) plus the four modals it opens.
  ثبت سرویس and the info modal resolve the patient record via the patient-list
  search (mobile) to reuse the existing wallet endpoint and NewSessionPage.
- Info modal mirrors appointments-info.pdf: start time, duration, phone,
  بخش/سرویس/پرسنل, wallet balance, status dropdown, مشاهده پرونده.
- Move/transfer/replace modals PATCH the new general update endpoint with
  optimistic-lock version; transfer uses day-level midnight slots.
- Status labels/transitions updated to the design set (ثبت شده/قطعی شده/
  در حال پیگیری/سالن/ویزیت شده/لغو شده) in AppointmentStatusDropdown and
  StatusBadge; Appointment type gains the new workflow fields; the table gains
  سرویس/پرسنل/عملیات columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 23:47:54 +03:30

100 lines
5.1 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 AppointmentActionsMenu from './AppointmentActions';
import type { Appointment } from '../types';
const get = api.get as ReturnType<typeof vi.fn>;
const patch = api.patch as ReturnType<typeof vi.fn>;
const appt: Appointment = {
uuid: 'ap1', patient_name: 'مریم خلیلی', patient_mobile: '09136549874',
doctor_uuid: 'd1', doctor_name: 'دکتر احمدی',
slot_start: 1735639200, slot_end: 1735641900, // 45 min
appointment_date: '2024-12-31', appointment_time: '09:00', end_time: '09:45',
status: 'confirmed', version: 3, created_at: '',
service_section: { uuid: 's1', name: 'زیبایی' },
service_item: { uuid: 'i1', name: 'لیزر توتال' },
staff: { uuid: 'st1', full_name: 'دکتر حمیدی' },
};
beforeEach(() => {
get.mockReset(); patch.mockReset();
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patient?search=')) return Promise.resolve({ success: true, data: [{ uuid: 'rec1' }] });
if (url === '/api/v1/patient/rec1/wallet') return Promise.resolve({ success: true, data: { balance_rials: 500000, recent_transactions: [] } });
return Promise.resolve({ success: true, data: [] });
});
patch.mockResolvedValue({ success: true, data: {} });
});
function openMenu() {
renderWithProviders(<AppointmentActionsMenu appointment={appt} queryKey={['appts']} />);
fireEvent.click(screen.getByRole('button', { name: 'عملیات' }));
}
describe('AppointmentActionsMenu (عملیات نوبت)', () => {
it('lists all six actions from the Figma menu', () => {
openMenu();
for (const label of ['ویرایش', 'ثبت سرویس', 'مشاهده', 'جا به جایی نوبت', 'انتقال به لیست رزرو', 'جایگزینی نوبت']) {
expect(screen.getByText(label)).toBeInTheDocument();
}
});
it('info modal shows appointment details and patient wallet balance', async () => {
openMenu();
fireEvent.click(screen.getByText('مشاهده'));
expect(await screen.findByText('ساعت شروع:')).toBeInTheDocument();
expect(screen.getByText('۴۵ دقیقه')).toBeInTheDocument();
expect(screen.getByText('لیزر توتال')).toBeInTheDocument();
expect(screen.getByText('دکتر حمیدی')).toBeInTheDocument();
// wallet resolved through record search → balance shown (rial → toman)
expect(await screen.findByText(/تومان/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'مشاهده پرونده' })).toBeInTheDocument();
});
it('move modal patches new slot times', async () => {
openMenu();
fireEvent.click(screen.getByText('جا به جایی نوبت'));
expect(await screen.findByText('اعمال تغییرات')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('ساعت شروع'), { target: { value: '15:00' } });
fireEvent.change(screen.getByLabelText('ساعت پایان'), { target: { value: '16:00' } });
fireEvent.click(screen.getByText('اعمال تغییرات'));
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
slot_start: Math.floor(new Date('2024-12-31T15:00').getTime() / 1000),
slot_end: Math.floor(new Date('2024-12-31T16:00').getTime() / 1000),
version: 3,
})));
});
it('transfer modal flips is_reserve with a day-level slot', async () => {
openMenu();
fireEvent.click(screen.getByText('انتقال به لیست رزرو'));
expect(await screen.findByText(/به لیست نوبت های رزرو شده منتقل می شود/)).toBeInTheDocument();
fireEvent.click(screen.getByText('انتقال و حذف از لیست'));
const day = Math.floor(new Date('2024-12-31T00:00').getTime() / 1000);
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
is_reserve: true, slot_start: day, slot_end: day, version: 3,
})));
});
it('replace modal swaps the patient on the slot', async () => {
openMenu();
fireEvent.click(screen.getByText('جایگزینی نوبت'));
expect(await screen.findByPlaceholderText('نام و نام خانوادگی مراجعه کننده')).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText('نام و نام خانوادگی مراجعه کننده'), { target: { value: 'ساغر صابری' } });
fireEvent.change(screen.getByPlaceholderText('شماره تماس'), { target: { value: '09356619438' } });
fireEvent.click(screen.getByText('ثبت نوبت'));
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
patient_name: 'ساغر صابری', patient_mobile: '09356619438', version: 3,
})));
});
});