- Vintage Modell hinzugefuegt - Equipment Multi-Select (Neue Buchung + Bearbeitung) - Kundenadresse in Formularen - Bearbeiten-Seite fuer Buchungen - Abbau-Zeiten in Formularen und Uebersicht - Vertrag PDF nur bei Privatkunden - LexOffice Kontakt-Erstellung Fix (BUSINESS) - Zurueck-Pfeil auf Touren-Seite
284 lines
8.3 KiB
TypeScript
284 lines
8.3 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { z } from 'zod';
|
|
import { DistanceCalculator } from '@/lib/distance-calculator';
|
|
import { PriceCalculator } from '@/lib/price-calculator';
|
|
import { bookingAutomationService } from '@/lib/booking-automation';
|
|
|
|
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,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!priceConfig) {
|
|
return NextResponse.json(
|
|
{ error: 'Preiskonfiguration nicht gefunden' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
let distance: number | null = null;
|
|
let calculatedPrice = priceConfig.basePrice;
|
|
|
|
if (location.warehouseAddress && location.warehouseZip && location.warehouseCity) {
|
|
const warehouseAddress = DistanceCalculator.formatAddress(
|
|
location.warehouseAddress,
|
|
location.warehouseZip,
|
|
location.warehouseCity
|
|
);
|
|
const eventAddress = DistanceCalculator.formatAddress(
|
|
data.eventAddress,
|
|
data.eventZip,
|
|
data.eventCity
|
|
);
|
|
|
|
const distanceResult = await DistanceCalculator.calculateDistance(
|
|
warehouseAddress,
|
|
eventAddress
|
|
);
|
|
|
|
if (distanceResult) {
|
|
distance = distanceResult.distance;
|
|
|
|
const priceBreakdown = PriceCalculator.calculateTotalPrice(
|
|
priceConfig.basePrice,
|
|
distance,
|
|
{
|
|
basePrice: priceConfig.basePrice,
|
|
kmFlatRate: priceConfig.kmFlatRate,
|
|
kmFlatRateUpTo: priceConfig.kmFlatRateUpTo,
|
|
pricePerKm: priceConfig.pricePerKm,
|
|
kmMultiplier: priceConfig.kmMultiplier,
|
|
}
|
|
);
|
|
|
|
calculatedPrice = priceBreakdown.totalPrice;
|
|
|
|
console.log('📍 Distanzberechnung:', {
|
|
from: warehouseAddress,
|
|
to: eventAddress,
|
|
distance: `${distance}km`,
|
|
breakdown: PriceCalculator.formatPriceBreakdown(priceBreakdown),
|
|
});
|
|
} else {
|
|
console.warn('⚠️ Distanzberechnung fehlgeschlagen, verwende nur Grundpreis');
|
|
}
|
|
} else {
|
|
console.warn('⚠️ Keine Lager-Adresse konfiguriert, verwende nur Grundpreis');
|
|
}
|
|
|
|
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,
|
|
distance,
|
|
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,
|
|
},
|
|
},
|
|
});
|
|
|
|
// 🤖 Automatische Post-Booking Aktionen (E-Mail + Kalender)
|
|
console.log('📢 Starte automatische Aktionen...');
|
|
bookingAutomationService.runPostBookingActions(booking.id).catch(err => {
|
|
console.error('⚠️ Automatische Aktionen fehlgeschlagen:', err);
|
|
});
|
|
|
|
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) {
|
|
// Support multiple statuses separated by comma
|
|
const statuses = status.split(',').map(s => s.trim());
|
|
if (statuses.length > 1) {
|
|
where.status = { in: statuses };
|
|
} else {
|
|
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 }
|
|
);
|
|
}
|
|
}
|