diff --git a/assets/admin/index.tsx b/assets/admin/index.tsx index 9c0039df..06ecf6cf 100644 --- a/assets/admin/index.tsx +++ b/assets/admin/index.tsx @@ -3,6 +3,12 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'sonner'; +import { + BellIcon, + CheckCircleIcon, + XCircleIcon, + ExclamationTriangleIcon, +} from '@heroicons/react/24/outline'; import App from './App'; import './styles.css'; @@ -12,13 +18,38 @@ const queryClient = new QueryClient({ }, }); +const toastStyle: React.CSSProperties = { + fontFamily: 'inherit', + fontSize: 14, + fontWeight: 600, + direction: 'rtl', + borderRadius: 14, + border: '1px solid var(--border, #e5e7eb)', + background: 'var(--surface, #fff)', + color: 'var(--text, #111)', + boxShadow: '0 4px 24px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,.04)', + padding: '14px 16px', + gap: 12, + minWidth: 260, +}; + const root = document.getElementById('admin-root')!; createRoot(root).render( - + , + error: , + warning: , + info: , + }} + /> diff --git a/assets/admin/pages/DoctorDetailPage.tsx b/assets/admin/pages/DoctorDetailPage.tsx index dc201cff..c475e1b6 100644 --- a/assets/admin/pages/DoctorDetailPage.tsx +++ b/assets/admin/pages/DoctorDetailPage.tsx @@ -66,7 +66,7 @@ interface AddressData { // ── Schedule types ───────────────────────────────────────────────────────── interface SlotConfig { start: string; end: string; duration: number; } -interface DateOverrideData { uuid: string; doctor_uuid: string; date: number; active: boolean; reason: string | null; custom_slots: SlotConfig[]; } +interface DateOverrideData { uuid: string; doctor_uuid: string; date: number; active: boolean; reason: string | null; custom_slots: SessionConfig[]; } interface HolidayData { uuid: string; doctor_uuid: string; start_date: number; end_date: number; active: boolean; reason: string | null; } interface SessionConfig { @@ -1329,15 +1329,27 @@ function WeeklyScheduleTab({ doctorUuid, addresses }: { doctorUuid: string; addr // ── Date Override Modal ──────────────────────────────────────────────────── -function DateOverrideModal({ open, onClose, existing, doctorUuid, onSaved }: { +function DateOverrideModal({ open, onClose, existing, doctorUuid, onSaved, addresses }: { open: boolean; onClose: () => void; existing: DateOverrideData | null; doctorUuid: string; - onSaved: () => void; + onSaved: () => void; addresses: AddressData[]; }) { const [dateStr, setDateStr] = useState(''); const [overrideType, setOverrideType] = useState<'closed' | 'custom'>('closed'); const [reason, setReason] = useState(''); - const [slots, setSlots] = useState([]); + const [slots, setSlots] = useState([]); + + const toSession = useCallback((s: any): SessionConfig => ({ + active: true, + start_time: s.start_time ?? s.start ?? '09:00', + end_time: s.end_time ?? s.end ?? '13:00', + duration_per_patient: s.duration_per_patient ?? s.duration ?? 20, + location_id: s.location_id ?? null, + has_rest: s.has_rest ?? false, + rest_interval: s.rest_interval ?? 60, + time_to_rest: s.time_to_rest ?? 10, + patient_limit: s.patient_limit ?? null, + }), []); useEffect(() => { if (open) { @@ -1345,13 +1357,13 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, onSaved }: { setDateStr(tsToDate(existing.date)); setOverrideType(existing.active ? 'custom' : 'closed'); setReason(existing.reason ?? ''); - setSlots(existing.custom_slots ?? []); + setSlots((existing.custom_slots ?? []).map(toSession)); } else { setDateStr(new Date().toISOString().slice(0, 10)); setOverrideType('closed'); setReason(''); setSlots([]); } } - }, [open, existing]); + }, [open, existing, toSession]); const saveMut = useMutation({ mutationFn: () => { @@ -1417,9 +1429,28 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, onSaved }: { {overrideType === 'custom' && (
- -

اگر خالی بماند از برنامه هفتگی استفاده می‌شود

- +
+ + +
+ {slots.length === 0 ? ( +

اگر خالی بماند از برنامه هفتگی استفاده می‌شود

+ ) : ( +
+ {slots.map((session, idx) => ( + setSlots(prev => prev.map((old, i) => i === idx ? s : old))} + onRemove={() => setSlots(prev => prev.filter((_, i) => i !== idx))} /> + ))} +
+ )}
)} @@ -1438,7 +1469,7 @@ function DateOverrideModal({ open, onClose, existing, doctorUuid, onSaved }: { // ── Date Overrides Tab ───────────────────────────────────────────────────── -function DateOverridesTab({ doctorUuid }: { doctorUuid: string }) { +function DateOverridesTab({ doctorUuid, addresses }: { doctorUuid: string; addresses: AddressData[] }) { const qc = useQueryClient(); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); @@ -1510,7 +1541,7 @@ function DateOverridesTab({ doctorUuid }: { doctorUuid: string }) { )} { setModalOpen(false); setEditing(null); }} - existing={editing} doctorUuid={doctorUuid} + existing={editing} doctorUuid={doctorUuid} addresses={addresses} onSaved={() => qc.invalidateQueries({ queryKey: ['doctor-overrides', doctorUuid] })} /> {tab === 'weekly' && } - {tab === 'overrides' && } + {tab === 'overrides' && } {tab === 'holidays' && } ); diff --git a/src/Appointment/Service/SlotCalculatorService.php b/src/Appointment/Service/SlotCalculatorService.php index bb136971..d975d100 100644 --- a/src/Appointment/Service/SlotCalculatorService.php +++ b/src/Appointment/Service/SlotCalculatorService.php @@ -127,27 +127,35 @@ class SlotCalculatorService } /** - * Build slots from flat date-override format: [{start, end, duration}] - * Kept for backward-compatibility with DateOverride.custom_slots. + * Build slots from date-override custom_slots. + * Supports new SessionConfig format {start_time, end_time, duration_per_patient, ...} + * and legacy flat format {start, end, duration} for backward compatibility. */ private function buildFlatSlots(array $slotConfigs, int $dayStart): array { $slots = []; foreach ($slotConfigs as $config) { - $startSec = $this->parseTime($config['start'] ?? '00:00'); - $endSec = $this->parseTime($config['end'] ?? '00:00'); - $duration = (int)($config['duration'] ?? 30) * 60; - - if ($duration <= 0 || $endSec <= $startSec) continue; - - for ($t = $startSec; $t + $duration <= $endSec; $t += $duration) { - $slots[] = [ - 'start' => $dayStart + $t, - 'end' => $dayStart + $t + $duration, - 'start_time' => gmdate('H:i', $t), - 'end_time' => gmdate('H:i', $t + $duration), - 'location_id' => null, - ]; + if (isset($config['start_time'])) { + // New SessionConfig format — reuse the same logic as weekly sessions + $slots = array_merge($slots, $this->buildSessionSlots( + array_merge(['active' => true], $config), + $dayStart + )); + } else { + // Legacy flat format: {start, end, duration} + $startSec = $this->parseTime($config['start'] ?? '00:00'); + $endSec = $this->parseTime($config['end'] ?? '00:00'); + $duration = (int)($config['duration'] ?? 30) * 60; + if ($duration <= 0 || $endSec <= $startSec) continue; + for ($t = $startSec; $t + $duration <= $endSec; $t += $duration) { + $slots[] = [ + 'start' => $dayStart + $t, + 'end' => $dayStart + $t + $duration, + 'start_time' => gmdate('H:i', $t), + 'end_time' => gmdate('H:i', $t + $duration), + 'location_id' => null, + ]; + } } } return $slots;