Initial commit - SaveTheMoment Atlas Basis-Setup

This commit is contained in:
Dennis Forte
2025-11-12 20:21:32 +01:00
commit 0b6e429329
167 changed files with 30843 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { aiService } from '@/lib/ai-service';
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const booking = await prisma.booking.findUnique({
where: { id: params.id },
include: {
emails: {
orderBy: { receivedAt: 'desc' },
take: 1,
},
},
});
if (!booking) {
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
}
if (!booking.emails || booking.emails.length === 0) {
return NextResponse.json({ error: 'Keine E-Mail gefunden' }, { status: 400 });
}
const email = booking.emails[0];
const result = await aiService.parseBookingEmail(
email.htmlBody || email.textBody || '',
email.subject
);
await prisma.booking.update({
where: { id: params.id },
data: {
aiParsed: true,
aiResponseDraft: result.responseDraft,
aiProcessedAt: new Date(),
customerName: result.parsed.customerName || booking.customerName,
customerEmail: result.parsed.customerEmail || booking.customerEmail,
customerPhone: result.parsed.customerPhone || booking.customerPhone,
customerAddress: result.parsed.customerAddress,
customerCity: result.parsed.customerCity,
customerZip: result.parsed.customerZip,
companyName: result.parsed.companyName,
invoiceType: result.parsed.invoiceType,
eventAddress: result.parsed.eventAddress || booking.eventAddress,
eventCity: result.parsed.eventCity || booking.eventCity,
eventZip: result.parsed.eventZip || booking.eventZip,
eventLocation: result.parsed.eventLocation,
eventDate: new Date(result.parsed.eventDate || booking.eventDate),
setupTimeStart: new Date(result.parsed.setupTimeStart || booking.setupTimeStart),
},
});
return NextResponse.json({
success: true,
parsed: result.parsed,
responseDraft: result.responseDraft,
confidence: result.confidence,
});
} catch (error) {
console.error('AI Analysis error:', error);
return NextResponse.json(
{ error: 'KI-Analyse fehlgeschlagen' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,82 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
function generateTourNumber(): string {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
return `T${year}${month}${day}-${random}`;
}
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { driverId } = await request.json();
if (!driverId) {
return NextResponse.json({ error: 'Fahrer-ID erforderlich' }, { status: 400 });
}
const booking = await prisma.booking.findUnique({
where: { id: params.id },
});
if (!booking) {
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
}
if (booking.status !== 'OPEN_FOR_DRIVERS') {
return NextResponse.json({ error: 'Buchung ist nicht für Zuweisung bereit' }, { status: 400 });
}
let tour = await prisma.tour.findFirst({
where: {
tourDate: booking.eventDate,
driverId: driverId,
status: 'PLANNED',
},
});
if (!tour) {
tour = await prisma.tour.create({
data: {
tourDate: booking.eventDate,
tourNumber: generateTourNumber(),
driverId: driverId,
status: 'PLANNED',
},
});
}
await prisma.booking.update({
where: { id: params.id },
data: {
tourId: tour.id,
status: 'ASSIGNED',
},
});
return NextResponse.json({
success: true,
tour,
message: `Buchung wurde Tour ${tour.tourNumber} zugewiesen`,
});
} catch (error) {
console.error('Assign driver error:', error);
return NextResponse.json(
{ error: 'Zuweisung fehlgeschlagen' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,99 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { available, message } = await request.json();
const booking = await prisma.booking.findUnique({
where: { id: params.id },
});
if (!booking) {
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
}
if (booking.status !== 'OPEN_FOR_DRIVERS') {
return NextResponse.json({ error: 'Buchung ist nicht für Fahrer freigegeben' }, { status: 400 });
}
const availability = await prisma.driverAvailability.upsert({
where: {
bookingId_driverId: {
bookingId: params.id,
driverId: session.user.id,
},
},
update: {
available,
message,
},
create: {
bookingId: params.id,
driverId: session.user.id,
available,
message,
},
});
return NextResponse.json({ success: true, availability });
} catch (error) {
console.error('Availability update error:', error);
return NextResponse.json(
{ error: 'Verfügbarkeit konnte nicht gespeichert werden' },
{ status: 500 }
);
}
}
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const availabilities = await prisma.driverAvailability.findMany({
where: {
bookingId: params.id,
available: true,
},
include: {
driver: {
select: {
id: true,
name: true,
email: true,
phoneNumber: true,
vehiclePlate: true,
vehicleModel: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
});
return NextResponse.json({ availabilities });
} catch (error) {
console.error('Get availability error:', error);
return NextResponse.json(
{ error: 'Fehler beim Laden der Verfügbarkeiten' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { generateContractPDF, generateSignedContractPDF } from '@/lib/pdf-service';
import { writeFile, mkdir } from 'fs/promises';
import path from 'path';
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = params;
const booking = await prisma.booking.findUnique({
where: { id },
include: {
location: true,
photobox: true,
},
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
// Generate PDF
const pdfBuffer = await generateContractPDF(booking, booking.location, booking.photobox);
// Save PDF to public/contracts folder
const contractsDir = path.join(process.cwd(), 'public', 'contracts');
await mkdir(contractsDir, { recursive: true });
const filename = `contract-${booking.bookingNumber}.pdf`;
const filepath = path.join(contractsDir, filename);
await writeFile(filepath, pdfBuffer);
const contractUrl = `/contracts/${filename}`;
// Update booking
await prisma.booking.update({
where: { id },
data: {
contractGenerated: true,
contractGeneratedAt: new Date(),
contractPdfUrl: contractUrl,
},
});
return NextResponse.json({
success: true,
contractUrl,
filename,
});
} catch (error: any) {
console.error('Contract generation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to generate contract' },
{ status: 500 }
);
}
}
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const { id } = params;
const booking = await prisma.booking.findUnique({
where: { id },
include: {
location: true,
photobox: true,
},
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
// Generate PDF in memory for download
const pdfBuffer = await generateSignedContractPDF(
booking,
booking.location,
booking.photobox,
booking.contractSignatureData || '',
booking.contractSignedBy || '',
booking.contractSignedAt || new Date(),
booking.contractSignedIp || ''
);
return new NextResponse(new Uint8Array(pdfBuffer), {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="Vertrag-${booking.bookingNumber}.pdf"`,
},
});
} catch (error: any) {
console.error('Contract download error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to download contract' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { sendContractEmail } from '@/lib/email-service';
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = params;
const booking = await prisma.booking.findUnique({
where: { id },
include: {
location: true,
photobox: true,
},
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
if (!booking.contractGenerated || !booking.contractPdfUrl) {
return NextResponse.json(
{ error: 'Contract not generated yet. Please generate contract first.' },
{ status: 400 }
);
}
try {
await sendContractEmail(booking, booking.contractPdfUrl);
await prisma.booking.update({
where: { id },
data: {
contractSentAt: new Date(),
},
});
return NextResponse.json({
success: true,
message: `Contract sent to ${booking.customerEmail}`,
});
} catch (emailError: any) {
console.error('Email send error:', emailError);
if (emailError.message?.includes('SMTP not configured')) {
return NextResponse.json({
success: false,
error: 'E-Mail-Service nicht konfiguriert. Bitte SMTP-Einstellungen in .env hinzufügen.',
}, { status: 503 });
}
return NextResponse.json({
success: false,
error: emailError.message || 'Failed to send email',
}, { status: 500 });
}
} catch (error: any) {
console.error('Contract send error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to send contract' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { writeFile, mkdir } from 'fs/promises';
import path from 'path';
// Note: Google Vision API can be added later for automatic signature detection
// For now, we trust admin verification
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = params;
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}
const booking = await prisma.booking.findUnique({
where: { id },
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
// Save uploaded file
const buffer = Buffer.from(await file.arrayBuffer());
const contractsDir = path.join(process.cwd(), 'public', 'contracts');
await mkdir(contractsDir, { recursive: true });
const filename = `contract-uploaded-${booking.bookingNumber}-${Date.now()}.pdf`;
const filepath = path.join(contractsDir, filename);
await writeFile(filepath, buffer);
const contractUrl = `/contracts/${filename}`;
// Update booking
await prisma.booking.update({
where: { id },
data: {
contractSigned: true,
contractSignedAt: new Date(),
contractSignedOnline: false,
contractPdfUrl: contractUrl,
contractSignedBy: booking.customerName,
contractUploadedBy: session.user.id,
},
});
return NextResponse.json({
success: true,
message: 'Contract uploaded successfully',
});
} catch (error: any) {
console.error('Contract upload error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to upload contract' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { LexOfficeService } from '@/lib/lexoffice';
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const booking = await prisma.booking.findUnique({
where: { id: params.id },
include: {
location: true,
photobox: true,
},
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
if (booking.lexofficeOfferId) {
return NextResponse.json(
{ error: 'Quotation already exists', offerId: booking.lexofficeOfferId },
{ status: 400 }
);
}
const lexoffice = new LexOfficeService();
// 1. Create or get contact
let contactId = booking.lexofficeContactId;
if (!contactId) {
contactId = await lexoffice.createContactFromBooking(booking);
await prisma.booking.update({
where: { id: booking.id },
data: { lexofficeContactId: contactId },
});
}
// 2. Create quotation
const quotationId = await lexoffice.createQuotationFromBooking(booking, contactId);
// 3. Update booking with offer ID
await prisma.booking.update({
where: { id: booking.id },
data: { lexofficeOfferId: quotationId },
});
return NextResponse.json({
success: true,
quotation: {
id: quotationId,
},
contactId,
});
} catch (error: any) {
console.error('LexOffice quotation creation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to create quotation' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,44 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const booking = await prisma.booking.findUnique({
where: { id: params.id },
});
if (!booking) {
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
}
if (booking.status !== 'READY_FOR_ASSIGNMENT') {
return NextResponse.json({ error: 'Buchung muss im Status READY_FOR_ASSIGNMENT sein' }, { status: 400 });
}
await prisma.booking.update({
where: { id: params.id },
data: {
status: 'OPEN_FOR_DRIVERS',
openForDrivers: true,
},
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Release to drivers error:', error);
return NextResponse.json(
{ error: 'Freigabe fehlgeschlagen' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { nextcloudCalendar } from '@/lib/nextcloud-calendar';
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { id } = params;
const booking = await prisma.booking.update({
where: { id },
data: {
customerName: body.customerName,
customerEmail: body.customerEmail,
customerPhone: body.customerPhone,
customerAddress: body.customerAddress,
customerCity: body.customerCity,
customerZip: body.customerZip,
notes: body.notes,
internalNotes: body.internalNotes,
},
include: {
location: true,
photobox: true,
},
});
try {
await nextcloudCalendar.syncBookingToCalendar(booking);
} catch (calError) {
console.error('Calendar sync error after booking update:', calError);
}
return NextResponse.json({ success: true, booking });
} catch (error) {
console.error('Booking update error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { id } = params;
const { status } = body;
if (!status) {
return NextResponse.json({ error: 'Status is required' }, { status: 400 });
}
const booking = await prisma.booking.update({
where: { id },
data: { status },
include: {
location: true,
photobox: true,
},
});
try {
if (status === 'CANCELLED') {
await nextcloudCalendar.removeBookingFromCalendar(booking.id);
} else {
await nextcloudCalendar.syncBookingToCalendar(booking);
}
} catch (calError) {
console.error('Calendar sync error after status change:', calError);
}
return NextResponse.json({ success: true, booking });
} catch (error) {
console.error('Booking status update error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,158 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const setupWindows = await prisma.setupWindow.findMany({
where: { bookingId: params.id },
orderBy: { setupDate: 'asc' },
});
return NextResponse.json({ setupWindows });
} catch (error: any) {
console.error('Setup windows fetch error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch setup windows' },
{ status: 500 }
);
}
}
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { setupDate, setupTimeStart, setupTimeEnd, preferred, notes } = body;
if (!setupDate || !setupTimeStart || !setupTimeEnd) {
return NextResponse.json(
{ error: 'Setup date and times are required' },
{ status: 400 }
);
}
const booking = await prisma.booking.findUnique({
where: { id: params.id },
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
const setupWindow = await prisma.setupWindow.create({
data: {
bookingId: params.id,
setupDate: new Date(setupDate),
setupTimeStart: new Date(setupTimeStart),
setupTimeEnd: new Date(setupTimeEnd),
preferred: preferred || false,
notes,
},
});
return NextResponse.json({ setupWindow }, { status: 201 });
} catch (error: any) {
console.error('Setup window creation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to create setup window' },
{ status: 500 }
);
}
}
export async function PATCH(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { windowId, selected, preferred, notes } = body;
if (!windowId) {
return NextResponse.json(
{ error: 'Window ID is required' },
{ status: 400 }
);
}
if (selected) {
await prisma.setupWindow.updateMany({
where: { bookingId: params.id },
data: { selected: false },
});
}
const setupWindow = await prisma.setupWindow.update({
where: { id: windowId },
data: {
selected: selected !== undefined ? selected : undefined,
preferred: preferred !== undefined ? preferred : undefined,
notes: notes !== undefined ? notes : undefined,
},
});
return NextResponse.json({ setupWindow });
} catch (error: any) {
console.error('Setup window update error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to update setup window' },
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const windowId = searchParams.get('windowId');
if (!windowId) {
return NextResponse.json(
{ error: 'Window ID is required' },
{ status: 400 }
);
}
await prisma.setupWindow.delete({
where: { id: windowId },
});
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('Setup window deletion error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to delete setup window' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { status } = await request.json();
const { id } = params;
const booking = await prisma.booking.update({
where: { id },
data: { status },
});
// Create notification
await prisma.notification.create({
data: {
type: 'BOOKING_STATUS_CHANGED',
title: 'Buchungsstatus geändert',
message: `Buchung ${booking.bookingNumber}${status}`,
metadata: {
bookingId: booking.id,
newStatus: status,
},
},
});
return NextResponse.json({ success: true, booking });
} catch (error) {
console.error('Status update error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,125 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { nextcloudCalendar } from '@/lib/nextcloud-calendar';
function generateBookingNumber(): string {
const date = new Date();
const year = date.getFullYear().toString().slice(-2);
const month = String(date.getMonth() + 1).padStart(2, '0');
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
return `STM-${year}${month}-${random}`;
}
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const eventDate = new Date(body.eventDate);
const startOfDay = new Date(eventDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(eventDate);
endOfDay.setHours(23, 59, 59, 999);
const availablePhotobox = await prisma.photobox.findFirst({
where: {
locationId: body.locationId,
model: body.model,
active: true,
NOT: {
bookings: {
some: {
eventDate: {
gte: startOfDay,
lte: endOfDay,
},
status: {
in: ['RESERVED', 'CONFIRMED'],
},
},
},
},
},
});
if (!availablePhotobox) {
return NextResponse.json(
{ error: 'Keine Fotobox verfügbar für dieses Datum' },
{ status: 409 }
);
}
const booking = await prisma.booking.create({
data: {
bookingNumber: generateBookingNumber(),
locationId: body.locationId,
photoboxId: availablePhotobox.id,
status: 'RESERVED',
customerName: body.customerName,
customerEmail: body.customerEmail,
customerPhone: body.customerPhone,
customerAddress: body.customerAddress,
customerCity: body.customerCity,
customerZip: body.customerZip,
invoiceType: body.invoiceType,
companyName: body.companyName,
eventDate: new Date(body.eventDate),
eventAddress: body.eventAddress,
eventCity: body.eventCity,
eventZip: body.eventZip,
eventLocation: body.eventLocation,
setupTimeStart: new Date(body.setupTimeStart),
setupTimeLatest: new Date(body.setupTimeLatest),
dismantleTimeEarliest: body.dismantleTimeEarliest ? new Date(body.dismantleTimeEarliest) : null,
dismantleTimeLatest: body.dismantleTimeLatest ? new Date(body.dismantleTimeLatest) : null,
calculatedPrice: body.calculatedPrice,
notes: body.notes,
},
include: {
location: true,
photobox: true,
},
});
await prisma.notification.create({
data: {
type: 'NEW_BOOKING_MANUAL',
title: 'Neue manuelle Buchung',
message: `${body.customerName} - ${eventDate.toLocaleDateString('de-DE')}`,
metadata: {
bookingId: booking.id,
bookingNumber: booking.bookingNumber,
},
},
});
try {
await nextcloudCalendar.syncBookingToCalendar(booking);
} catch (calError) {
console.error('Calendar sync error after booking creation:', calError);
}
return NextResponse.json({
success: true,
booking,
});
} catch (error: any) {
console.error('Booking creation error:', error);
return NextResponse.json(
{ error: error.message || 'Internal server error' },
{ status: 500 }
);
}
}

212
app/api/bookings/route.ts Normal file
View File

@@ -0,0 +1,212 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';
const bookingSchema = z.object({
locationSlug: z.string(),
model: z.enum(['VINTAGE_SMILE', 'VINTAGE_PHOTOS', 'NOSTALGIE', 'MAGIC_MIRROR']),
customerName: z.string().min(2),
customerEmail: z.string().email(),
customerPhone: z.string().min(5),
customerAddress: z.string().optional(),
customerCity: z.string().optional(),
customerZip: z.string().optional(),
invoiceType: z.enum(['PRIVATE', 'BUSINESS']),
companyName: z.string().optional(),
eventDate: z.string(),
eventAddress: z.string().min(5),
eventCity: z.string().min(2),
eventZip: z.string().min(4),
eventLocation: z.string().optional(),
setupTimeStart: z.string(),
setupTimeLatest: z.string(),
dismantleTimeEarliest: z.string().optional(),
dismantleTimeLatest: z.string().optional(),
notes: z.string().optional(),
});
function generateBookingNumber(): string {
const date = new Date();
const year = date.getFullYear().toString().slice(-2);
const month = String(date.getMonth() + 1).padStart(2, '0');
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
return `STM-${year}${month}-${random}`;
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const data = bookingSchema.parse(body);
const location = await prisma.location.findUnique({
where: { slug: data.locationSlug },
});
if (!location) {
return NextResponse.json(
{ error: 'Standort nicht gefunden' },
{ status: 404 }
);
}
const eventDate = new Date(data.eventDate);
const startOfDay = new Date(eventDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(eventDate);
endOfDay.setHours(23, 59, 59, 999);
const availablePhotobox = await prisma.photobox.findFirst({
where: {
locationId: location.id,
model: data.model,
active: true,
NOT: {
bookings: {
some: {
eventDate: {
gte: startOfDay,
lte: endOfDay,
},
status: {
in: ['RESERVED', 'CONFIRMED'],
},
},
},
},
},
});
if (!availablePhotobox) {
return NextResponse.json(
{ error: 'Keine Fotobox verfügbar für dieses Datum' },
{ status: 409 }
);
}
const priceConfig = await prisma.priceConfig.findUnique({
where: {
locationId_model: {
locationId: location.id,
model: data.model,
},
},
});
const calculatedPrice = priceConfig ? priceConfig.basePrice : 0;
const booking = await prisma.booking.create({
data: {
bookingNumber: generateBookingNumber(),
locationId: location.id,
photoboxId: availablePhotobox.id,
status: 'RESERVED',
customerName: data.customerName,
customerEmail: data.customerEmail,
customerPhone: data.customerPhone,
customerAddress: data.customerAddress,
customerCity: data.customerCity,
customerZip: data.customerZip,
invoiceType: data.invoiceType,
companyName: data.companyName,
eventDate: new Date(data.eventDate),
eventAddress: data.eventAddress,
eventCity: data.eventCity,
eventZip: data.eventZip,
eventLocation: data.eventLocation,
setupTimeStart: new Date(data.setupTimeStart),
setupTimeLatest: new Date(data.setupTimeLatest),
dismantleTimeEarliest: data.dismantleTimeEarliest ? new Date(data.dismantleTimeEarliest) : null,
dismantleTimeLatest: data.dismantleTimeLatest ? new Date(data.dismantleTimeLatest) : null,
calculatedPrice,
notes: data.notes,
},
include: {
location: true,
photobox: true,
},
});
await prisma.notification.create({
data: {
type: 'NEW_BOOKING',
title: 'Neue Buchungsanfrage',
message: `${data.customerName} hat eine ${data.model} für ${data.eventCity} am ${eventDate.toLocaleDateString('de-DE')} angefragt.`,
metadata: {
bookingId: booking.id,
bookingNumber: booking.bookingNumber,
},
},
});
return NextResponse.json({
success: true,
booking: {
id: booking.id,
bookingNumber: booking.bookingNumber,
status: booking.status,
eventDate: booking.eventDate,
calculatedPrice: booking.calculatedPrice,
},
message: 'Buchungsanfrage erfolgreich erstellt! Wir melden uns in Kürze bei Ihnen.',
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Ungültige Daten', details: error.errors },
{ status: 400 }
);
}
console.error('Booking creation error:', error);
return NextResponse.json(
{ error: 'Interner Serverfehler' },
{ status: 500 }
);
}
}
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const status = searchParams.get('status');
const locationSlug = searchParams.get('location');
const where: any = {};
if (status) {
where.status = status;
}
if (locationSlug) {
const location = await prisma.location.findUnique({
where: { slug: locationSlug },
});
if (location) {
where.locationId = location.id;
}
}
const bookings = await prisma.booking.findMany({
where,
include: {
location: true,
photobox: true,
setupWindows: {
orderBy: { setupDate: 'asc' },
},
},
orderBy: {
createdAt: 'desc',
},
take: 100,
});
return NextResponse.json({ bookings });
} catch (error) {
console.error('Bookings fetch error:', error);
return NextResponse.json(
{ error: 'Interner Serverfehler' },
{ status: 500 }
);
}
}