411 lines
14 KiB
TypeScript
411 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { Calendar, dateFnsLocalizer, View } from 'react-big-calendar';
|
||
import { format, parse, startOfWeek, getDay, addMonths, subMonths } from 'date-fns';
|
||
import { de } from 'date-fns/locale';
|
||
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
||
import DashboardSidebar from '@/components/DashboardSidebar';
|
||
import { useSession } from 'next-auth/react';
|
||
import { FiChevronLeft, FiChevronRight, FiCalendar } from 'react-icons/fi';
|
||
|
||
const locales = {
|
||
de: de,
|
||
};
|
||
|
||
const localizer = dateFnsLocalizer({
|
||
format,
|
||
parse,
|
||
startOfWeek: () => startOfWeek(new Date(), { locale: de }),
|
||
getDay,
|
||
locales,
|
||
});
|
||
|
||
interface CalendarEvent {
|
||
id: string;
|
||
title: string;
|
||
start: Date;
|
||
end: Date;
|
||
resource: {
|
||
bookingId: string;
|
||
status: string;
|
||
customerName: string;
|
||
customerEmail: string;
|
||
locationName: string;
|
||
photoboxName: string;
|
||
tourId?: string;
|
||
eventType: string;
|
||
};
|
||
}
|
||
|
||
export default function KalenderPage() {
|
||
const { data: session } = useSession();
|
||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
||
const [view, setView] = useState<View>('month');
|
||
const [date, setDate] = useState(new Date());
|
||
|
||
const fetchEvents = useCallback(async (start?: Date, end?: Date) => {
|
||
setLoading(true);
|
||
try {
|
||
const params = new URLSearchParams();
|
||
if (start) params.append('start', start.toISOString());
|
||
if (end) params.append('end', end.toISOString());
|
||
|
||
const response = await fetch(`/api/calendar?${params.toString()}`);
|
||
const data = await response.json();
|
||
|
||
const parsedEvents = data.events.map((event: any) => ({
|
||
...event,
|
||
start: new Date(event.start),
|
||
end: new Date(event.end),
|
||
}));
|
||
|
||
setEvents(parsedEvents);
|
||
} catch (error) {
|
||
console.error('Error fetching calendar events:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchEvents();
|
||
}, [fetchEvents]);
|
||
|
||
const handleNavigate = (newDate: Date) => {
|
||
setDate(newDate);
|
||
};
|
||
|
||
const handleViewChange = (newView: View) => {
|
||
setView(newView);
|
||
};
|
||
|
||
const eventStyleGetter = (event: CalendarEvent) => {
|
||
let backgroundColor = '#6b7280';
|
||
|
||
switch (event.resource.status) {
|
||
case 'PENDING':
|
||
backgroundColor = '#f59e0b';
|
||
break;
|
||
case 'RESERVED':
|
||
backgroundColor = '#3b82f6';
|
||
break;
|
||
case 'CONFIRMED':
|
||
backgroundColor = '#10b981';
|
||
break;
|
||
case 'TOUR_CREATED':
|
||
backgroundColor = '#8b5cf6';
|
||
break;
|
||
case 'COMPLETED':
|
||
backgroundColor = '#6b7280';
|
||
break;
|
||
case 'CANCELLED':
|
||
backgroundColor = '#ef4444';
|
||
break;
|
||
}
|
||
|
||
return {
|
||
style: {
|
||
backgroundColor,
|
||
borderRadius: '4px',
|
||
opacity: 0.9,
|
||
color: 'white',
|
||
border: '0px',
|
||
display: 'block',
|
||
},
|
||
};
|
||
};
|
||
|
||
const CustomToolbar = ({ label, onNavigate }: any) => {
|
||
return (
|
||
<div className="flex items-center justify-between mb-6 bg-gray-800 p-4 rounded-lg">
|
||
<div className="flex items-center gap-2">
|
||
<FiCalendar className="text-2xl text-red-400" />
|
||
<h2 className="text-2xl font-bold text-white">{label}</h2>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => onNavigate('PREV')}
|
||
className="p-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
||
>
|
||
<FiChevronLeft />
|
||
</button>
|
||
<button
|
||
onClick={() => onNavigate('TODAY')}
|
||
className="px-4 py-2 bg-red-600 hover:bg-red-500 text-white rounded-lg font-medium transition-colors"
|
||
>
|
||
Heute
|
||
</button>
|
||
<button
|
||
onClick={() => onNavigate('NEXT')}
|
||
className="p-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
||
>
|
||
<FiChevronRight />
|
||
</button>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => setView('month')}
|
||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||
view === 'month'
|
||
? 'bg-red-600 text-white'
|
||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||
}`}
|
||
>
|
||
Monat
|
||
</button>
|
||
<button
|
||
onClick={() => setView('week')}
|
||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||
view === 'week'
|
||
? 'bg-red-600 text-white'
|
||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||
}`}
|
||
>
|
||
Woche
|
||
</button>
|
||
<button
|
||
onClick={() => setView('day')}
|
||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||
view === 'day'
|
||
? 'bg-red-600 text-white'
|
||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||
}`}
|
||
>
|
||
Tag
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
||
<div className="flex">
|
||
<DashboardSidebar user={session?.user} />
|
||
<main className="flex-1 p-8">
|
||
<div className="bg-gray-800/50 rounded-lg p-6 shadow-lg">
|
||
{loading ? (
|
||
<div className="flex items-center justify-center h-96">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-red-500"></div>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div style={{ height: '700px' }} className="calendar-container">
|
||
<Calendar
|
||
localizer={localizer}
|
||
events={events}
|
||
startAccessor="start"
|
||
endAccessor="end"
|
||
style={{ height: '100%' }}
|
||
onSelectEvent={(event) => setSelectedEvent(event)}
|
||
eventPropGetter={eventStyleGetter}
|
||
view={view}
|
||
onView={handleViewChange}
|
||
date={date}
|
||
onNavigate={handleNavigate}
|
||
components={{
|
||
toolbar: CustomToolbar,
|
||
}}
|
||
messages={{
|
||
next: 'Weiter',
|
||
previous: 'Zurück',
|
||
today: 'Heute',
|
||
month: 'Monat',
|
||
week: 'Woche',
|
||
day: 'Tag',
|
||
agenda: 'Agenda',
|
||
date: 'Datum',
|
||
time: 'Zeit',
|
||
event: 'Event',
|
||
noEventsInRange: 'Keine Events in diesem Zeitraum',
|
||
showMore: (total) => `+ ${total} mehr`,
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div className="mt-6 flex gap-4 flex-wrap">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-amber-500"></div>
|
||
<span className="text-sm text-gray-300">Pending</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-blue-500"></div>
|
||
<span className="text-sm text-gray-300">Reserviert</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-green-500"></div>
|
||
<span className="text-sm text-gray-300">Bestätigt</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-purple-500"></div>
|
||
<span className="text-sm text-gray-300">Tour erstellt</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-gray-500"></div>
|
||
<span className="text-sm text-gray-300">Abgeschlossen</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-4 h-4 rounded bg-red-500"></div>
|
||
<span className="text-sm text-gray-300">Storniert</span>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{selectedEvent && (
|
||
<div
|
||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||
onClick={() => setSelectedEvent(null)}
|
||
>
|
||
<div
|
||
className="bg-gray-800 rounded-lg p-6 max-w-lg w-full mx-4 shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-xl font-bold text-white">Buchungsdetails</h3>
|
||
<button
|
||
onClick={() => setSelectedEvent(null)}
|
||
className="text-gray-400 hover:text-white text-2xl"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-3 text-gray-300">
|
||
<div>
|
||
<span className="font-semibold text-white">Kunde:</span>{' '}
|
||
{selectedEvent.resource.customerName}
|
||
</div>
|
||
<div>
|
||
<span className="font-semibold text-white">E-Mail:</span>{' '}
|
||
{selectedEvent.resource.customerEmail}
|
||
</div>
|
||
<div>
|
||
<span className="font-semibold text-white">Standort:</span>{' '}
|
||
{selectedEvent.resource.locationName}
|
||
</div>
|
||
<div>
|
||
<span className="font-semibold text-white">Fotobox:</span>{' '}
|
||
{selectedEvent.resource.photoboxName}
|
||
</div>
|
||
<div>
|
||
<span className="font-semibold text-white">Event-Typ:</span>{' '}
|
||
{selectedEvent.resource.eventType}
|
||
</div>
|
||
<div>
|
||
<span className="font-semibold text-white">Status:</span>{' '}
|
||
<span
|
||
className={`px-2 py-1 rounded text-sm ${
|
||
selectedEvent.resource.status === 'PENDING'
|
||
? 'bg-amber-500/20 text-amber-300'
|
||
: selectedEvent.resource.status === 'RESERVED'
|
||
? 'bg-blue-500/20 text-blue-300'
|
||
: selectedEvent.resource.status === 'CONFIRMED'
|
||
? 'bg-green-500/20 text-green-300'
|
||
: selectedEvent.resource.status === 'TOUR_CREATED'
|
||
? 'bg-purple-500/20 text-purple-300'
|
||
: selectedEvent.resource.status === 'COMPLETED'
|
||
? 'bg-gray-500/20 text-gray-300'
|
||
: 'bg-red-500/20 text-red-300'
|
||
}`}
|
||
>
|
||
{selectedEvent.resource.status}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span className="font-semibold text-white">Datum:</span>{' '}
|
||
{format(selectedEvent.start, 'PPP', { locale: de })}
|
||
</div>
|
||
<div>
|
||
<span className="font-semibold text-white">Zeit:</span>{' '}
|
||
{format(selectedEvent.start, 'HH:mm', { locale: de })} -{' '}
|
||
{format(selectedEvent.end, 'HH:mm', { locale: de })}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 flex gap-3">
|
||
<a
|
||
href={`/dashboard/bookings?id=${selectedEvent.id}`}
|
||
className="flex-1 px-4 py-2 bg-red-600 hover:bg-red-500 text-white rounded-lg text-center font-medium transition-colors"
|
||
>
|
||
Buchung öffnen
|
||
</a>
|
||
{selectedEvent.resource.tourId && (
|
||
<a
|
||
href={`/dashboard/tours?id=${selectedEvent.resource.tourId}`}
|
||
className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded-lg text-center font-medium transition-colors"
|
||
>
|
||
Tour anzeigen
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
|
||
<style jsx global>{`
|
||
.calendar-container .rbc-calendar {
|
||
background: transparent;
|
||
color: white;
|
||
}
|
||
.rbc-header {
|
||
padding: 12px 4px;
|
||
font-weight: 600;
|
||
color: white;
|
||
background: rgba(31, 41, 55, 0.5);
|
||
border-color: rgba(75, 85, 99, 0.5) !important;
|
||
}
|
||
.rbc-month-view,
|
||
.rbc-time-view {
|
||
background: rgba(31, 41, 55, 0.3);
|
||
border-color: rgba(75, 85, 99, 0.5) !important;
|
||
}
|
||
.rbc-day-bg,
|
||
.rbc-time-slot {
|
||
border-color: rgba(75, 85, 99, 0.5) !important;
|
||
}
|
||
.rbc-today {
|
||
background-color: rgba(239, 68, 68, 0.1) !important;
|
||
}
|
||
.rbc-off-range-bg {
|
||
background: rgba(17, 24, 39, 0.5) !important;
|
||
}
|
||
.rbc-date-cell {
|
||
padding: 6px;
|
||
color: #d1d5db;
|
||
}
|
||
.rbc-now .rbc-button-link {
|
||
color: #ef4444;
|
||
font-weight: 700;
|
||
}
|
||
.rbc-event {
|
||
padding: 2px 5px;
|
||
font-size: 0.875rem;
|
||
cursor: pointer;
|
||
}
|
||
.rbc-event:hover {
|
||
opacity: 1 !important;
|
||
}
|
||
.rbc-time-content {
|
||
border-color: rgba(75, 85, 99, 0.5) !important;
|
||
}
|
||
.rbc-time-header-content {
|
||
border-color: rgba(75, 85, 99, 0.5) !important;
|
||
}
|
||
.rbc-timeslot-group {
|
||
border-color: rgba(75, 85, 99, 0.5) !important;
|
||
}
|
||
.rbc-current-time-indicator {
|
||
background-color: #ef4444;
|
||
}
|
||
`}</style>
|
||
</div>
|
||
);
|
||
}
|