115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { CalendarDays } from 'lucide-react';
|
|||
|
|
import { purposeStyle } from '@/components/ui/appointments/appointmentPurposeStyles';
|
|||
|
|
import { ScheduleDayPicker } from '@/components/ui/common/ScheduleDayPicker';
|
|||
|
|
import { startOfLocalDay } from '@/lib/appointmentTime';
|
|||
|
|
import type { TreatmentAppointment } from '@/types/treatment';
|
|||
|
|
|
|||
|
|
interface AppointmentsStripProps {
|
|||
|
|
stripHidden: boolean;
|
|||
|
|
onToggleStripHidden: () => void;
|
|||
|
|
selectedDay: Date;
|
|||
|
|
onSelectDay: (day: Date) => void;
|
|||
|
|
/** Same lower bound as Appointments schedule (cannot pick days before this). */
|
|||
|
|
minScheduleDate: Date;
|
|||
|
|
appointments: TreatmentAppointment[];
|
|||
|
|
selectedAppointmentId: string | null;
|
|||
|
|
onSelectAppointment: (id: string) => void;
|
|||
|
|
loading?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function AppointmentsStrip({
|
|||
|
|
stripHidden,
|
|||
|
|
onToggleStripHidden,
|
|||
|
|
selectedDay,
|
|||
|
|
onSelectDay,
|
|||
|
|
minScheduleDate,
|
|||
|
|
appointments,
|
|||
|
|
selectedAppointmentId,
|
|||
|
|
onSelectAppointment,
|
|||
|
|
loading = false,
|
|||
|
|
}: AppointmentsStripProps) {
|
|||
|
|
if (stripHidden) {
|
|||
|
|
return (
|
|||
|
|
<div className="surface-card p-3 flex items-center justify-between gap-3 flex-wrap">
|
|||
|
|
<p className="text-sm text-text-secondary">Appointments are hidden.</p>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={onToggleStripHidden}
|
|||
|
|
className="text-sm font-medium text-primary hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 rounded-[var(--radius-sm)]"
|
|||
|
|
>
|
|||
|
|
Show appointments
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="surface-card p-4 space-y-4">
|
|||
|
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
|||
|
|
<div className="flex items-center gap-2 min-w-0">
|
|||
|
|
<CalendarDays className="w-5 h-5 text-text-muted shrink-0 icon-flat" aria-hidden />
|
|||
|
|
<h2 className="text-sm font-semibold text-text-primary truncate">My appointments</h2>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={onToggleStripHidden}
|
|||
|
|
className="text-sm font-medium text-primary hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 rounded-[var(--radius-sm)]"
|
|||
|
|
>
|
|||
|
|
Hide appointments
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex flex-col sm:flex-row sm:items-end gap-4 sm:justify-between">
|
|||
|
|
<ScheduleDayPicker
|
|||
|
|
value={selectedDay}
|
|||
|
|
minDate={minScheduleDate}
|
|||
|
|
onChange={(d) => onSelectDay(startOfLocalDay(d))}
|
|||
|
|
/>
|
|||
|
|
{loading && (
|
|||
|
|
<p className="text-sm text-text-muted pb-2">Loading appointments…</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex flex-wrap gap-2">
|
|||
|
|
{appointments.length === 0 && (
|
|||
|
|
<p className="text-sm text-text-muted">No appointments assigned to you on this day.</p>
|
|||
|
|
)}
|
|||
|
|
{appointments.map((a) => {
|
|||
|
|
const sel = a.id === selectedAppointmentId;
|
|||
|
|
const start = new Date(a.startAt);
|
|||
|
|
const end = new Date(a.endAt);
|
|||
|
|
const timeLabel = `${start.toLocaleTimeString(undefined, {
|
|||
|
|
hour: 'numeric',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
})} – ${end.toLocaleTimeString(undefined, {
|
|||
|
|
hour: 'numeric',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
})}`;
|
|||
|
|
const palette = purposeStyle(a.purpose);
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
key={a.id}
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => onSelectAppointment(a.id)}
|
|||
|
|
className={`
|
|||
|
|
text-left rounded-[var(--radius-sm)] border px-3 py-2 min-w-[200px] max-w-[280px] transition-shadow min-h-[52px]
|
|||
|
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/45
|
|||
|
|
${palette}
|
|||
|
|
${sel ? 'ring-2 ring-primary ring-offset-2 ring-offset-background-secondary shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]' : 'hover:brightness-110'}
|
|||
|
|
`}
|
|||
|
|
>
|
|||
|
|
<p className="text-[11px] font-medium tabular-nums opacity-95">{timeLabel}</p>
|
|||
|
|
<p className="text-sm font-medium leading-tight truncate mt-0.5">
|
|||
|
|
{a.patientFirstName} {a.patientLastName}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-[11px] opacity-90 capitalize mt-0.5">{a.purpose}</p>
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|