diff --git a/apps/dispatch/app/_helpers/radioEffect.ts b/apps/dispatch/app/_helpers/radioEffect.ts index ab2a7f88..3698dbdc 100644 --- a/apps/dispatch/app/_helpers/radioEffect.ts +++ b/apps/dispatch/app/_helpers/radioEffect.ts @@ -1,5 +1,5 @@ // Helper function for distortion curve generation -function createDistortionCurve(amount: number): Float32Array { +function createDistortionCurve(amount: number): Float32Array { const k = typeof amount === "number" ? amount : 50; const nSamples = 44100; const curve = new Float32Array(nSamples); diff --git a/apps/hub/app/(app)/_querys/bookings.ts b/apps/hub/app/(app)/_querys/bookings.ts new file mode 100644 index 00000000..fc4c5f19 --- /dev/null +++ b/apps/hub/app/(app)/_querys/bookings.ts @@ -0,0 +1,35 @@ +import { Booking, Prisma, PublicUser, Station } from "@repo/db"; +import axios from "axios"; + +export const getBookingsAPI = async (filter: Prisma.BookingWhereInput) => { + const res = await axios.get< + (Booking & { + Station: Station; + User: PublicUser; + })[] + >("/api/bookings", { + params: { + filter: JSON.stringify(filter), + }, + }); + if (res.status !== 200) { + throw new Error("Failed to fetch stations"); + } + return res.data; +}; + +export const createBookingAPI = async (booking: Prisma.BookingCreateInput) => { + const response = await axios.post("/api/bookings", booking); + if (response.status !== 201) { + throw new Error("Failed to create booking"); + } + return response.data; +}; + +export const deleteBookingAPI = async (bookingId: string) => { + const response = await axios.delete(`/api/bookings/${bookingId}`); + if (!response.status.toString().startsWith("2")) { + throw new Error("Failed to delete booking"); + } + return bookingId; +}; diff --git a/apps/hub/app/_components/BookingButton.tsx b/apps/hub/app/_components/BookingButton.tsx index bee3f695..c34f9a0c 100644 --- a/apps/hub/app/_components/BookingButton.tsx +++ b/apps/hub/app/_components/BookingButton.tsx @@ -3,29 +3,17 @@ import { useState } from "react"; import { CalendarIcon } from "lucide-react"; import { BookingSystem } from "./BookingSystem"; +import { User } from "@repo/db"; interface BookingButtonProps { - currentUser: { - id: string; - emailVerified: boolean; - is_banned: boolean; - permissions: string[]; - }; + currentUser: User; } export const BookingButton = ({ currentUser }: BookingButtonProps) => { const [isBookingSystemOpen, setIsBookingSystemOpen] = useState(false); // Check if user can access booking system - const canAccessBookingSystem = currentUser && currentUser.emailVerified && !currentUser.is_banned; - - const handleOpenBookingSystem = () => { - setIsBookingSystemOpen(true); - }; - - const handleCloseBookingSystem = () => { - setIsBookingSystemOpen(false); - }; + const canAccessBookingSystem = currentUser && currentUser.emailVerified && !currentUser.isBanned; // Don't render the button if user doesn't have access if (!canAccessBookingSystem) { @@ -37,14 +25,14 @@ export const BookingButton = ({ currentUser }: BookingButtonProps) => { setIsBookingSystemOpen(false)} currentUser={currentUser} /> diff --git a/apps/hub/app/_components/BookingSystem.tsx b/apps/hub/app/_components/BookingSystem.tsx index 91798e2a..f24edb92 100644 --- a/apps/hub/app/_components/BookingSystem.tsx +++ b/apps/hub/app/_components/BookingSystem.tsx @@ -3,16 +3,12 @@ import { useState } from "react"; import { BookingTimelineModal } from "./BookingTimelineModal"; import { NewBookingModal } from "./NewBookingModal"; +import { User } from "@repo/db"; interface BookingSystemProps { isOpen: boolean; onClose: () => void; - currentUser: { - id: string; - emailVerified: boolean; - is_banned: boolean; - permissions: string[]; - }; + currentUser: User; } export const BookingSystem = ({ isOpen, onClose, currentUser }: BookingSystemProps) => { diff --git a/apps/hub/app/_components/BookingTimelineModal.tsx b/apps/hub/app/_components/BookingTimelineModal.tsx index 09504d90..1f4002b8 100644 --- a/apps/hub/app/_components/BookingTimelineModal.tsx +++ b/apps/hub/app/_components/BookingTimelineModal.tsx @@ -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([]); - const [loading, setLoading] = useState(false); + const queryClient = useQueryClient(); const [currentDate, setCurrentDate] = useState(new Date()); const [viewMode, setViewMode] = useState("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 = {}; + const groups: Record = {}; - 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 = {}; + const sortedGroups: Record = {}; 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 = {}; + const groups: Record = {}; - 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 = {}; + const sortedGroups: Record = {}; 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 = ({ - {loading ? ( + {isBookingsLoading ? (
@@ -369,29 +298,29 @@ export const BookingTimelineModal = ({ >
- {getStationDisplay(booking)} - - - {formatUserName(booking.user)} + {booking.type.startsWith("LST_") + ? "" + : booking.Station.bosCallsignShort || booking.Station.bosCallsign} + {booking.User.fullName}

{formatTimeRange(booking)}

- {canDeleteBooking(booking.user.id) && ( - + )}
@@ -400,7 +329,7 @@ export const BookingTimelineModal = ({ ))} - {Object.keys(groupedBookings).length === 0 && !loading && ( + {Object.keys(groupedBookings).length === 0 && !isBookingsLoading && (
Keine Buchungen im aktuellen Zeitraum gefunden
diff --git a/apps/hub/app/_components/Nav.tsx b/apps/hub/app/_components/Nav.tsx index 172b38e1..bd5f29fd 100644 --- a/apps/hub/app/_components/Nav.tsx +++ b/apps/hub/app/_components/Nav.tsx @@ -136,14 +136,7 @@ export const HorizontalNav = async () => {