/opt/mawid/apps/dashboard/app/(app)
NameSizeModeActions
conversations/-0755rm
patients/-0755rm
settings/-0755rm
layout.tsx26710644editdlrm
page.tsx159060644editdlrm
Edit: /opt/mawid/apps/dashboard/app/(app)/page.tsx (15906B)
'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { api, serviceName, timeInTz } from '@/lib/api'; import { useLang } from '@/lib/i18n'; import { Badge, Button, Card, Field, Input, Modal, Select, Spinner } from '@/components/ui'; interface Clinic { id: string; timezone: string; } interface Appointment { id: string; startsAt: string; endsAt: string; status: 'pending' | 'confirmed' | 'cancelled' | 'completed' | 'no_show'; unconfirmedRisk: boolean; patient: { id: string; name: string | null; waPhone: string }; staff: { id: string; name: string }; service: { id: string; name: unknown }; } interface Slot { startsAtUtc: string; local: string; staffId: string; staffName: string; } interface Summary { appointments: number; cancellations: number; noShows: number; confirmedAfterReminder: number; refilledSlots: number; } interface ServiceRow { id: string; name: unknown; active: boolean; } interface StaffRow { id: string; name: string; services: { id: string }[]; } interface PatientRow { id: string; name: string | null; waPhone: string; } const STATUS_STYLES: Record = { pending: 'border-amber-300 bg-amber-50', confirmed: 'border-emerald-300 bg-emerald-50', cancelled: 'border-slate-200 bg-slate-50 opacity-60', completed: 'border-blue-300 bg-blue-50', no_show: 'border-red-300 bg-red-50', }; const STATUS_BADGE: Record = { pending: 'amber', confirmed: 'emerald', cancelled: 'slate', completed: 'blue', no_show: 'red', }; const DAY_MS = 24 * 3600_000; function startOfWeek(date: Date): Date { const d = new Date(date); d.setHours(0, 0, 0, 0); const day = (d.getDay() + 6) % 7; // Monday = 0 return new Date(d.getTime() - day * DAY_MS); } export default function CalendarPage() { const { t, lang } = useLang(); const [clinic, setClinic] = useState(null); const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date())); const [appointments, setAppointments] = useState(null); const [summary, setSummary] = useState(null); const [selected, setSelected] = useState(null); const [creating, setCreating] = useState(false); const weekEnd = useMemo(() => new Date(weekStart.getTime() + 7 * DAY_MS), [weekStart]); const load = useCallback(async () => { const [clinicData, appts] = await Promise.all([ api('/clinic'), api(`/appointments?from=${weekStart.toISOString()}&to=${weekEnd.toISOString()}`), ]); setClinic(clinicData); setAppointments(appts); }, [weekStart, weekEnd]); useEffect(() => { load().catch(() => setAppointments([])); }, [load]); useEffect(() => { api('/summary/weekly').then(setSummary).catch(() => {}); }, []); const days = useMemo( () => Array.from({ length: 7 }, (_, i) => new Date(weekStart.getTime() + i * DAY_MS)), [weekStart], ); const tz = clinic?.timezone ?? 'Europe/Istanbul'; function apptsForDay(day: Date): Appointment[] { const next = new Date(day.getTime() + DAY_MS); return (appointments ?? []).filter((a) => { const start = new Date(a.startsAt); return start >= day && start < next; }); } return (
{summary && (

{t('weeklySummary')}

)}
{weekStart.toLocaleDateString(lang === 'ar' ? 'ar' : lang, { day: 'numeric', month: 'short' })} {' – '} {new Date(weekEnd.getTime() - DAY_MS).toLocaleDateString(lang === 'ar' ? 'ar' : lang, { day: 'numeric', month: 'short', })}
{appointments === null ? ( ) : (
{days.map((day) => (
{day.toLocaleDateString(lang === 'ar' ? 'ar' : lang, { weekday: 'short', day: 'numeric' })}
{apptsForDay(day).map((appt) => ( ))}
))}
)} {selected && clinic && ( setSelected(null)} onChanged={() => { setSelected(null); load(); }} /> )} {creating && clinic && ( setCreating(false)} onCreated={() => { setCreating(false); load(); }} /> )}
); } function Stat({ label, value }: { label: string; value: number }) { return (
{value}
{label}
); } function AppointmentModal({ appointment, tz, onClose, onChanged, }: { appointment: Appointment; tz: string; onClose: () => void; onChanged: () => void; }) { const { t, lang } = useLang(); const [busy, setBusy] = useState(false); const [slots, setSlots] = useState(null); async function patch(body: object) { setBusy(true); try { await api(`/appointments/${appointment.id}`, { method: 'PATCH', body: JSON.stringify(body) }); onChanged(); } finally { setBusy(false); } } async function cancel() { setBusy(true); try { await api(`/appointments/${appointment.id}/cancel`, { method: 'POST', body: JSON.stringify({}) }); onChanged(); } finally { setBusy(false); } } async function loadSlots() { const from = new Date().toISOString().slice(0, 10); const to = new Date(Date.now() + 14 * DAY_MS).toISOString().slice(0, 10); const result = await api( `/appointments/availability?serviceId=${appointment.service.id}&staffId=${appointment.staff.id}&fromDate=${from}&toDate=${to}`, ); setSlots(result); } const isOpen = ['pending', 'confirmed'].includes(appointment.status); return (
{t(`st_${appointment.status}`)} {appointment.unconfirmedRisk && appointment.status === 'pending' && ( ⚠ {t('atRisk')} )}
{isOpen && (
{appointment.status === 'pending' && ( )}
)} {slots && (

{t('pickSlot')}

{slots.length === 0 &&

{t('noSlots')}

}
{slots.slice(0, 60).map((slot) => ( ))}
)}
); } function NewAppointmentModal({ onClose, onCreated }: { onClose: () => void; onCreated: () => void }) { const { t, lang } = useLang(); const [patientSearch, setPatientSearch] = useState(''); const [patients, setPatients] = useState([]); const [patientId, setPatientId] = useState(''); const [services, setServices] = useState([]); const [staff, setStaff] = useState([]); const [serviceId, setServiceId] = useState(''); const [staffId, setStaffId] = useState(''); const [slots, setSlots] = useState(null); const [busy, setBusy] = useState(false); useEffect(() => { api('/services').then((rows) => setServices(rows.filter((r) => r.active))); api('/staff').then(setStaff); }, []); useEffect(() => { const handle = setTimeout(() => { api(`/patients?search=${encodeURIComponent(patientSearch)}`).then(setPatients); }, 250); return () => clearTimeout(handle); }, [patientSearch]); const eligibleStaff = useMemo( () => staff.filter((s) => !serviceId || s.services.some((sv) => sv.id === serviceId)), [staff, serviceId], ); useEffect(() => { setSlots(null); if (!serviceId) return; const from = new Date().toISOString().slice(0, 10); const to = new Date(Date.now() + 14 * DAY_MS).toISOString().slice(0, 10); api( `/appointments/availability?serviceId=${serviceId}${staffId ? `&staffId=${staffId}` : ''}&fromDate=${from}&toDate=${to}`, ).then(setSlots); }, [serviceId, staffId]); async function create(slot: Slot) { setBusy(true); try { await api('/appointments', { method: 'POST', body: JSON.stringify({ patientId, serviceId, staffId: slot.staffId, startsAtUtc: slot.startsAtUtc, }), }); onCreated(); } finally { setBusy(false); } } return (
setPatientSearch(e.target.value)} />
{patients.map((p) => ( ))}
{serviceId && slots === null && } {slots && (

{t('pickSlot')}

{slots.length === 0 &&

{t('noSlots')}

}
{slots.slice(0, 60).map((slot) => ( ))}
)}
); } function Row({ label, value }: { label: string; value: string }) { return (
{label} {value}
); }