feat(appointment): implement service mode functionality with service slot selection and validation

This commit is contained in:
hamed
2026-07-16 00:02:36 +03:30
parent ac4a430564
commit 4715649fd8
5 changed files with 339 additions and 49 deletions
+65 -34
View File
@@ -10,6 +10,8 @@ import PersianDateInput from '../components/ui/PersianDateInput';
import PriceInput from '../components/ui/PriceInput';
import SearchableSelect from '../components/ui/SearchableSelect';
import { WalletChargeLink } from '../components/AppointmentActions';
import { useDoctorBookingServices } from '../hooks/useDoctorBookingServices';
import ServiceSlotPicker from '../components/appointments/ServiceSlotPicker';
/**
* افزودن نوبت — صفحهٔ کامل (بازسازی `CreateTurn.jsx` طرح tauri). از همان endpointهای
@@ -76,6 +78,11 @@ export default function AppointmentCreatePage() {
});
const staffQ = useQuery<ApiResponse<Option[]>>({ queryKey: ['staff-list'], queryFn: () => api.get('/api/v1/staff') });
// ── روش نوبت‌دهی پزشک (سرویسی/اسلاتی)
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 [date, setDate] = useState(params.get('date') || today);
const [duration, setDuration] = useState(40);
@@ -92,21 +99,28 @@ export default function AppointmentCreatePage() {
const effectiveName = picked?.user_name || name.trim();
const effectiveMobile = picked?.user_mobile || mobile.trim();
const effectiveNationalCode = (picked?.user_national_code || nationalCode).replace(/\D/g, '');
const timingValid = serviceMode
? (servicePick.serviceUuids.length > 0 && !!servicePick.slot)
: (!!start && !!end);
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10
&& effectiveNationalCode.length === 10 && !!start && !!end;
&& effectiveNationalCode.length === 10 && timingValid;
const create = useMutation({
mutationFn: async () => {
const createEndpoint = primaryRole === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/my/appointment';
const payload: Record<string, unknown> = {
doctor_uuid: doctorUuid,
slot_start: toEpoch(date, start),
slot_end: toEpoch(date, end),
slot_start: serviceMode ? servicePick.slot!.start : toEpoch(date, start),
slot_end: serviceMode ? servicePick.slot!.end : toEpoch(date, end),
patient_name: effectiveName,
patient_mobile: effectiveMobile,
patient_national_code: effectiveNationalCode,
...(sectionUuid ? { service_section_uuid: sectionUuid } : {}),
...(itemUuid ? { service_item_uuid: itemUuid } : {}),
...(serviceMode
? { service_item_uuids: servicePick.serviceUuids }
: {
...(sectionUuid ? { service_section_uuid: sectionUuid } : {}),
...(itemUuid ? { service_item_uuid: itemUuid } : {}),
}),
...(staffUuid ? { staff_uuid: staffUuid } : {}),
...(depositRequired ? { deposit_required: true, deposit_amount_rials: depositRials } : {}),
...(note.trim() ? { note: note.trim() } : {}),
@@ -210,22 +224,24 @@ export default function AppointmentCreatePage() {
{/* مشخصات سرویس */}
<div style={sectionTitle}>مشخصات سرویس:</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 10 }}>
<div>
<label style={label}>بخش</label>
<select aria-label="بخش" style={{ ...sel, marginTop: 6 }} value={sectionUuid} onChange={e => { setSectionUuid(e.target.value); setItemUuid(''); }}>
<option value="">انتخاب بخش</option>
{(sectionsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
{!serviceMode && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 10 }}>
<div>
<label style={label}>بخش</label>
<select aria-label="بخش" style={{ ...sel, marginTop: 6 }} value={sectionUuid} onChange={e => { setSectionUuid(e.target.value); setItemUuid(''); }}>
<option value="">انتخاب بخش</option>
{(sectionsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
</div>
<div>
<label style={label}>سرویس</label>
<select aria-label="سرویس" style={{ ...sel, marginTop: 6 }} value={itemUuid} onChange={e => setItemUuid(e.target.value)} disabled={!sectionUuid}>
<option value="">انتخاب سرویس</option>
{(itemsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
</div>
</div>
<div>
<label style={label}>سرویس</label>
<select aria-label="سرویس" style={{ ...sel, marginTop: 6 }} value={itemUuid} onChange={e => setItemUuid(e.target.value)} disabled={!sectionUuid}>
<option value="">انتخاب سرویس</option>
{(itemsQ.data?.data ?? []).map(o => <option key={o.uuid} value={o.uuid}>{o.name}</option>)}
</select>
</div>
</div>
)}
<label style={label}>انتخاب پرسنل</label>
<select aria-label="پرسنل" style={{ ...sel, margin: '6px 0 4px' }} value={staffUuid} onChange={e => setStaffUuid(e.target.value)}>
<option value="">انتخاب...</option>
@@ -236,22 +252,37 @@ export default function AppointmentCreatePage() {
<div style={sectionTitle}>زمان نوبت:</div>
<label style={label}>انتخاب تاریخ</label>
<div style={{ margin: '6px 0 10px' }}><PersianDateInput value={date} onChange={setDate} /></div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 12 }}>
<div>
<label style={label}>زمان پیش فرض (دقیقه)</label>
<div className="field" style={{ marginTop: 6 }}>
<input aria-label="زمان پیش فرض" type="number" min={5} value={duration} onChange={e => setDuration(Math.max(5, Number(e.target.value) || 0))} dir="ltr" />
{serviceMode ? (
<div style={{ marginBottom: 12 }}>
{doctorUuid ? (
<ServiceSlotPicker
doctorUuid={doctorUuid}
date={date}
services={services}
onSelect={setServicePick}
/>
) : (
<div style={{ fontSize: 12.5, color: 'var(--text-3)' }}>ابتدا پزشک را انتخاب کنید.</div>
)}
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10, marginBottom: 12 }}>
<div>
<label style={label}>زمان پیش فرض (دقیقه)</label>
<div className="field" style={{ marginTop: 6 }}>
<input aria-label="زمان پیش فرض" type="number" min={5} value={duration} onChange={e => setDuration(Math.max(5, Number(e.target.value) || 0))} dir="ltr" />
</div>
</div>
<div>
<label style={label}>ساعت شروع</label>
<div className="field" style={{ marginTop: 6 }}><input aria-label="ساعت شروع" type="time" value={start} onChange={e => setStart(e.target.value)} dir="ltr" /></div>
</div>
<div>
<label style={label}>ساعت پایان</label>
<div className="field" style={{ marginTop: 6 }}><input aria-label="ساعت پایان" type="time" value={end} onChange={e => setEnd(e.target.value)} dir="ltr" /></div>
</div>
</div>
<div>
<label style={label}>ساعت شروع</label>
<div className="field" style={{ marginTop: 6 }}><input aria-label="ساعت شروع" type="time" value={start} onChange={e => setStart(e.target.value)} dir="ltr" /></div>
</div>
<div>
<label style={label}>ساعت پایان</label>
<div className="field" style={{ marginTop: 6 }}><input aria-label="ساعت پایان" type="time" value={end} onChange={e => setEnd(e.target.value)} dir="ltr" /></div>
</div>
</div>
)}
{/* بیعانه */}
<div style={sectionTitle}>بیعانه:</div>