git minicalender-view, improved agenda-view

This commit is contained in:
colinislit
2025-12-03 14:30:40 +01:00
parent 9724398302
commit 56c4f1cedc
3 changed files with 260 additions and 15 deletions

View File

@@ -6,7 +6,7 @@
* FullCalendar wrapper with day/week/workweek views. * FullCalendar wrapper with day/week/workweek views.
*/ */
import { useCallback, useRef } from 'react'; import { useCallback, useRef, useEffect } from 'react';
import FullCalendar from '@fullcalendar/react'; import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid'; import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid'; import timeGridPlugin from '@fullcalendar/timegrid';
@@ -19,6 +19,7 @@ import type { CalendarEvent, CalendarView } from '../types';
interface AgendaCalendarProps { interface AgendaCalendarProps {
events: CalendarEvent[]; events: CalendarEvent[];
initialView?: CalendarView; initialView?: CalendarView;
currentView?: CalendarView;
onEventClick?: (event: CalendarEvent) => void; onEventClick?: (event: CalendarEvent) => void;
onDateSelect?: (start: Date, end: Date) => void; onDateSelect?: (start: Date, end: Date) => void;
onEventDrop?: (eventId: string, newStart: Date, newEnd: Date | null) => void; onEventDrop?: (eventId: string, newStart: Date, newEnd: Date | null) => void;
@@ -29,6 +30,7 @@ interface AgendaCalendarProps {
export function AgendaCalendar({ export function AgendaCalendar({
events, events,
initialView = 'timeGridWeek', initialView = 'timeGridWeek',
currentView,
onEventClick, onEventClick,
onDateSelect, onDateSelect,
onEventDrop, onEventDrop,
@@ -36,6 +38,22 @@ export function AgendaCalendar({
calendarRef: externalRef, calendarRef: externalRef,
}: AgendaCalendarProps) { }: AgendaCalendarProps) {
const internalRef = useRef<FullCalendar>(null); const internalRef = useRef<FullCalendar>(null);
const isChangingViewRef = useRef(false);
// Sync view when currentView prop changes
useEffect(() => {
if (currentView && internalRef.current) {
const api = internalRef.current.getApi();
if (api.view.type !== currentView) {
isChangingViewRef.current = true;
api.changeView(currentView);
// Reset flag after a short delay to allow the view change to complete
setTimeout(() => {
isChangingViewRef.current = false;
}, 100);
}
}
}, [currentView]);
const handleEventClick = useCallback((info: EventClickArg) => { const handleEventClick = useCallback((info: EventClickArg) => {
if (onEventClick) { if (onEventClick) {
@@ -70,6 +88,10 @@ export function AgendaCalendar({
}, [onEventDrop]); }, [onEventDrop]);
const handleDatesSet = useCallback((dateInfo: { start: Date; end: Date }) => { const handleDatesSet = useCallback((dateInfo: { start: Date; end: Date }) => {
// Skip if we're in the middle of a programmatic view change
if (isChangingViewRef.current) {
return;
}
if (onDateChange) { if (onDateChange) {
onDateChange(dateInfo.start, dateInfo.end); onDateChange(dateInfo.start, dateInfo.end);
} }

View File

@@ -14,6 +14,7 @@ import { AgendaCalendar } from './agenda-calendar';
import { AgendaToolbar } from './agenda-toolbar'; import { AgendaToolbar } from './agenda-toolbar';
import { AppointmentModal } from './appointment-modal'; import { AppointmentModal } from './appointment-modal';
import { RescheduleDialog } from './reschedule-dialog'; import { RescheduleDialog } from './reschedule-dialog';
import { MiniCalendar } from './mini-calendar';
import { getEncounters, rescheduleEncounter } from '../actions'; import { getEncounters, rescheduleEncounter } from '../actions';
import type { CalendarEvent, CalendarView } from '../types'; import type { CalendarEvent, CalendarView } from '../types';
@@ -190,6 +191,21 @@ export function AgendaView({ initialEvents, initialDate, highlightEncounterId }:
setIsModalOpen(true); setIsModalOpen(true);
}, [currentDate]); }, [currentDate]);
// Handle mini calendar date selection
const handleMiniCalendarDateSelect = useCallback((date: Date) => {
setCurrentDate(date);
// Calculate date range based on current view
const start = currentView === 'timeGridDay'
? date
: startOfWeek(date, { weekStartsOn: 1 });
const end = currentView === 'timeGridDay'
? addDays(date, 1)
: endOfWeek(date, { weekStartsOn: 1 });
fetchEvents(start, end);
}, [currentView, fetchEvents]);
// Handle modal success (refresh events) // Handle modal success (refresh events)
const handleModalSuccess = useCallback(() => { const handleModalSuccess = useCallback(() => {
const start = currentView === 'timeGridDay' const start = currentView === 'timeGridDay'
@@ -211,20 +227,32 @@ export function AgendaView({ initialEvents, initialDate, highlightEncounterId }:
onNewAppointment={handleNewAppointment} onNewAppointment={handleNewAppointment}
/> />
<div className="flex-1 bg-white rounded-lg border border-slate-200 overflow-hidden"> <div className="flex flex-1 gap-4 min-h-0">
{isPending && ( {/* Mini calendar sidebar */}
<div className="absolute inset-0 bg-white/50 flex items-center justify-center z-10"> <div className="w-64 flex-shrink-0">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" /> <MiniCalendar
</div> selectedDate={currentDate}
)} onDateSelect={handleMiniCalendarDateSelect}
<AgendaCalendar />
events={events} </div>
initialView={currentView}
onEventClick={handleEventClick} {/* Main calendar */}
onDateSelect={handleDateSelect} <div className="flex-1 bg-white rounded-lg border border-slate-200 overflow-hidden relative">
onEventDrop={handleEventDrop} {isPending && (
onDateChange={handleDateChange} <div className="absolute inset-0 bg-white/50 flex items-center justify-center z-10">
/> <div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
</div>
)}
<AgendaCalendar
events={events}
initialView={currentView}
currentView={currentView}
onEventClick={handleEventClick}
onDateSelect={handleDateSelect}
onEventDrop={handleEventDrop}
onDateChange={handleDateChange}
/>
</div>
</div> </div>
{/* Appointment Modal */} {/* Appointment Modal */}

View File

@@ -0,0 +1,195 @@
'use client';
/**
* Mini Calendar Component
*
* Compact month view calendar for quick date navigation.
* Uses FullCalendar daygrid plugin for consistency with the main calendar.
*/
import { useCallback, useRef, useEffect } from 'react';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import nlLocale from '@fullcalendar/core/locales/nl';
import type { DateClickArg } from '@fullcalendar/interaction';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { format } from 'date-fns';
import { nl } from 'date-fns/locale';
interface MiniCalendarProps {
selectedDate: Date;
onDateSelect: (date: Date) => void;
}
export function MiniCalendar({ selectedDate, onDateSelect }: MiniCalendarProps) {
const calendarRef = useRef<FullCalendar>(null);
// Navigate to month containing selected date
useEffect(() => {
if (calendarRef.current) {
const api = calendarRef.current.getApi();
api.gotoDate(selectedDate);
}
}, [selectedDate]);
const handleDateClick = useCallback((info: DateClickArg) => {
onDateSelect(info.date);
}, [onDateSelect]);
const handlePrevMonth = useCallback(() => {
if (calendarRef.current) {
calendarRef.current.getApi().prev();
}
}, []);
const handleNextMonth = useCallback(() => {
if (calendarRef.current) {
calendarRef.current.getApi().next();
}
}, []);
const handleToday = useCallback(() => {
if (calendarRef.current) {
calendarRef.current.getApi().today();
}
onDateSelect(new Date());
}, [onDateSelect]);
return (
<div className="bg-white rounded-lg border border-slate-200 p-3">
{/* Custom header with month/year navigation */}
<div className="flex items-center justify-between mb-2">
<Button
variant="ghost"
size="icon"
onClick={handlePrevMonth}
className="h-7 w-7"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<button
onClick={handleToday}
className="text-sm font-semibold text-slate-800 hover:text-teal-600 transition-colors capitalize"
>
{format(selectedDate, 'MMMM yyyy', { locale: nl })}
</button>
<Button
variant="ghost"
size="icon"
onClick={handleNextMonth}
className="h-7 w-7"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{/* Mini calendar */}
<div className="mini-calendar">
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
locale={nlLocale}
headerToolbar={false}
height="auto"
fixedWeekCount={false}
showNonCurrentDates={true}
dayMaxEvents={0}
dateClick={handleDateClick}
dayCellClassNames={(arg) => {
const isSelected = arg.date.toDateString() === selectedDate.toDateString();
return isSelected ? 'mini-cal-selected' : '';
}}
/>
</div>
<style jsx global>{`
.mini-calendar .fc {
--fc-border-color: #e2e8f0;
--fc-today-bg-color: #f0fdf4;
font-family: inherit;
font-size: 0.75rem;
}
.mini-calendar .fc-theme-standard td,
.mini-calendar .fc-theme-standard th {
border: none;
}
.mini-calendar .fc-theme-standard .fc-scrollgrid {
border: none;
}
.mini-calendar .fc-col-header-cell {
padding: 0.25rem 0;
font-weight: 500;
color: #64748b;
font-size: 0.65rem;
text-transform: uppercase;
}
.mini-calendar .fc-daygrid-day {
cursor: pointer;
}
.mini-calendar .fc-daygrid-day-frame {
min-height: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.mini-calendar .fc-daygrid-day-top {
flex-direction: row;
justify-content: center;
}
.mini-calendar .fc-daygrid-day-number {
padding: 0;
font-size: 0.75rem;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.15s;
}
.mini-calendar .fc-daygrid-day:hover .fc-daygrid-day-number {
background-color: #f1f5f9;
}
.mini-calendar .fc-day-today .fc-daygrid-day-number {
background-color: #14b8a6;
color: white;
font-weight: 600;
}
.mini-calendar .mini-cal-selected .fc-daygrid-day-number {
background-color: #0d9488;
color: white;
font-weight: 600;
}
.mini-calendar .fc-day-today.mini-cal-selected .fc-daygrid-day-number {
background-color: #0f766e;
}
.mini-calendar .fc-day-other .fc-daygrid-day-number {
color: #cbd5e1;
}
.mini-calendar .fc-daygrid-day-events {
display: none;
}
.mini-calendar .fc-daygrid-day-bottom {
display: none;
}
`}</style>
</div>
);
}