Release v2.0.4 #140
@@ -1,5 +1,5 @@
|
||||
// Helper function for distortion curve generation
|
||||
function createDistortionCurve(amount: number): Float32Array {
|
||||
function createDistortionCurve(amount: number): Float32Array<ArrayBuffer> {
|
||||
const k = typeof amount === "number" ? amount : 50;
|
||||
const nSamples = 44100;
|
||||
const curve = new Float32Array(nSamples);
|
||||
|
||||
35
apps/hub/app/(app)/_querys/bookings.ts
Normal file
35
apps/hub/app/(app)/_querys/bookings.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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) => {
|
||||
<button
|
||||
className="btn btn-sm btn-ghost tooltip tooltip-bottom"
|
||||
data-tip="Slot Buchung"
|
||||
onClick={handleOpenBookingSystem}
|
||||
onClick={() => setIsBookingSystemOpen(true)}
|
||||
>
|
||||
<CalendarIcon size={20} />
|
||||
</button>
|
||||
|
||||
<BookingSystem
|
||||
isOpen={isBookingSystemOpen}
|
||||
onClose={handleCloseBookingSystem}
|
||||
onClose={() => setIsBookingSystemOpen(false)}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -136,14 +136,7 @@ export const HorizontalNav = async () => {
|
||||
<div className="ml-auto flex items-center">
|
||||
<ul className="flex items-center space-x-2 px-1">
|
||||
<li>
|
||||
<BookingButton
|
||||
currentUser={{
|
||||
id: session.user.id,
|
||||
emailVerified: session.user.emailVerified,
|
||||
is_banned: session.user.isBanned,
|
||||
permissions: session.user.permissions,
|
||||
}}
|
||||
/>
|
||||
<BookingButton currentUser={session?.user} />
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getServerSession } from "../../auth/[...nextauth]/auth";
|
||||
// DELETE /api/booking/[id] - Delete a booking
|
||||
export const DELETE = async (req: NextRequest, { params }: { params: { id: string } }) => {
|
||||
try {
|
||||
console.log(params);
|
||||
const session = await getServerSession();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
||||
@@ -117,14 +118,8 @@ export const PUT = async (req: NextRequest, { params }: { params: { id: string }
|
||||
endTime: new Date(endTime),
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstname: true,
|
||||
lastname: true,
|
||||
},
|
||||
},
|
||||
station: {
|
||||
User: true,
|
||||
Station: {
|
||||
select: {
|
||||
id: true,
|
||||
bosCallsignShort: true,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@repo/db";
|
||||
import { getPublicUser, prisma } from "@repo/db";
|
||||
import { getServerSession } from "../auth/[...nextauth]/auth";
|
||||
|
||||
// GET /api/booking - Get all bookings for the timeline
|
||||
@@ -10,47 +10,14 @@ export const GET = async (req: NextRequest) => {
|
||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const startDate = searchParams.get("startDate");
|
||||
const endDate = searchParams.get("endDate");
|
||||
|
||||
const whereClause: Record<string, unknown> = {};
|
||||
|
||||
// Filter by date range if provided
|
||||
if (startDate && endDate) {
|
||||
whereClause.OR = [
|
||||
{
|
||||
startTime: {
|
||||
gte: new Date(startDate),
|
||||
lte: new Date(endDate),
|
||||
},
|
||||
},
|
||||
{
|
||||
endTime: {
|
||||
gte: new Date(startDate),
|
||||
lte: new Date(endDate),
|
||||
},
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{ startTime: { lte: new Date(startDate) } },
|
||||
{ endTime: { gte: new Date(endDate) } },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
const { searchParams } = req.nextUrl;
|
||||
const filter = JSON.parse(searchParams.get("filter") || "{}");
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: whereClause,
|
||||
where: filter,
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstname: true,
|
||||
lastname: true,
|
||||
},
|
||||
},
|
||||
station: {
|
||||
User: true,
|
||||
Station: {
|
||||
select: {
|
||||
id: true,
|
||||
bosCallsign: true,
|
||||
@@ -63,7 +30,12 @@ export const GET = async (req: NextRequest) => {
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ bookings });
|
||||
return NextResponse.json(
|
||||
bookings.map((b) => ({
|
||||
...b,
|
||||
user: b.User && getPublicUser(b.User),
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching bookings:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
@@ -154,14 +126,14 @@ export const POST = async (req: NextRequest) => {
|
||||
endTime: new Date(endTime),
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
User: {
|
||||
select: {
|
||||
id: true,
|
||||
firstname: true,
|
||||
lastname: true,
|
||||
},
|
||||
},
|
||||
station: {
|
||||
Station: {
|
||||
select: {
|
||||
id: true,
|
||||
bosCallsign: true,
|
||||
@@ -171,7 +143,7 @@ export const POST = async (req: NextRequest) => {
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ booking }, { status: 201 });
|
||||
return NextResponse.json(booking, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Error creating booking:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
@@ -17,8 +17,8 @@ model Booking {
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
station Station? @relation(fields: [stationId], references: [id], onDelete: Cascade)
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
Station Station? @relation(fields: [stationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([startTime, endTime])
|
||||
@@index([type, startTime, endTime])
|
||||
|
||||
Reference in New Issue
Block a user