feat: Implement resource-based appointment booking flow

- Updated DoctorPage component to accept bookingResources prop for appointment list.
- Added serviceQuery function to serialize service item UUIDs for API requests.
- Introduced new API endpoints for fetching booking resources and resource slots.
- Enhanced tests for new resource-based booking functionality, including resource selection and service availability.
- Created ResourceSelect component for selecting appointment types, including doctor and resource options.
- Updated appointment submission logic to include resource_uuid in payload when applicable.
- Ensured UI reflects changes in booking flow without disrupting existing doctor-centric experience.
This commit is contained in:
hamed
2026-08-09 10:45:52 +03:30
parent 1668ef8890
commit 54bebbd41a
23 changed files with 1094 additions and 32 deletions
@@ -0,0 +1,64 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ResourceSelect from './index';
const resource = (over = {}) => ({
uuid: 'r1',
name: 'کندلا2021',
type: { code: 'laser_device', name: 'دستگاه لیزر' },
services: [{ uuid: 's1', name: 'لیزر دست', duration_minutes: 20, price_rials: 2000000 }],
...over,
});
describe('ResourceSelect', () => {
it('کارت «نوبت با پزشک» همیشه اول می‌آید', () => {
render(<ResourceSelect resources={[resource()]} onSelect={vi.fn()} />);
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(2);
expect(buttons[0]).toHaveTextContent('نوبت با پزشک');
expect(buttons[1]).toHaveTextContent('کندلا2021');
});
it('نوع منبع به‌عنوان برچسب دیده می‌شود', () => {
render(<ResourceSelect resources={[resource()]} onSelect={vi.fn()} />);
expect(screen.getByText('دستگاه لیزر')).toBeInTheDocument();
});
it('انتخاب «نوبت با پزشک» مقدار null می‌فرستد', async () => {
const onSelect = vi.fn();
render(<ResourceSelect resources={[resource()]} onSelect={onSelect} />);
await userEvent.click(screen.getByText('نوبت با پزشک'));
expect(onSelect).toHaveBeenCalledWith(null);
});
it('انتخاب منبع، خودِ آبجکت منبع را می‌فرستد', async () => {
const onSelect = vi.fn();
const r = resource();
render(<ResourceSelect resources={[r]} onSelect={onSelect} />);
await userEvent.click(screen.getByText('کندلا2021'));
expect(onSelect).toHaveBeenCalledWith(r);
});
it('تا سه سرویس با نام می‌آید و بقیه با +n', () => {
const services = ['الف', 'ب', 'ج', 'د', 'ه'].map((n, i) => ({ uuid: `s${i}`, name: n }));
render(<ResourceSelect resources={[resource({ services })]} onSelect={vi.fn()} />);
expect(screen.getByText('الف، ب، ج +2')).toBeInTheDocument();
});
it('منبعِ بدون سرویس، زیرنویس خالی نمی‌سازد', () => {
render(<ResourceSelect resources={[resource({ services: [] })]} onSelect={vi.fn()} />);
// فقط زیرنویسِ کارتِ پزشک می‌ماند.
expect(screen.getAllByRole('button')[1].textContent).toBe('کندلا2021دستگاه لیزر');
});
it('منبعِ بدون type برچسب نمی‌گیرد و نمی‌شکند', () => {
render(<ResourceSelect resources={[resource({ type: null })]} onSelect={vi.fn()} />);
expect(screen.getByText('کندلا2021')).toBeInTheDocument();
});
});