feat: section-based service picker + per-appointment duration override (service mode)
Service-booking mode now selects services by section like slot mode: appointment-booking-services returns service_section per item; ServiceSlotPicker groups by section (SearchableSelect), accumulates picks across sections into a removable 'section -> service' chip list. Secretaries can override a service's duration for a single appointment without changing the service default: appointment-service-slots accepts durations[uuid] and both create endpoints accept service_durations; the override drives total duration and slot_end. Online (patient) booking is unaffected — it never sends overrides. Backend + frontend tests and docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,7 +42,7 @@ describe('AppointmentCreatePage — افزودن نوبت', () => {
|
||||
it('service mode: picks service + suggested time and posts service_item_uuids', async () => {
|
||||
get.mockImplementation((url: string) => {
|
||||
if (url.startsWith('/api/v1/appointment-booking-services/'))
|
||||
return Promise.resolve({ success: true, data: { booking_mode: 'service', buffer_minutes: 5, services: [{ uuid: 'sv1', name: 'عصبکشی', duration_minutes: 30, price_rials: 500000 }] } });
|
||||
return Promise.resolve({ success: true, data: { booking_mode: 'service', buffer_minutes: 5, services: [{ uuid: 'sv1', name: 'عصبکشی', duration_minutes: 30, price_rials: 500000, service_section: { uuid: 'sec1', name: 'دندان' } }] } });
|
||||
if (url.startsWith('/api/v1/appointment-service-slots'))
|
||||
return Promise.resolve({ success: true, data: { total_duration_minutes: 30, buffer_minutes: 5, start_times: [{ start: 1754000000, end: 1754001800, start_time: '15:00' }] } });
|
||||
return Promise.resolve({ success: true, data: [] });
|
||||
@@ -58,6 +58,11 @@ describe('AppointmentCreatePage — افزودن نوبت', () => {
|
||||
// حالت سرویس: ورودی ساعت شروع نباید باشد
|
||||
await waitFor(() => expect(screen.queryByLabelText('ساعت شروع')).toBeNull());
|
||||
|
||||
// حالت سرویسی هم «بخش → سرویس» است: ابتدا بخش، سپس سرویس
|
||||
const secInput = document.getElementById('service-mode-section-select') as HTMLInputElement;
|
||||
fireEvent.focus(secInput);
|
||||
fireEvent.keyDown(secInput, { key: 'ArrowDown' });
|
||||
fireEvent.click(await screen.findByText('دندان'));
|
||||
fireEvent.click(await screen.findByText('عصبکشی'));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '15:00' }));
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function AppointmentCreatePage() {
|
||||
// ── روش نوبتدهی پزشک (سرویسی/اسلاتی)
|
||||
const { bookingMode, services } = useDoctorBookingServices(doctorUuid);
|
||||
const serviceMode = bookingMode === 'service';
|
||||
const [servicePick, setServicePick] = useState<{ serviceUuids: string[]; slot: { start: number; end: number } | null }>({ serviceUuids: [], slot: null });
|
||||
const [servicePick, setServicePick] = useState<{ serviceUuids: string[]; durations: Record<string, number>; slot: { start: number; end: number } | null }>({ serviceUuids: [], durations: {}, slot: null });
|
||||
|
||||
// ── زمان نوبت
|
||||
const [date, setDate] = useState(params.get('date') || today);
|
||||
@@ -124,7 +124,7 @@ export default function AppointmentCreatePage() {
|
||||
patient_mobile: effectiveMobile,
|
||||
patient_national_code: effectiveNationalCode,
|
||||
...(serviceMode
|
||||
? { service_item_uuids: servicePick.serviceUuids, duration_from_services: true }
|
||||
? { service_item_uuids: servicePick.serviceUuids, duration_from_services: true, service_durations: servicePick.durations }
|
||||
: {
|
||||
...(sectionUuid ? { service_section_uuid: sectionUuid } : {}),
|
||||
...(selectedServices.length ? { service_item_uuids: selectedServices.map(s => s.uuid) } : {}),
|
||||
|
||||
Reference in New Issue
Block a user