improvement: datepicker component now let user choose past dates too.

This commit is contained in:
2026-05-08 14:25:44 +03:30
parent a0348e4079
commit 803564802c
5 changed files with 127 additions and 45 deletions

View File

@@ -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<void>;
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({
>
<div className="flex items-start justify-between gap-2">
<h2 id="appointment-modal-title" className="text-lg font-semibold text-text-primary pr-2">
New appointment
{editingAppointment ? 'Edit appointment' : 'New appointment'}
</h2>
<button
type="button"
@@ -154,7 +178,11 @@ export function AppointmentBookingModal({
<div>
<label className="block text-sm font-medium text-text-secondary mb-1">Patient</label>
<p className="text-sm text-text-primary rounded-[var(--radius-md)] border border-border bg-background-secondary/60 px-3 py-2">
{patient ? `${patient.firstName} ${patient.lastName}` : '—'}
{editingAppointment
? `${editingAppointment.patient.firstName} ${editingAppointment.patient.lastName}`
: patient
? `${patient.firstName} ${patient.lastName}`
: '—'}
</p>
</div>

View File

@@ -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 (
<div
<button
type="button"
key={apt.id}
className={`absolute left-0.5 right-0.5 rounded-[var(--radius-sm)] border pointer-events-none z-10 flex flex-row items-center gap-1.5 px-1.5 py-1 min-h-[36px] ${purposeStyle(apt.purpose)}`}
onClick={() => 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 }}
>
<div className="pointer-events-none flex-1 min-w-0 overflow-hidden text-left">
@@ -147,7 +151,7 @@ export function AppointmentScheduleGrid({
<Trash2 className={`w-4 h-4 ${purposeDeleteIconClass(apt.purpose)}`} />
</button>
)}
</div>
</button>
);
})}
</div>

View File

@@ -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
<div className="flex items-center gap-1 rounded-[var(--radius-md)] border border-border bg-background-secondary/90 px-1 py-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.02)]">
<button
type="button"
disabled={!canGoPrev}
onClick={() => onChange(addCalendarDays(value, -1))}
className="shrink-0 rounded-[var(--radius-sm)] p-2 text-text-muted hover:text-text-primary hover:bg-background-card/80 disabled:opacity-35 disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-primary/35"
className="shrink-0 rounded-[var(--radius-sm)] p-2 text-text-muted hover:text-text-primary hover:bg-background-card/80 focus:outline-none focus:ring-2 focus:ring-primary/35"
aria-label="Previous day"
>
<ChevronLeft className="h-4 w-4 icon-flat" />