feat(appointment): implement service mode functionality with service slot selection and validation
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../../lib/api';
|
||||
import type { ApiResponse } from '../../lib/api';
|
||||
import type { BookingService } from '../../hooks/useDoctorBookingServices';
|
||||
|
||||
interface ServiceSlot { start: number; end: number; start_time: string }
|
||||
|
||||
/**
|
||||
* انتخاب سرویس (یک/چند) + زمانهای خالیِ کافیِ پیشنهادی برای نوبتدهی سرویسی.
|
||||
* مدت نوبت از مجموع مدت سرویسها میآید؛ زمانها از `appointment-service-slots`.
|
||||
* انتخاب را از طریق onSelect بالا میفرستد تا فرمِ میزبان payload بسازد.
|
||||
*/
|
||||
export default function ServiceSlotPicker({
|
||||
doctorUuid, date, services, onSelect,
|
||||
}: {
|
||||
doctorUuid: string;
|
||||
date: string;
|
||||
services: BookingService[];
|
||||
onSelect: (v: { serviceUuids: string[]; slot: ServiceSlot | null }) => void;
|
||||
}) {
|
||||
const [serviceUuids, setServiceUuids] = useState<string[]>([]);
|
||||
const [pickedSlot, setPickedSlot] = useState<ServiceSlot | null>(null);
|
||||
|
||||
useEffect(() => { setPickedSlot(null); }, [serviceUuids, date, doctorUuid]);
|
||||
useEffect(() => { onSelect({ serviceUuids, slot: pickedSlot }); }, [serviceUuids, pickedSlot]);
|
||||
|
||||
const slotsQ = useQuery<ApiResponse<any>>({
|
||||
queryKey: ['service-slots-picker', doctorUuid, date, serviceUuids],
|
||||
queryFn: () => api.get(
|
||||
`/api/v1/appointment-service-slots?doctor_uuid=${doctorUuid}&date=${date}`
|
||||
+ serviceUuids.map(u => `&service_item_uuids[]=${encodeURIComponent(u)}`).join('')
|
||||
),
|
||||
enabled: !!doctorUuid && !!date && serviceUuids.length > 0,
|
||||
});
|
||||
const startTimes: ServiceSlot[] = (slotsQ.data?.data as any)?.start_times ?? [];
|
||||
const totalMinutes = (slotsQ.data?.data as any)?.total_duration_minutes as number | undefined;
|
||||
|
||||
const label = { fontSize: 12.5, color: 'var(--text-3)', display: 'block' } as const;
|
||||
|
||||
const toggle = (uuid: string) =>
|
||||
setServiceUuids(prev => prev.includes(uuid) ? prev.filter(u => u !== uuid) : [...prev, uuid]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label style={label}>سرویس (یک یا چند)</label>
|
||||
{services.length === 0 ? (
|
||||
<div style={{ fontSize: 12.5, color: 'var(--danger)', margin: '6px 0' }}>
|
||||
سرویسی با «نمایش در نوبتدهی» برای این پزشک تعریف نشده است.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, margin: '6px 0 10px' }}>
|
||||
{services.map(s => {
|
||||
const active = serviceUuids.includes(s.uuid);
|
||||
return (
|
||||
<button
|
||||
key={s.uuid}
|
||||
type="button"
|
||||
onClick={() => toggle(s.uuid)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8,
|
||||
padding: '8px 10px', borderRadius: 'var(--r-sm)', cursor: 'pointer', textAlign: 'right',
|
||||
fontFamily: 'inherit', fontSize: 13,
|
||||
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
|
||||
background: active ? 'var(--primary-soft)' : 'var(--surface)',
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{
|
||||
width: 15, height: 15, borderRadius: 4, display: 'grid', placeItems: 'center', flexShrink: 0,
|
||||
border: active ? '1px solid var(--primary)' : '1px solid var(--border-2)',
|
||||
background: active ? 'var(--primary)' : 'transparent',
|
||||
}}>
|
||||
{active && <span style={{ width: 7, height: 7, background: '#fff', borderRadius: 2 }} />}
|
||||
</span>
|
||||
{s.name}
|
||||
</span>
|
||||
{s.duration_minutes ? <span style={{ color: 'var(--text-3)', fontSize: 12 }}>{s.duration_minutes} دقیقه</span> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{serviceUuids.length > 0 && (
|
||||
<>
|
||||
<label style={label}>زمانهای خالی پیشنهادی{totalMinutes != null ? ` (مدت کل: ${totalMinutes} دقیقه)` : ''}</label>
|
||||
{slotsQ.isLoading ? (
|
||||
<div style={{ fontSize: 12.5, color: 'var(--text-3)', margin: '6px 0' }}>در حال محاسبه...</div>
|
||||
) : startTimes.length === 0 ? (
|
||||
<div style={{ fontSize: 12.5, color: 'var(--danger)', margin: '6px 0' }}>
|
||||
برای این سرویس در این روز زمان خالی کافی نیست؛ روز دیگری انتخاب کنید.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, margin: '6px 0 4px' }}>
|
||||
{startTimes.map(s => {
|
||||
const active = pickedSlot?.start === s.start;
|
||||
return (
|
||||
<button
|
||||
key={s.start}
|
||||
type="button"
|
||||
dir="ltr"
|
||||
onClick={() => setPickedSlot({ start: s.start, end: s.end, start_time: s.start_time })}
|
||||
style={{
|
||||
fontSize: 13, padding: '6px 12px', borderRadius: 'var(--r-sm)', cursor: 'pointer', fontFamily: 'inherit',
|
||||
border: active ? '1px solid var(--primary)' : '1px solid var(--border)',
|
||||
background: active ? 'var(--primary)' : 'var(--surface)', color: active ? '#fff' : 'var(--text)',
|
||||
}}
|
||||
>
|
||||
{s.start_time}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user