"use client"; import { useState } from "react"; import { CalendarIcon, Plus, X, ChevronLeft, ChevronRight, Trash2 } from "lucide-react"; import { Booking, PublicUser, Station, User } from "@repo/db"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { deleteBookingAPI, getBookingsAPI } from "(app)/_querys/bookings"; import { Button } from "@repo/shared-components"; import { formatTimeRange } from "../../helper/timerange"; import toast from "react-hot-toast"; import Link from "next/link"; interface BookingTimelineModalProps { isOpen: boolean; onClose: () => void; onOpenNewBooking: () => void; currentUser: User; } type ViewMode = "day" | "week" | "month"; export const BookingTimelineModal = ({ isOpen, onClose, onOpenNewBooking, currentUser, }: BookingTimelineModalProps) => { const queryClient = useQueryClient(); const [currentDate, setCurrentDate] = useState(new Date()); const [viewMode, setViewMode] = useState("day"); const getTimeRange = () => { const start = new Date(currentDate); const end = new Date(currentDate); switch (viewMode) { case "day": start.setHours(0, 0, 0, 0); end.setHours(23, 59, 59, 999); break; case "week": { const dayOfWeek = start.getDay(); const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; start.setDate(start.getDate() + mondayOffset); start.setHours(0, 0, 0, 0); end.setDate(start.getDate() + 6); end.setHours(23, 59, 59, 999); break; } case "month": start.setDate(1); start.setHours(0, 0, 0, 0); end.setMonth(end.getMonth() + 1); end.setDate(0); end.setHours(23, 59, 59, 999); break; } return { start, end }; }; const { data: bookings, isLoading: isBookingsLoading } = useQuery({ queryKey: ["bookings", getTimeRange().start, getTimeRange().end], queryFn: () => getBookingsAPI({ startTime: { gte: getTimeRange().start, }, endTime: { lte: getTimeRange().end, }, }), }); const { mutate: deleteBooking } = useMutation({ mutationKey: ["deleteBooking"], mutationFn: async (bookingId: string) => { await deleteBookingAPI(bookingId); queryClient.invalidateQueries({ queryKey: ["bookings"] }); }, onSuccess: () => { toast.success("Buchung erfolgreich gelöscht"); }, }); // Check if user can create bookings const canCreateBookings = currentUser && currentUser.emailVerified && !currentUser.isBanned && (currentUser.permissions.includes("PILOT") || currentUser.permissions.includes("DISPO")); // Check if user can delete a booking const canDeleteBooking = (bookingUserPublicId: string) => { if (!currentUser) return false; if (currentUser.permissions.includes("ADMIN_BOOKING")) return true; // User can delete their own bookings if they meet basic requirements if (bookingUserPublicId === currentUser.publicId) { return true; } // Admins can delete any booking return false; }; const navigate = (direction: "prev" | "next") => { const newDate = new Date(currentDate); switch (viewMode) { case "day": newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1)); break; case "week": newDate.setDate(newDate.getDate() + (direction === "next" ? 7 : -7)); break; case "month": newDate.setMonth(newDate.getMonth() + (direction === "next" ? 1 : -1)); break; } setCurrentDate(newDate); }; const formatDateRange = () => { const { start, end } = getTimeRange(); const options: Intl.DateTimeFormatOptions = { day: "2-digit", month: "2-digit", year: "numeric", }; switch (viewMode) { case "day": return start.toLocaleDateString("de-DE", options); case "week": return `${start.toLocaleDateString("de-DE", options)} - ${end.toLocaleDateString("de-DE", options)}`; case "month": return start.toLocaleDateString("de-DE", { month: "long", year: "numeric" }); } }; const groupBookingsByResource = () => { if (!bookings) return {}; // For day view, group by resource (station/type) if (viewMode === "day") { const groups: Record = {}; bookings.forEach((booking) => { let key: string = booking.type; if (booking.Station) { key = `${booking.Station.bosCallsign}`; } if (!groups[key]) { groups[key] = []; } groups[key]!.push(booking); }); // Sort bookings within each group, LST_ types first, then alphanumerical Object.keys(groups).forEach((key) => { groups[key] = groups[key]!.sort((a, b) => { const aIsLST = a.type.startsWith("LST_"); const bIsLST = b.type.startsWith("LST_"); if (aIsLST && !bIsLST) return -1; if (!aIsLST && bIsLST) return 1; // Within same category (both LST_ or both non-LST_), sort alphanumerically by type return a.type.localeCompare(b.type); }); }); // Sort the groups themselves - LST_ types first, then alphabetical const sortedGroups: Record = {}; Object.keys(groups) .sort((a, b) => { // Check if groups contain LST_ types const aHasLST = groups[a]?.some((booking) => booking.type.startsWith("LST_")); const bHasLST = groups[b]?.some((booking) => booking.type.startsWith("LST_")); if (aHasLST && !bHasLST) return -1; if (!aHasLST && bHasLST) return 1; // Within same category, sort alphabetically by group name return a.localeCompare(b); }) .forEach((key) => { sortedGroups[key] = groups[key]!; }); return sortedGroups; } // For week and month views, group by date const groups: Record = {}; bookings.forEach((booking) => { const dateKey = new Date(booking.startTime).toLocaleDateString("de-DE", { weekday: "long", day: "2-digit", month: "2-digit", year: "numeric", }); if (!groups[dateKey]) { groups[dateKey] = []; } groups[dateKey]!.push(booking); }); // Sort groups by date for week/month view and sort bookings within each group const sortedGroups: Record = {}; Object.keys(groups) .sort((a, b) => { // Extract date from the formatted string and compare const dateA = groups[a]?.[0]?.startTime; const dateB = groups[b]?.[0]?.startTime; if (!dateA || !dateB) return 0; return new Date(dateA).getTime() - new Date(dateB).getTime(); }) .forEach((key) => { const bookingsForKey = groups[key]; if (bookingsForKey) { // Sort bookings within each date group, LST_ types first, then alphanumerical sortedGroups[key] = bookingsForKey.sort((a, b) => { const aIsLST = a.type.startsWith("LST_"); const bIsLST = b.type.startsWith("LST_"); if (aIsLST && !bIsLST) return -1; if (!aIsLST && bIsLST) return 1; // Within same category (both LST_ or both non-LST_), sort alphanumerically by type return a.type.localeCompare(b.type); }); } }); return sortedGroups; }; if (!isOpen) return null; const groupedBookings = groupBookingsByResource(); return (

Slot Buchung

{/* Controls */}
{formatDateRange()}
{(["day", "week", "month"] as ViewMode[]).map((mode) => ( ))}
{isBookingsLoading ? (
) : (
{Object.entries(groupedBookings).map(([groupName, resourceBookings]) => (

{viewMode === "day" ? groupName : groupName}

{resourceBookings.map((booking) => (
{booking.type.startsWith("LST_") ? "LST" : booking.Station.bosCallsignShort || booking.Station.bosCallsign} {currentUser?.permissions.includes("ADMIN_USER") ? ( {booking.User.fullName} ) : ( {booking.User.fullName} )}

{formatTimeRange(booking)}

{canDeleteBooking(booking.User.publicId) && ( )}
))}
))} {Object.keys(groupedBookings).length === 0 && !isBookingsLoading && (
Keine Buchungen im aktuellen Zeitraum gefunden
)}
)}
{canCreateBookings && ( )}
); };