From 803564802c331972c69a4f3f5909f7e9893ad405 Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 8 May 2026 14:25:44 +0330 Subject: [PATCH] improvement: datepicker component now let user choose past dates too. --- .../src/app/(dashboard)/appointments/page.tsx | 55 ++++++++++-- .../appointments/AppointmentBookingModal.tsx | 84 ++++++++++++------- .../appointments/AppointmentScheduleGrid.tsx | 10 ++- .../ui/common/ScheduleDayPicker.tsx | 13 ++- frontend/src/lib/api/appointments.ts | 10 +++ 5 files changed, 127 insertions(+), 45 deletions(-) diff --git a/frontend/src/app/(dashboard)/appointments/page.tsx b/frontend/src/app/(dashboard)/appointments/page.tsx index 3561ff0..1f6a85b 100644 --- a/frontend/src/app/(dashboard)/appointments/page.tsx +++ b/frontend/src/app/(dashboard)/appointments/page.tsx @@ -17,7 +17,7 @@ import { ScheduleDayPicker } from '@/components/ui/common/ScheduleDayPicker'; import { Toast } from '@/components/ui/common/Toast'; import type { AppointmentPurpose } from '@/types/appointment'; import { formatApiErrorMessage } from '@/lib/formatApiError'; -import { getLocalDayIsoRange, startOfLocalDay } from '@/lib/appointmentTime'; +import { compareLocalDayStart, getLocalDayIsoRange, startOfLocalDay } from '@/lib/appointmentTime'; const EMPTY_PATIENT_FORM: CreatePatientInput = { firstName: '', @@ -48,6 +48,7 @@ export default function AppointmentsPage() { const [bookingHour, setBookingHour] = useState(9); const [bookingProviderId, setBookingProviderId] = useState(null); const [bookingProviderName, setBookingProviderName] = useState(''); + const [editingAppointmentId, setEditingAppointmentId] = useState(null); const [savingAppointment, setSavingAppointment] = useState(false); const [toastError, setToastError] = useState(''); @@ -58,6 +59,14 @@ export default function AppointmentsPage() { const canEditPatients = hasPermission(currentOrganization, 'TAB_PATIENTS_EDIT'); const todayStart = useMemo(() => startOfLocalDay(new Date()), []); + const isViewingPastDay = useMemo( + () => compareLocalDayStart(scheduleDate, todayStart) < 0, + [scheduleDate, todayStart], + ); + const activeEditingAppointment = useMemo( + () => appointments.find((a) => a.id === editingAppointmentId) ?? null, + [appointments, editingAppointmentId], + ); const scheduleLoadGen = useRef(0); @@ -155,6 +164,12 @@ export default function AppointmentsPage() { } function handleSlotClick(hour: number, providerUserId: string, providerName: string) { + if (isViewingPastDay) { + setToastSuccess(''); + setToastError(''); + setToastInfo('Past appointments are view-only.'); + return; + } if (!selectedPatient) { setToastSuccess(''); setToastError(''); @@ -164,6 +179,22 @@ export default function AppointmentsPage() { setBookingHour(hour); setBookingProviderId(providerUserId); setBookingProviderName(providerName); + setEditingAppointmentId(null); + setBookingOpen(true); + } + + function handleAppointmentClick(appointment: AppointmentRecord) { + if (isViewingPastDay) { + setToastSuccess(''); + setToastError(''); + setToastInfo('Past appointments are view-only.'); + return; + } + const provider = providers.find((p) => p.userId === appointment.providerUserId); + setBookingHour(new Date(appointment.startAt).getHours()); + setBookingProviderId(appointment.providerUserId); + setBookingProviderName(provider?.name ?? bookingProviderName); + setEditingAppointmentId(appointment.id); setBookingOpen(true); } @@ -179,15 +210,22 @@ export default function AppointmentsPage() { setToastSuccess(''); setToastInfo(''); try { - await appointmentsApi.create(payload); + if (activeEditingAppointment) { + await appointmentsApi.update(activeEditingAppointment.id, payload); + } else { + await appointmentsApi.create(payload); + } setBookingOpen(false); - setToastSuccess('Appointment saved.'); + setEditingAppointmentId(null); + setToastSuccess(activeEditingAppointment ? 'Appointment updated.' : 'Appointment saved.'); await loadSchedule(); } catch (err: unknown) { const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) - : 'Could not save appointment.'; + : activeEditingAppointment + ? 'Could not update appointment.' + : 'Could not save appointment.'; setToastError(message); } finally { setSavingAppointment(false); @@ -285,10 +323,11 @@ export default function AppointmentsPage() { day={scheduleDate} providers={providers} appointments={appointments} - canBook={canManageAppointments} + canBook={canManageAppointments && !isViewingPastDay} canDelete={canManageAppointments} onDeleteAppointment={(id) => void handleDeleteAppointment(id)} onSlotClick={(hour, uid, name) => handleSlotClick(hour, uid, name)} + onAppointmentClick={(apt) => handleAppointmentClick(apt)} /> @@ -300,7 +339,11 @@ export default function AppointmentsPage() { providerUserId={bookingProviderId} providerName={bookingProviderName} initialHour={bookingHour} - onClose={() => setBookingOpen(false)} + editingAppointment={activeEditingAppointment} + onClose={() => { + setBookingOpen(false); + setEditingAppointmentId(null); + }} onSubmit={handleSaveAppointment} loading={savingAppointment} /> diff --git a/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx b/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx index d5f4903..770f753 100644 --- a/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx +++ b/frontend/src/components/ui/appointments/AppointmentBookingModal.tsx @@ -4,11 +4,12 @@ import { useEffect, useState } from 'react'; import { X } from 'lucide-react'; import { Button } from '@/components/ui/common/Button'; import { Dropdown } from '@/components/ui/common/Dropdown'; -import type { AppointmentPurpose } from '@/types/appointment'; +import type { AppointmentPurpose, AppointmentRecord } from '@/types/appointment'; import { APPOINTMENT_PURPOSE_LABEL } from '@/components/ui/appointments/appointmentPurposeStyles'; import type { Patient } from '@/types/patient'; import { combineLocalDateAndTime, + compareLocalDayStart, formatTimeForInput, isSameLocalCalendarDay, } from '@/lib/appointmentTime'; @@ -28,6 +29,7 @@ interface AppointmentBookingModalProps { endAt: string; purpose: AppointmentPurpose; }) => Promise; + editingAppointment?: AppointmentRecord | null; loading?: boolean; } @@ -40,6 +42,7 @@ export function AppointmentBookingModal({ initialHour, onClose, onSubmit, + editingAppointment = null, loading = false, }: AppointmentBookingModalProps) { const [startTime, setStartTime] = useState('09:00'); @@ -61,29 +64,37 @@ export function AppointmentBookingModal({ if (!open) { return; } - const start = new Date( - scheduleDate.getFullYear(), - scheduleDate.getMonth(), - scheduleDate.getDate(), - initialHour, - 0, - 0, - 0, - ); - const end = new Date( - scheduleDate.getFullYear(), - scheduleDate.getMonth(), - scheduleDate.getDate(), - initialHour < 23 ? initialHour + 1 : 23, - initialHour < 23 ? 0 : 59, - 0, - 0, - ); - setStartTime(formatTimeForInput(start)); - setEndTime(formatTimeForInput(end)); - setPurpose('consultation'); + if (editingAppointment) { + const start = new Date(editingAppointment.startAt); + const end = new Date(editingAppointment.endAt); + setStartTime(formatTimeForInput(start)); + setEndTime(formatTimeForInput(end)); + setPurpose((editingAppointment.purpose as AppointmentPurpose) ?? 'consultation'); + } else { + const start = new Date( + scheduleDate.getFullYear(), + scheduleDate.getMonth(), + scheduleDate.getDate(), + initialHour, + 0, + 0, + 0, + ); + const end = new Date( + scheduleDate.getFullYear(), + scheduleDate.getMonth(), + scheduleDate.getDate(), + initialHour < 23 ? initialHour + 1 : 23, + initialHour < 23 ? 0 : 59, + 0, + 0, + ); + setStartTime(formatTimeForInput(start)); + setEndTime(formatTimeForInput(end)); + setPurpose('consultation'); + } setError(''); - }, [open, scheduleDate, initialHour]); + }, [open, scheduleDate, initialHour, editingAppointment]); if (!open || !providerUserId) { return null; @@ -97,7 +108,7 @@ export function AppointmentBookingModal({ if (!providerUserId) { return; } - if (!patient) { + if (!editingAppointment && !patient) { setError('Select a patient first.'); return; } @@ -116,9 +127,22 @@ export function AppointmentBookingModal({ return; } + const today = new Date(); + if (compareLocalDayStart(scheduleDate, today) < 0) { + setError('Past appointments are view-only.'); + return; + } + + const effectivePatientId = editingAppointment?.patientId ?? patient?.id; + const effectiveProviderId = editingAppointment?.providerUserId ?? providerUserId; + if (!effectivePatientId || !effectiveProviderId) { + setError('Missing appointment details.'); + return; + } + await onSubmit({ - patientId: patient.id, - providerUserId, + patientId: effectivePatientId, + providerUserId: effectiveProviderId, startAt: startAt.toISOString(), endAt: endAt.toISOString(), purpose, @@ -135,7 +159,7 @@ export function AppointmentBookingModal({ >

- New appointment + {editingAppointment ? 'Edit appointment' : 'New appointment'}

diff --git a/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx b/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx index ad515af..acb47a4 100644 --- a/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx +++ b/frontend/src/components/ui/appointments/AppointmentScheduleGrid.tsx @@ -35,6 +35,7 @@ interface AppointmentScheduleGridProps { canDelete?: boolean; onDeleteAppointment?: (id: string) => void; onSlotClick: (hour: number, providerUserId: string, providerName: string) => void; + onAppointmentClick?: (appointment: AppointmentRecord) => void; } export function AppointmentScheduleGrid({ @@ -45,6 +46,7 @@ export function AppointmentScheduleGrid({ canDelete = false, onDeleteAppointment, onSlotClick, + onAppointmentClick, }: AppointmentScheduleGridProps) { const gridHeight = HOURS.length * HOUR_PX; @@ -120,9 +122,11 @@ export function AppointmentScheduleGrid({ return null; } return ( -
onAppointmentClick?.(apt)} + className={`absolute left-0.5 right-0.5 rounded-[var(--radius-sm)] border pointer-events-auto z-10 flex flex-row items-center gap-1.5 px-1.5 py-1 min-h-[36px] text-left ${purposeStyle(apt.purpose)} focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/35`} style={{ top: pos.top, height: pos.height, minHeight: 36 }} >
@@ -147,7 +151,7 @@ export function AppointmentScheduleGrid({ )} -
+ ); })}
diff --git a/frontend/src/components/ui/common/ScheduleDayPicker.tsx b/frontend/src/components/ui/common/ScheduleDayPicker.tsx index 744f308..e174d21 100644 --- a/frontend/src/components/ui/common/ScheduleDayPicker.tsx +++ b/frontend/src/components/ui/common/ScheduleDayPicker.tsx @@ -1,19 +1,17 @@ 'use client'; import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { addCalendarDays, compareLocalDayStart } from '@/lib/appointmentTime'; +import { addCalendarDays } from '@/lib/appointmentTime'; interface ScheduleDayPickerProps { value: Date; onChange: (day: Date) => void; - /** Inclusive minimum calendar day (typically today at local midnight). */ - minDate: Date; + /** Optional lower bound; picker navigation is unrestricted for history browsing. */ + minDate?: Date; label?: string; } -export function ScheduleDayPicker({ value, onChange, minDate, label = 'Schedule date' }: ScheduleDayPickerProps) { - const canGoPrev = compareLocalDayStart(value, minDate) > 0; - +export function ScheduleDayPicker({ value, onChange, label = 'Schedule date' }: ScheduleDayPickerProps) { const labelText = value.toLocaleDateString(undefined, { weekday: 'short', month: 'short', @@ -27,9 +25,8 @@ export function ScheduleDayPicker({ value, onChange, minDate, label = 'Schedule