Initial commit - SaveTheMoment Atlas Basis-Setup

This commit is contained in:
Dennis Forte
2025-11-12 20:21:32 +01:00
commit 0b6e429329
167 changed files with 30843 additions and 0 deletions

View File

@@ -0,0 +1,410 @@
'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>
);
}