/** * GET /api/calendars/[id]/reservations - Reservierungen eines Kalenders. * POST /api/calendars/[id]/reservations - neue Reservierung (atomar). * * Berechtigungen werden serverseitig ueber die zentrale * Berechtigungsschicht erzwungen (plan.md Abschnitt 2 und 20). * * Sichtbarkeiten: Titel und Notizen fremder Reservierungen werden * fuer Nicht-Admins gemaess der Kalender-Einstellungen geschwaerzt * (show_title_to_members / show_notes_to_members). */ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db/client'; import { requireCalendarContext } from '@/lib/api/context'; import { createReservationSchema } from '@/lib/validation'; import { canCreateReservation } from '@/lib/permissions/permissions'; import { createReservationAtomically } from '@/lib/reservations/service'; import { errorResponse, forbiddenResponse } from '@/lib/api/responses'; import { displayName } from '@/lib/privacy/display'; interface RouteParams { params: Promise<{ id: string }>; } export async function GET( request: NextRequest, { params }: RouteParams, ): Promise { try { const { id } = await params; const context = await requireCalendarContext(id); // Optionales Zeitfenster-Filter (Monat/Woche/Tag-Ansicht). // Overlap-Bedingung (startAt < to UND endAt > from) statt Enthaltensein: // Eine Reservierung muss zurueckgegeben werden, wenn sie den // Zeitraum auch nur teilweise ueberdeckt (z. B. Mehrtages-Reservierung, // die vor dem Fenster beginnt oder danach endet). const from = request.nextUrl.searchParams.get('from'); const to = request.nextUrl.searchParams.get('to'); const reservations = await prisma.reservation.findMany({ where: { calendarId: id, status: 'CONFIRMED', ...(from && to ? { startAt: { lt: new Date(to) }, endAt: { gt: new Date(from) } } : {}), }, orderBy: { startAt: 'asc' }, select: { id: true, userId: true, title: true, startAt: true, endAt: true, status: true, notes: true, user: { select: { email: true, username: true } }, }, }); const showForeignTitles = context.user.role === 'ADMIN' || context.calendar.showTitleToMembers; const showForeignNotes = context.user.role === 'ADMIN' || context.calendar.showNotesToMembers; return NextResponse.json({ reservations: reservations.map((reservation) => { const isOwn = reservation.userId === context.user.id; return { id: reservation.id, userId: reservation.userId, title: isOwn || showForeignTitles ? reservation.title : 'Gebucht', startAt: reservation.startAt, endAt: reservation.endAt, status: reservation.status, notes: isOwn || showForeignNotes ? reservation.notes : null, // Datenschutz: Keine vollstaendigen E-Mail-Adressen an // Mitglieder; nur Username oder maskierte E-Mail. ownerName: displayName( reservation.user.username, reservation.user.email, ), }; }), }); } catch (error) { return errorResponse(error); } } export async function POST( request: NextRequest, { params }: RouteParams, ): Promise { try { const { id } = await params; const context = await requireCalendarContext(id); if (!canCreateReservation(context.user, context.calendar, context.isMember)) { return forbiddenResponse(); } const body = createReservationSchema.parse(await request.json()); // Serverseitige, atomare Pruefung (plan.md Abschnitt 6 und 15): // Kalender gueltig? Benutzer berechtigt? Max-Dauer? Zeit verfuegbar? const reservation = await createReservationAtomically({ calendarId: id, userId: context.user.id, title: body.title, startAt: body.startAt, endAt: body.endAt, notes: body.notes ?? null, }); return NextResponse.json({ reservation }, { status: 201 }); } catch (error) { return errorResponse(error); } }