- E1.S1: FullCalendar setup with React wrapper - E1.S2: Day view (timeGridDay) with hourly slots - E1.S3: Week view (timeGridWeek) with 7-day grid - E1.S4: Workweek view (timeGridWorkWeek, Mon-Fri) Features: - View switcher toolbar (Dag/Week/Werkweek) - Date navigation (prev/next/today) - Dutch localization (nl) - Business hours highlighting (08:00-18:00) - Color-coded appointments by type - Drag-and-drop rescheduling support - Server actions for encounter CRUD 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
212 lines
5.9 KiB
TypeScript
212 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Agenda Calendar Component
|
|
*
|
|
* FullCalendar wrapper with day/week/workweek views.
|
|
*/
|
|
|
|
import { useCallback, useRef } from 'react';
|
|
import FullCalendar from '@fullcalendar/react';
|
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
import interactionPlugin from '@fullcalendar/interaction';
|
|
import type { EventClickArg, DateSelectArg, EventDropArg, CalendarApi } from '@fullcalendar/core';
|
|
import nlLocale from '@fullcalendar/core/locales/nl';
|
|
|
|
import type { CalendarEvent, CalendarView } from '../types';
|
|
|
|
interface AgendaCalendarProps {
|
|
events: CalendarEvent[];
|
|
initialView?: CalendarView;
|
|
onEventClick?: (event: CalendarEvent) => void;
|
|
onDateSelect?: (start: Date, end: Date) => void;
|
|
onEventDrop?: (eventId: string, newStart: Date, newEnd: Date | null) => void;
|
|
onDateChange?: (start: Date, end: Date) => void;
|
|
calendarRef?: React.RefObject<CalendarApi | null>;
|
|
}
|
|
|
|
export function AgendaCalendar({
|
|
events,
|
|
initialView = 'timeGridWeek',
|
|
onEventClick,
|
|
onDateSelect,
|
|
onEventDrop,
|
|
onDateChange,
|
|
calendarRef: externalRef,
|
|
}: AgendaCalendarProps) {
|
|
const internalRef = useRef<FullCalendar>(null);
|
|
|
|
const handleEventClick = useCallback((info: EventClickArg) => {
|
|
if (onEventClick) {
|
|
const event = info.event;
|
|
const calendarEvent: CalendarEvent = {
|
|
id: event.id,
|
|
title: event.title,
|
|
start: event.start!,
|
|
end: event.end || undefined,
|
|
extendedProps: event.extendedProps as CalendarEvent['extendedProps'],
|
|
backgroundColor: event.backgroundColor,
|
|
borderColor: event.borderColor,
|
|
};
|
|
onEventClick(calendarEvent);
|
|
}
|
|
}, [onEventClick]);
|
|
|
|
const handleDateSelect = useCallback((info: DateSelectArg) => {
|
|
if (onDateSelect) {
|
|
onDateSelect(info.start, info.end);
|
|
}
|
|
}, [onDateSelect]);
|
|
|
|
const handleEventDrop = useCallback((info: EventDropArg) => {
|
|
if (onEventDrop) {
|
|
onEventDrop(
|
|
info.event.id,
|
|
info.event.start!,
|
|
info.event.end
|
|
);
|
|
}
|
|
}, [onEventDrop]);
|
|
|
|
const handleDatesSet = useCallback((dateInfo: { start: Date; end: Date }) => {
|
|
if (onDateChange) {
|
|
onDateChange(dateInfo.start, dateInfo.end);
|
|
}
|
|
}, [onDateChange]);
|
|
|
|
return (
|
|
<div className="agenda-calendar h-full">
|
|
<FullCalendar
|
|
ref={internalRef}
|
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
|
initialView={initialView}
|
|
locale={nlLocale}
|
|
headerToolbar={false}
|
|
events={events}
|
|
editable={true}
|
|
selectable={true}
|
|
selectMirror={true}
|
|
dayMaxEvents={true}
|
|
weekends={true}
|
|
slotMinTime="07:00:00"
|
|
slotMaxTime="20:00:00"
|
|
slotDuration="00:30:00"
|
|
slotLabelInterval="01:00:00"
|
|
slotLabelFormat={{
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
}}
|
|
eventTimeFormat={{
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
}}
|
|
allDaySlot={false}
|
|
nowIndicator={true}
|
|
height="100%"
|
|
expandRows={true}
|
|
stickyHeaderDates={true}
|
|
eventClick={handleEventClick}
|
|
select={handleDateSelect}
|
|
eventDrop={handleEventDrop}
|
|
datesSet={handleDatesSet}
|
|
views={{
|
|
timeGridWorkWeek: {
|
|
type: 'timeGrid',
|
|
duration: { weeks: 1 },
|
|
hiddenDays: [0, 6], // Hide Sunday and Saturday
|
|
buttonText: 'Werkweek',
|
|
},
|
|
}}
|
|
businessHours={{
|
|
daysOfWeek: [1, 2, 3, 4, 5],
|
|
startTime: '08:00',
|
|
endTime: '18:00',
|
|
}}
|
|
eventContent={(eventInfo) => (
|
|
<div className="p-1 overflow-hidden h-full">
|
|
<div className="font-medium text-xs truncate">
|
|
{eventInfo.timeText}
|
|
</div>
|
|
<div className="font-semibold text-sm truncate">
|
|
{eventInfo.event.title}
|
|
</div>
|
|
{eventInfo.event.extendedProps?.encounter?.type_display && (
|
|
<div className="text-xs opacity-80 truncate">
|
|
{eventInfo.event.extendedProps.encounter.type_display}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
<style jsx global>{`
|
|
.agenda-calendar .fc {
|
|
--fc-border-color: #e2e8f0;
|
|
--fc-today-bg-color: #f0fdf4;
|
|
--fc-now-indicator-color: #22c55e;
|
|
--fc-event-border-color: transparent;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.agenda-calendar .fc-theme-standard td,
|
|
.agenda-calendar .fc-theme-standard th {
|
|
border-color: var(--fc-border-color);
|
|
}
|
|
|
|
.agenda-calendar .fc-timegrid-slot {
|
|
height: 2.5rem;
|
|
}
|
|
|
|
.agenda-calendar .fc-timegrid-slot-label {
|
|
font-size: 0.75rem;
|
|
color: #64748b;
|
|
}
|
|
|
|
.agenda-calendar .fc-col-header-cell {
|
|
padding: 0.5rem 0;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
.agenda-calendar .fc-col-header-cell-cushion {
|
|
padding: 0.25rem 0.5rem;
|
|
}
|
|
|
|
.agenda-calendar .fc-day-today .fc-col-header-cell-cushion {
|
|
color: #16a34a;
|
|
}
|
|
|
|
.agenda-calendar .fc-timegrid-event {
|
|
border-radius: 0.375rem;
|
|
border-width: 2px;
|
|
border-left-width: 4px;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.agenda-calendar .fc-timegrid-event:hover {
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.agenda-calendar .fc-timegrid-now-indicator-line {
|
|
border-width: 2px;
|
|
}
|
|
|
|
.agenda-calendar .fc-timegrid-now-indicator-arrow {
|
|
border-width: 6px;
|
|
}
|
|
|
|
.agenda-calendar .fc-highlight {
|
|
background-color: #dbeafe;
|
|
}
|
|
|
|
.agenda-calendar .fc-non-business {
|
|
background-color: #f8fafc;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|