1. شارژ کیف پول is now functional end-to-end. New owner-gated
POST /api/v1/patient/{uuid}/wallet/charge creates a manual credit
WalletTransaction (computed balance_after); the patient detail's wallet tab
gains a top-up modal (PriceInput + description) and supports ?tab= deep
links. The deposit sections of the create drawer, the edit page and the
replace modal link to it via WalletChargeLink (record resolved by mobile).
2. جایگزینی نوبت now matches appointments-replace.pdf: patient search-or-new,
بخش/سرویس/پرسنل selects prefilled from the appointment, deposit toggle +
amount + charge link, read-only original date/time, status pick and notes —
all through the general PATCH.
3. The confirmed-appointments table is paginated (20/page, client-side so the
schedule view and doctor-tab derivation keep the whole day), resetting on
date/doctor/filter changes. The page-local STATUS_META also adopts the
design labels plus following_up/salon for the schedule cards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
125 lines
6.4 KiB
TypeScript
125 lines
6.4 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 and keeps the slot locked', async () => {
|
|
openMenu();
|
|
fireEvent.click(screen.getByText('جایگزینی نوبت'));
|
|
expect(await screen.findByPlaceholderText('نام و نام خانوادگی')).toBeInTheDocument();
|
|
// the original slot is shown read-only
|
|
expect(screen.getByDisplayValue('2024-12-31')).toBeDisabled();
|
|
expect(screen.getByDisplayValue('09:00')).toBeDisabled();
|
|
// prefilled from the appointment's current specs
|
|
expect((screen.getByLabelText('وضعیت') as HTMLSelectElement).value).toBe('confirmed');
|
|
|
|
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',
|
|
service_section_uuid: 's1', service_item_uuid: 'i1', staff_uuid: 'st1',
|
|
version: 3,
|
|
})));
|
|
});
|
|
|
|
it('replace modal picks an existing patient from the record search', async () => {
|
|
get.mockImplementation((url: string) => {
|
|
if (url.startsWith('/api/v1/patient?search=')) return Promise.resolve({ success: true, data: [
|
|
{ uuid: 'rec9', user_name: 'پریسا همتی', user_mobile: '09120009999' },
|
|
] });
|
|
return Promise.resolve({ success: true, data: [] });
|
|
});
|
|
openMenu();
|
|
fireEvent.click(screen.getByText('جایگزینی نوبت'));
|
|
fireEvent.change(await screen.findByPlaceholderText('جستجوی نام، شماره تماس، شماره پرونده...'), { target: { value: 'پریسا' } });
|
|
fireEvent.click(await screen.findByText('پریسا همتی'));
|
|
fireEvent.click(screen.getByText('ثبت نوبت'));
|
|
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
|
|
patient_name: 'پریسا همتی', patient_mobile: '09120009999',
|
|
})));
|
|
});
|
|
});
|