|
|
|
|
@@ -1,44 +1,17 @@
|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
import { CalendarIcon, Plus, X, ChevronLeft, ChevronRight, Trash2 } from "lucide-react";
|
|
|
|
|
import toast from "react-hot-toast";
|
|
|
|
|
|
|
|
|
|
interface BookingUser {
|
|
|
|
|
id: string;
|
|
|
|
|
firstname: string;
|
|
|
|
|
lastname: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface BookingStation {
|
|
|
|
|
id: number;
|
|
|
|
|
bosCallsign: string;
|
|
|
|
|
bosCallsignShort: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Booking {
|
|
|
|
|
id: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
type: "STATION" | "LST_01" | "LST_02" | "LST_03" | "LST_04";
|
|
|
|
|
stationId?: number;
|
|
|
|
|
startTime: string;
|
|
|
|
|
endTime: string;
|
|
|
|
|
title?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
user: BookingUser;
|
|
|
|
|
station?: BookingStation;
|
|
|
|
|
}
|
|
|
|
|
import { Booking, getPublicUser, 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";
|
|
|
|
|
|
|
|
|
|
interface BookingTimelineModalProps {
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onOpenNewBooking: () => void;
|
|
|
|
|
currentUser: {
|
|
|
|
|
id: string;
|
|
|
|
|
emailVerified: boolean;
|
|
|
|
|
is_banned: boolean;
|
|
|
|
|
permissions: string[];
|
|
|
|
|
};
|
|
|
|
|
currentUser: User;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ViewMode = "day" | "week" | "month";
|
|
|
|
|
@@ -49,71 +22,9 @@ export const BookingTimelineModal = ({
|
|
|
|
|
onOpenNewBooking,
|
|
|
|
|
currentUser,
|
|
|
|
|
}: BookingTimelineModalProps) => {
|
|
|
|
|
const [bookings, setBookings] = useState<Booking[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const [currentDate, setCurrentDate] = useState(new Date());
|
|
|
|
|
const [viewMode, setViewMode] = useState<ViewMode>("day");
|
|
|
|
|
|
|
|
|
|
// Check if user can create bookings
|
|
|
|
|
const canCreateBookings =
|
|
|
|
|
currentUser &&
|
|
|
|
|
currentUser.emailVerified &&
|
|
|
|
|
!currentUser.is_banned &&
|
|
|
|
|
(currentUser.permissions.includes("PILOT") || currentUser.permissions.includes("DISPO"));
|
|
|
|
|
|
|
|
|
|
// Check if user can delete a booking
|
|
|
|
|
const canDeleteBooking = (bookingUserId: string) => {
|
|
|
|
|
if (!currentUser) return false;
|
|
|
|
|
|
|
|
|
|
// User can delete their own bookings if they meet basic requirements
|
|
|
|
|
if (bookingUserId === currentUser.id && currentUser.emailVerified && !currentUser.is_banned) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Admins can delete any booking
|
|
|
|
|
return currentUser.permissions.includes("ADMIN_KICK");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deleteBooking = async (bookingId: string) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/booking/${bookingId}`, {
|
|
|
|
|
method: "DELETE",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error("Failed to delete booking");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh the bookings
|
|
|
|
|
fetchBookings();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error deleting booking:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchBookings = useCallback(async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch("/api/booking");
|
|
|
|
|
if (!response.ok) throw new Error("Failed to fetch bookings");
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
// API returns { bookings: [] }, so we need to access the bookings property
|
|
|
|
|
setBookings(data.bookings || []);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error fetching bookings:", error);
|
|
|
|
|
toast.error("Fehler beim Laden der Buchungen");
|
|
|
|
|
setBookings([]); // Set empty array on error
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
fetchBookings();
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, fetchBookings]);
|
|
|
|
|
|
|
|
|
|
const getTimeRange = () => {
|
|
|
|
|
const start = new Date(currentDate);
|
|
|
|
|
const end = new Date(currentDate);
|
|
|
|
|
@@ -144,6 +55,51 @@ export const BookingTimelineModal = ({
|
|
|
|
|
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"] });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
// User can delete their own bookings if they meet basic requirements
|
|
|
|
|
if (
|
|
|
|
|
bookingUserPublicId === currentUser.publicId &&
|
|
|
|
|
currentUser.emailVerified &&
|
|
|
|
|
!currentUser.isBanned
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Admins can delete any booking
|
|
|
|
|
return currentUser.permissions.includes("ADMIN_KICK");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const navigate = (direction: "prev" | "next") => {
|
|
|
|
|
const newDate = new Date(currentDate);
|
|
|
|
|
|
|
|
|
|
@@ -162,21 +118,6 @@ export const BookingTimelineModal = ({
|
|
|
|
|
setCurrentDate(newDate);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getFilteredBookings = () => {
|
|
|
|
|
// Ensure bookings is an array before filtering
|
|
|
|
|
if (!Array.isArray(bookings)) {
|
|
|
|
|
console.warn("Bookings is not an array:", bookings);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { start, end } = getTimeRange();
|
|
|
|
|
return bookings.filter((booking) => {
|
|
|
|
|
const bookingStart = new Date(booking.startTime);
|
|
|
|
|
const bookingEnd = new Date(booking.endTime);
|
|
|
|
|
return bookingStart <= end && bookingEnd >= start;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatDateRange = () => {
|
|
|
|
|
const { start, end } = getTimeRange();
|
|
|
|
|
const options: Intl.DateTimeFormatOptions = {
|
|
|
|
|
@@ -196,16 +137,15 @@ export const BookingTimelineModal = ({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const groupBookingsByResource = () => {
|
|
|
|
|
const filteredBookings = getFilteredBookings();
|
|
|
|
|
|
|
|
|
|
if (!bookings) return {};
|
|
|
|
|
// For day view, group by resource (station/type)
|
|
|
|
|
if (viewMode === "day") {
|
|
|
|
|
const groups: Record<string, Booking[]> = {};
|
|
|
|
|
const groups: Record<string, (Booking & { Station: Station; User: PublicUser })[]> = {};
|
|
|
|
|
|
|
|
|
|
filteredBookings.forEach((booking) => {
|
|
|
|
|
bookings.forEach((booking) => {
|
|
|
|
|
let key: string = booking.type;
|
|
|
|
|
if (booking.station) {
|
|
|
|
|
key = `${booking.station.bosCallsign}`;
|
|
|
|
|
if (booking.Station) {
|
|
|
|
|
key = `${booking.Station.bosCallsign}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!groups[key]) {
|
|
|
|
|
@@ -227,7 +167,7 @@ export const BookingTimelineModal = ({
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Sort the groups themselves - LST_ types first, then alphabetical
|
|
|
|
|
const sortedGroups: Record<string, Booking[]> = {};
|
|
|
|
|
const sortedGroups: Record<string, (Booking & { Station: Station; User: PublicUser })[]> = {};
|
|
|
|
|
Object.keys(groups)
|
|
|
|
|
.sort((a, b) => {
|
|
|
|
|
// Check if groups contain LST_ types
|
|
|
|
|
@@ -246,9 +186,9 @@ export const BookingTimelineModal = ({
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For week and month views, group by date
|
|
|
|
|
const groups: Record<string, Booking[]> = {};
|
|
|
|
|
const groups: Record<string, (Booking & { Station: Station; User: PublicUser })[]> = {};
|
|
|
|
|
|
|
|
|
|
filteredBookings.forEach((booking) => {
|
|
|
|
|
bookings.forEach((booking) => {
|
|
|
|
|
const dateKey = new Date(booking.startTime).toLocaleDateString("de-DE", {
|
|
|
|
|
weekday: "long",
|
|
|
|
|
day: "2-digit",
|
|
|
|
|
@@ -263,7 +203,7 @@ export const BookingTimelineModal = ({
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Sort groups by date for week/month view and sort bookings within each group
|
|
|
|
|
const sortedGroups: Record<string, Booking[]> = {};
|
|
|
|
|
const sortedGroups: Record<string, (Booking & { Station: Station; User: PublicUser })[]> = {};
|
|
|
|
|
Object.keys(groups)
|
|
|
|
|
.sort((a, b) => {
|
|
|
|
|
// Extract date from the formatted string and compare
|
|
|
|
|
@@ -290,23 +230,12 @@ export const BookingTimelineModal = ({
|
|
|
|
|
return sortedGroups;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatUserName = (user: BookingUser) => {
|
|
|
|
|
return `${user.firstname} ${user.lastname.charAt(0)}.`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatTimeRange = (booking: Booking) => {
|
|
|
|
|
const start = new Date(booking.startTime);
|
|
|
|
|
const end = new Date(booking.endTime);
|
|
|
|
|
return `${start.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} - ${end.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getStationDisplay = (booking: Booking) => {
|
|
|
|
|
if (booking.station) {
|
|
|
|
|
return booking.station.bosCallsignShort || booking.station.bosCallsign;
|
|
|
|
|
}
|
|
|
|
|
return booking.type;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
|
|
|
|
const groupedBookings = groupBookingsByResource();
|
|
|
|
|
@@ -349,7 +278,7 @@ export const BookingTimelineModal = ({
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
{isBookingsLoading ? (
|
|
|
|
|
<div className="flex justify-center py-8">
|
|
|
|
|
<span className="loading loading-spinner loading-lg"></span>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -369,29 +298,29 @@ export const BookingTimelineModal = ({
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<span className="badge badge-outline text-xs">
|
|
|
|
|
{getStationDisplay(booking)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-sm font-medium">
|
|
|
|
|
{formatUserName(booking.user)}
|
|
|
|
|
{booking.type.startsWith("LST_")
|
|
|
|
|
? ""
|
|
|
|
|
: booking.Station.bosCallsignShort || booking.Station.bosCallsign}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-sm font-medium">{booking.User.fullName}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<div className="text-right">
|
|
|
|
|
<p className="text-xs font-medium">{formatTimeRange(booking)}</p>
|
|
|
|
|
</div>
|
|
|
|
|
{canDeleteBooking(booking.user.id) && (
|
|
|
|
|
<button
|
|
|
|
|
{canDeleteBooking(booking.User.publicId) && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => deleteBooking(booking.id)}
|
|
|
|
|
className={`btn btn-xs ${
|
|
|
|
|
currentUser?.permissions.includes("ADMIN_KICK") &&
|
|
|
|
|
booking.user.id !== currentUser.id
|
|
|
|
|
currentUser?.permissions.includes("ADMIN_EVENT") &&
|
|
|
|
|
booking.User.publicId !== currentUser.publicId
|
|
|
|
|
? "btn-error"
|
|
|
|
|
: "btn-neutral"
|
|
|
|
|
}`}
|
|
|
|
|
title="Buchung löschen"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 size={12} />
|
|
|
|
|
</button>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -400,7 +329,7 @@ export const BookingTimelineModal = ({
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
{Object.keys(groupedBookings).length === 0 && !loading && (
|
|
|
|
|
{Object.keys(groupedBookings).length === 0 && !isBookingsLoading && (
|
|
|
|
|
<div className="col-span-full py-8 text-center opacity-70">
|
|
|
|
|
Keine Buchungen im aktuellen Zeitraum gefunden
|
|
|
|
|
</div>
|
|
|
|
|
|