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,6 @@
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth';
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@@ -0,0 +1,81 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const locationSlug = searchParams.get('location');
const model = searchParams.get('model');
const date = searchParams.get('date');
if (!locationSlug || !model || !date) {
return NextResponse.json(
{ error: 'Missing required parameters: location, model, date' },
{ status: 400 }
);
}
const location = await prisma.location.findUnique({
where: { slug: locationSlug },
});
if (!location) {
return NextResponse.json(
{ error: 'Location not found' },
{ status: 404 }
);
}
const eventDate = new Date(date);
const startOfDay = new Date(eventDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(eventDate);
endOfDay.setHours(23, 59, 59, 999);
const totalPhotoboxes = await prisma.photobox.count({
where: {
locationId: location.id,
model: model as any,
active: true,
},
});
const bookedPhotoboxes = await prisma.booking.count({
where: {
locationId: location.id,
photobox: {
model: model as any,
},
eventDate: {
gte: startOfDay,
lte: endOfDay,
},
status: {
in: ['RESERVED', 'CONFIRMED'],
},
},
});
const available = totalPhotoboxes - bookedPhotoboxes;
const isAvailable = available > 0;
const isLastOne = available === 1;
return NextResponse.json({
available: isAvailable,
count: available,
total: totalPhotoboxes,
isLastOne,
message: isAvailable
? isLastOne
? 'Nur noch 1 Fotobox verfügbar!'
: `${available} Fotoboxen verfügbar`
: 'Leider ausgebucht',
});
} catch (error) {
console.error('Availability check error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

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 }
);
}
}

62
app/api/calendar/route.ts Normal file
View File

@@ -0,0 +1,62 @@
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(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const searchParams = req.nextUrl.searchParams;
const start = searchParams.get('start');
const end = searchParams.get('end');
const where: any = {};
if (start && end) {
where.eventDate = {
gte: new Date(start),
lte: new Date(end),
};
}
const bookings = await prisma.booking.findMany({
where,
include: {
location: true,
photobox: true,
tour: true,
},
orderBy: { eventDate: 'asc' },
});
const events = bookings.map((booking) => ({
id: booking.id,
title: `${booking.customerName} - ${booking.location.name}`,
start: booking.eventDate,
end: new Date(new Date(booking.eventDate).getTime() + 4 * 60 * 60 * 1000),
resource: {
bookingId: booking.id,
status: booking.status,
customerName: booking.customerName,
customerEmail: booking.customerEmail,
locationName: booking.location.name,
photoboxName: booking.photobox?.name || 'Keine Box',
tourId: booking.tourId,
eventType: booking.eventType,
},
}));
return NextResponse.json({ events });
} catch (error) {
console.error('Error fetching calendar events:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ 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 { nextcloudCalendar } from '@/lib/nextcloud-calendar';
import { prisma } from '@/lib/prisma';
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { action, bookingId } = await req.json();
if (action === 'test-connection') {
try {
const calendars = await nextcloudCalendar.getCalendars();
return NextResponse.json({
success: true,
calendars: calendars.map((cal: any) => ({
displayName: cal.displayName,
url: cal.url,
description: cal.description,
}))
});
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 });
}
}
if (action === 'sync-booking' && bookingId) {
const booking = await prisma.booking.findUnique({
where: { id: bookingId },
include: {
location: true,
photobox: true,
},
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
try {
await nextcloudCalendar.syncBookingToCalendar(booking);
return NextResponse.json({ success: true, message: 'Booking synced to calendar' });
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 });
}
}
if (action === 'sync-all') {
const bookings = await prisma.booking.findMany({
where: {
status: {
in: ['RESERVED', 'CONFIRMED', 'TOUR_CREATED'],
},
},
include: {
location: true,
photobox: true,
},
});
let synced = 0;
let failed = 0;
for (const booking of bookings) {
try {
await nextcloudCalendar.syncBookingToCalendar(booking);
synced++;
} catch (error) {
console.error(`Failed to sync booking ${booking.id}:`, error);
failed++;
}
}
return NextResponse.json({
success: true,
synced,
failed,
total: bookings.length
});
}
if (action === 'remove-booking' && bookingId) {
try {
await nextcloudCalendar.removeBookingFromCalendar(bookingId);
return NextResponse.json({ success: true, message: 'Booking removed from calendar' });
} catch (error: any) {
return NextResponse.json({
success: false,
error: error.message
}, { status: 500 });
}
}
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
} catch (error) {
console.error('Error in calendar sync:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,96 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { generateSignedContractPDF } from '@/lib/pdf-service';
import { writeFile, mkdir } from 'fs/promises';
import path from 'path';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { token, signatureData, name, email } = body;
// Decode token to get booking ID
const decoded = Buffer.from(token, 'base64url').toString();
const bookingId = decoded.split('-')[0];
const booking = await prisma.booking.findUnique({
where: { id: bookingId },
include: {
location: true,
photobox: true,
},
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
if (booking.contractSigned) {
return NextResponse.json(
{ error: 'Contract already signed' },
{ status: 400 }
);
}
// Get client IP
const ip = request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
'unknown';
const now = new Date();
// Update booking with signature
await prisma.booking.update({
where: { id: bookingId },
data: {
contractSigned: true,
contractSignedAt: now,
contractSignedOnline: true,
contractSignatureData: signatureData,
contractSignedBy: name,
contractSignedIp: ip,
},
});
// Generate signed PDF
const pdfBuffer = await generateSignedContractPDF(
booking,
booking.location,
booking.photobox,
signatureData,
name,
now,
ip
);
// Save signed PDF
const contractsDir = path.join(process.cwd(), 'public', 'contracts');
await mkdir(contractsDir, { recursive: true });
const filename = `contract-signed-${booking.bookingNumber}.pdf`;
const filepath = path.join(contractsDir, filename);
await writeFile(filepath, pdfBuffer);
const contractUrl = `/contracts/${filename}`;
await prisma.booking.update({
where: { id: bookingId },
data: {
contractPdfUrl: contractUrl,
},
});
// TODO: Send email with signed contract to customer and admin
return NextResponse.json({
success: true,
message: 'Contract signed successfully',
});
} catch (error: any) {
console.error('Contract signing error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to sign contract' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { writeFile, mkdir } from 'fs/promises';
import path from 'path';
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
const token = formData.get('token') as string;
if (!file || !token) {
return NextResponse.json(
{ error: 'File and token required' },
{ status: 400 }
);
}
// Decode token to get booking ID
const decoded = Buffer.from(token, 'base64url').toString();
const bookingId = decoded.split('-')[0];
const booking = await prisma.booking.findUnique({
where: { id: bookingId },
});
if (!booking) {
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
}
if (booking.contractSigned) {
return NextResponse.json(
{ error: 'Contract already signed' },
{ status: 400 }
);
}
// 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: bookingId },
data: {
contractSigned: true,
contractSignedAt: new Date(),
contractSignedOnline: false,
contractPdfUrl: contractUrl,
contractSignedBy: booking.customerName,
},
});
// TODO: Send email with confirmation to customer and admin
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,83 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { lexofficeService } from '@/lib/lexoffice';
export async function GET(request: Request) {
try {
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const bookings = await prisma.booking.findMany({
where: {
status: 'CONFIRMED',
contractSigned: true,
lexofficeConfirmationId: null,
},
});
let processed = 0;
const results = [];
for (const booking of bookings) {
try {
let contactId = booking.lexofficeContactId;
if (!contactId) {
contactId = await lexofficeService.createContactFromBooking(booking);
await prisma.booking.update({
where: { id: booking.id },
data: { lexofficeContactId: contactId },
});
}
const confirmationId = await lexofficeService.createConfirmationFromBooking(
booking,
contactId
);
await prisma.booking.update({
where: { id: booking.id },
data: {
status: 'READY_FOR_ASSIGNMENT',
readyForAssignment: true,
lexofficeConfirmationId: confirmationId,
confirmationSentAt: new Date(),
},
});
processed++;
results.push({
bookingId: booking.id,
bookingNumber: booking.bookingNumber,
confirmationId,
status: 'success',
});
console.log(`✅ Auftragsbestätigung für ${booking.bookingNumber} erstellt`);
} catch (error) {
console.error(`❌ Fehler bei ${booking.bookingNumber}:`, error);
results.push({
bookingId: booking.id,
bookingNumber: booking.bookingNumber,
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return NextResponse.json({
success: true,
processed,
total: bookings.length,
results,
});
} catch (error) {
console.error('Contract check cron error:', error);
return NextResponse.json(
{ error: 'Cron job failed' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
import { emailSyncService } from '@/lib/email-sync';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('authorization');
const cronSecret = process.env.CRON_SECRET || 'development-secret';
if (authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const locations = await prisma.location.findMany({
where: {
active: true,
emailSyncEnabled: true,
},
});
const results: any[] = [];
for (const location of locations) {
try {
const result = await emailSyncService.syncLocationEmails(location.id);
results.push({
locationId: location.id,
locationName: location.name,
...result,
});
} catch (error: any) {
results.push({
locationId: location.id,
locationName: location.name,
success: false,
newEmails: 0,
newBookings: 0,
errors: [error.message],
});
}
}
const summary = {
totalLocations: locations.length,
totalEmails: results.reduce((sum, r) => sum + (r.newEmails || 0), 0),
totalBookings: results.reduce((sum, r) => sum + (r.newBookings || 0), 0),
results,
};
console.log('Cron email sync completed:', summary);
return NextResponse.json(summary);
} catch (error: any) {
console.error('Cron email sync error:', error);
return NextResponse.json(
{ error: error.message || 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,173 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { AIService } from '@/lib/ai-service';
import { LexOfficeService } from '@/lib/lexoffice';
import { getCalendarService } from '@/lib/nextcloud-calendar';
/**
* AUTO-WORKFLOW CRON-JOB
*
* Läuft alle 5 Minuten und:
* 1. Findet Buchungen mit `aiParsed=false` (neue E-Mails)
* 2. Startet KI-Analyse
* 3. Generiert Entwürfe (E-Mail, Angebot, Vertrag)
* 4. Setzt Status auf PENDING_REVIEW
*/
export async function GET(request: NextRequest) {
try {
// Cron-Secret validieren
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
console.log('🔄 Auto-Workflow Cron-Job gestartet...');
// 1. Finde neue Buchungen (noch nicht von KI analysiert)
const pendingBookings = await prisma.booking.findMany({
where: {
aiParsed: false,
status: 'RESERVED', // Nur neue Reservierungen
},
include: {
location: true,
photobox: true,
},
take: 10, // Max 10 pro Lauf
});
if (pendingBookings.length === 0) {
console.log('✅ Keine neuen Buchungen gefunden');
return NextResponse.json({
message: 'No pending bookings',
processed: 0
});
}
console.log(`📋 ${pendingBookings.length} neue Buchungen gefunden`);
const aiService = new AIService();
const lexoffice = new LexOfficeService();
const calendar = getCalendarService();
let processed = 0;
let errors = 0;
for (const booking of pendingBookings) {
try {
console.log(`\n🤖 Verarbeite Buchung: ${booking.bookingNumber}`);
// 2. KI-Analyse (falls noch nicht vorhanden)
if (!booking.aiResponseDraft) {
console.log(' → Generiere E-Mail-Entwurf...');
// Generiere einfache Antwort (später kann man das erweitern)
const emailDraft = `Hallo ${booking.customerName},
vielen Dank für Ihre Anfrage für eine Fotobox-Vermietung am ${new Date(booking.eventDate).toLocaleDateString('de-DE')} in ${booking.eventCity}!
Wir freuen uns sehr über Ihr Interesse. Anbei finden Sie unser Angebot sowie den Mietvertrag zur Durchsicht.
Falls Sie Fragen haben, stehen wir Ihnen jederzeit gerne zur Verfügung!
Mit freundlichen Grüßen
Ihr ${booking.location?.name || 'SaveTheMoment'} Team`;
await prisma.booking.update({
where: { id: booking.id },
data: {
aiResponseDraft: emailDraft,
aiProcessedAt: new Date(),
},
});
}
// 3. LexOffice Kontakt & Angebot (Entwurf)
if (!booking.lexofficeContactId) {
console.log(' → Erstelle LexOffice Kontakt...');
try {
const contactId = await lexoffice.createContactFromBooking(booking);
await prisma.booking.update({
where: { id: booking.id },
data: { lexofficeContactId: contactId },
});
console.log(' → Erstelle LexOffice Angebot-Entwurf...');
const quotationId = await lexoffice.createQuotationFromBooking(booking, contactId);
await prisma.booking.update({
where: { id: booking.id },
data: { lexofficeOfferId: quotationId },
});
} catch (lexError) {
console.error(' ⚠️ LexOffice-Fehler (wird übersprungen):', lexError);
// Weiter machen, auch wenn LexOffice fehlschlägt
}
}
// 4. Kalender-Eintrag erstellen (Reservierung)
if (!booking.calendarEventId) {
console.log(' → Erstelle Kalender-Eintrag...');
try {
const eventId = await calendar.createBookingEvent(booking);
if (eventId) {
await prisma.booking.update({
where: { id: booking.id },
data: {
calendarEventId: eventId,
calendarSynced: true,
calendarSyncedAt: new Date(),
},
});
}
} catch (calError) {
console.error(' ⚠️ Kalender-Fehler (wird übersprungen):', calError);
}
}
// 5. Status aktualisieren: Bereit für Admin-Review
await prisma.booking.update({
where: { id: booking.id },
data: {
aiParsed: true,
readyForAssignment: true, // Admin kann jetzt prüfen
},
});
console.log(`✅ Buchung ${booking.bookingNumber} erfolgreich verarbeitet`);
processed++;
} catch (error) {
console.error(`❌ Fehler bei Buchung ${booking.bookingNumber}:`, error);
errors++;
// Markiere als fehlerhaft, damit es beim nächsten Lauf erneut versucht wird
await prisma.booking.update({
where: { id: booking.id },
data: {
internalNotes: `Auto-Workflow Fehler: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`,
},
});
}
}
console.log(`\n📊 Cron-Job abgeschlossen:`);
console.log(` ✅ Erfolgreich: ${processed}`);
console.log(` ❌ Fehler: ${errors}`);
return NextResponse.json({
success: true,
processed,
errors,
total: pendingBookings.length,
});
} catch (error: any) {
console.error('❌ Cron-Job Fehler:', error);
return NextResponse.json(
{ error: error.message || 'Cron job failed' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,145 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';
export async function GET(
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 driver = await prisma.user.findUnique({
where: { id: params.id, role: 'DRIVER' },
select: {
id: true,
name: true,
email: true,
phoneNumber: true,
vehiclePlate: true,
vehicleModel: true,
active: true,
available: true,
createdAt: true,
driverTours: {
include: {
bookings: {
select: {
id: true,
bookingNumber: true,
eventDate: true,
eventLocation: true,
customerName: true,
},
},
},
orderBy: {
tourDate: 'desc',
},
take: 10,
},
},
});
if (!driver) {
return NextResponse.json({ error: 'Driver not found' }, { status: 404 });
}
return NextResponse.json({ driver });
} catch (error: any) {
console.error('Driver fetch error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch driver' },
{ 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 {
name,
email,
password,
phoneNumber,
vehiclePlate,
vehicleModel,
active,
available,
} = body;
const updateData: any = {};
if (name !== undefined) updateData.name = name;
if (email !== undefined) updateData.email = email;
if (phoneNumber !== undefined) updateData.phoneNumber = phoneNumber;
if (vehiclePlate !== undefined) updateData.vehiclePlate = vehiclePlate;
if (vehicleModel !== undefined) updateData.vehicleModel = vehicleModel;
if (active !== undefined) updateData.active = active;
if (available !== undefined) updateData.available = available;
if (password) {
updateData.password = await bcrypt.hash(password, 10);
}
const driver = await prisma.user.update({
where: { id: params.id },
data: updateData,
select: {
id: true,
name: true,
email: true,
phoneNumber: true,
vehiclePlate: true,
vehicleModel: true,
active: true,
available: true,
},
});
return NextResponse.json({ driver });
} catch (error: any) {
console.error('Driver update error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to update driver' },
{ 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 });
}
await prisma.user.delete({
where: { id: params.id },
});
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('Driver deletion error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to delete driver' },
{ status: 500 }
);
}
}

124
app/api/drivers/route.ts Normal file
View File

@@ -0,0 +1,124 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';
export async function GET(request: NextRequest) {
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 available = searchParams.get('available');
const where: any = { role: 'DRIVER' };
if (available === 'true') where.available = true;
if (available === 'false') where.available = false;
const drivers = await prisma.user.findMany({
where,
select: {
id: true,
name: true,
email: true,
phoneNumber: true,
vehiclePlate: true,
vehicleModel: true,
active: true,
available: true,
createdAt: true,
_count: {
select: {
driverTours: true,
},
},
},
orderBy: {
name: 'asc',
},
});
return NextResponse.json({ drivers });
} catch (error: any) {
console.error('Driver fetch error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch drivers' },
{ status: 500 }
);
}
}
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 {
name,
email,
password,
phoneNumber,
vehiclePlate,
vehicleModel,
} = body;
if (!name || !email || !password) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
return NextResponse.json(
{ error: 'Email already in use' },
{ status: 400 }
);
}
const hashedPassword = await bcrypt.hash(password, 10);
const driver = await prisma.user.create({
data: {
name,
email,
password: hashedPassword,
phoneNumber,
vehiclePlate,
vehicleModel,
role: 'DRIVER',
active: true,
available: true,
},
select: {
id: true,
name: true,
email: true,
phoneNumber: true,
vehiclePlate: true,
vehicleModel: true,
active: true,
available: true,
createdAt: true,
},
});
return NextResponse.json({ driver }, { status: 201 });
} catch (error: any) {
console.error('Driver creation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to create driver' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { emailSyncService } from '@/lib/email-sync';
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 { locationId } = await request.json();
if (!locationId) {
return NextResponse.json({ error: 'Location ID required' }, { status: 400 });
}
const result = await emailSyncService.syncLocationEmails(locationId);
return NextResponse.json(result);
} catch (error: any) {
console.error('Email sync API error:', error);
return NextResponse.json(
{ error: error.message || 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,102 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const equipment = await prisma.equipment.findUnique({
where: { id: params.id },
include: {
location: true,
project: true,
bookingEquipment: {
include: {
booking: true,
},
},
},
});
if (!equipment) {
return NextResponse.json({ error: 'Equipment not found' }, { status: 404 });
}
return NextResponse.json({ equipment });
} catch (error) {
console.error('Error fetching equipment:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const data = await request.json();
const equipment = await prisma.equipment.update({
where: { id: params.id },
data: {
name: data.name,
type: data.type,
brand: data.brand,
model: data.model,
serialNumber: data.serialNumber,
quantity: data.quantity,
status: data.status,
locationId: data.locationId,
projectId: data.projectId,
notes: data.notes,
purchaseDate: data.purchaseDate ? new Date(data.purchaseDate) : null,
purchasePrice: data.purchasePrice,
minStockLevel: data.minStockLevel,
currentStock: data.currentStock,
},
include: {
location: true,
project: true,
},
});
return NextResponse.json({ equipment });
} catch (error) {
console.error('Error updating equipment:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
await prisma.equipment.delete({
where: { id: params.id },
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting equipment:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,67 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const equipment = await prisma.equipment.findMany({
include: {
location: true,
project: true,
},
orderBy: {
createdAt: 'desc',
},
});
return NextResponse.json({ equipment });
} catch (error) {
console.error('Error fetching equipment:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const data = await request.json();
const equipment = await prisma.equipment.create({
data: {
name: data.name,
type: data.type,
brand: data.brand,
model: data.model,
serialNumber: data.serialNumber,
quantity: data.quantity || 1,
status: data.status || 'AVAILABLE',
locationId: data.locationId,
projectId: data.projectId,
notes: data.notes,
purchaseDate: data.purchaseDate ? new Date(data.purchaseDate) : null,
purchasePrice: data.purchasePrice,
minStockLevel: data.minStockLevel,
currentStock: data.currentStock,
},
include: {
location: true,
project: true,
},
});
return NextResponse.json({ equipment });
} catch (error) {
console.error('Error creating equipment:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,45 @@
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 body = await request.json();
const { id } = params;
const location = await prisma.location.update({
where: { id },
data: {
imapHost: body.imapHost || null,
imapPort: body.imapPort || null,
imapUser: body.imapUser || null,
imapPassword: body.imapPassword || null,
imapSecure: body.imapSecure ?? true,
smtpHost: body.smtpHost || null,
smtpPort: body.smtpPort || null,
smtpUser: body.smtpUser || null,
smtpPassword: body.smtpPassword || null,
smtpSecure: body.smtpSecure ?? true,
emailSyncEnabled: body.emailSyncEnabled ?? false,
},
});
return NextResponse.json({ success: true, location });
} catch (error) {
console.error('Email settings update error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET() {
try {
const locations = await prisma.location.findMany({
where: {
active: true,
},
orderBy: {
name: 'asc',
},
});
return NextResponse.json({ locations });
} catch (error) {
console.error('Locations fetch error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,119 @@
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 || session.user.role !== 'ADMIN') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const photobox = await prisma.photobox.findUnique({
where: { id: params.id },
include: {
location: true,
bookings: {
select: {
id: true,
bookingNumber: true,
eventDate: true,
status: true,
customerName: true,
},
orderBy: {
eventDate: 'desc',
},
},
},
});
if (!photobox) {
return NextResponse.json({ error: 'Photobox not found' }, { status: 404 });
}
return NextResponse.json({ photobox });
} catch (error: any) {
console.error('Photobox fetch error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch photobox' },
{ 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 {
model,
serialNumber,
status,
active,
description,
lastMaintenance,
} = body;
const updateData: any = {};
if (model !== undefined) updateData.model = model;
if (serialNumber !== undefined) updateData.serialNumber = serialNumber;
if (status !== undefined) updateData.status = status;
if (active !== undefined) updateData.active = active;
if (description !== undefined) updateData.description = description;
if (lastMaintenance !== undefined) {
updateData.lastMaintenance = lastMaintenance ? new Date(lastMaintenance) : null;
}
const photobox = await prisma.photobox.update({
where: { id: params.id },
data: updateData,
include: {
location: true,
},
});
return NextResponse.json({ photobox });
} catch (error: any) {
console.error('Photobox update error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to update photobox' },
{ 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 });
}
await prisma.photobox.delete({
where: { id: params.id },
});
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('Photobox deletion error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to delete photobox' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,98 @@
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) {
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 locationId = searchParams.get('locationId');
const status = searchParams.get('status');
const where: any = {};
if (locationId) where.locationId = locationId;
if (status) where.status = status;
const photoboxes = await prisma.photobox.findMany({
where,
include: {
location: {
select: {
id: true,
name: true,
city: true,
},
},
_count: {
select: {
bookings: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
return NextResponse.json({ photoboxes });
} catch (error: any) {
console.error('Photobox fetch error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch photoboxes' },
{ status: 500 }
);
}
}
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 {
locationId,
model,
serialNumber,
description,
purchaseDate,
} = body;
if (!locationId || !model || !serialNumber) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const photobox = await prisma.photobox.create({
data: {
locationId,
model,
serialNumber,
description,
purchaseDate: purchaseDate ? new Date(purchaseDate) : null,
status: 'AVAILABLE',
active: true,
},
include: {
location: true,
},
});
return NextResponse.json({ photobox }, { status: 201 });
} catch (error: any) {
console.error('Photobox creation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to create photobox' },
{ status: 500 }
);
}
}

41
app/api/prices/route.ts Normal file
View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const locationSlug = searchParams.get('location');
if (!locationSlug) {
return NextResponse.json(
{ error: 'Location parameter required' },
{ status: 400 }
);
}
const location = await prisma.location.findUnique({
where: { slug: locationSlug },
});
if (!location) {
return NextResponse.json(
{ error: 'Location not found' },
{ status: 404 }
);
}
const prices = await prisma.priceConfig.findMany({
where: {
locationId: location.id,
},
});
return NextResponse.json({ prices });
} catch (error) {
console.error('Prices fetch error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

27
app/api/projects/route.ts Normal file
View File

@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const projects = await prisma.project.findMany({
where: {
active: true,
},
orderBy: {
name: 'asc',
},
});
return NextResponse.json({ projects });
} catch (error) {
console.error('Error fetching projects:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

View File

@@ -0,0 +1,89 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { routeService } from '@/lib/route-optimization';
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 tour = await prisma.tour.findUnique({
where: { id: params.id },
include: {
bookings: {
include: {
location: true,
},
orderBy: {
setupTimeStart: 'asc',
},
},
driver: true,
},
});
if (!tour) {
return NextResponse.json({ error: 'Tour nicht gefunden' }, { status: 404 });
}
if (tour.bookings.length === 0) {
return NextResponse.json({ error: 'Keine Buchungen in dieser Tour' }, { status: 400 });
}
const stops = tour.bookings.map((booking) => ({
address: `${booking.eventAddress}, ${booking.eventZip} ${booking.eventCity}`,
bookingId: booking.id,
setupTime: booking.setupTimeStart.toISOString(),
}));
const startLocation = tour.bookings[0].location.city;
const optimizedRoute = await routeService.optimizeRouteWithTimeWindows(
stops,
startLocation
);
if (!optimizedRoute) {
return NextResponse.json(
{ error: 'Routenoptimierung fehlgeschlagen' },
{ status: 500 }
);
}
const updatedTour = await prisma.tour.update({
where: { id: params.id },
data: {
routeOptimized: optimizedRoute as any,
totalDistance: optimizedRoute.totalDistance,
estimatedDuration: optimizedRoute.totalDuration,
},
include: {
bookings: {
include: {
location: true,
},
},
driver: true,
},
});
return NextResponse.json({
success: true,
tour: updatedTour,
route: optimizedRoute,
});
} catch (error) {
console.error('Route optimization error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

175
app/api/tours/[id]/route.ts Normal file
View File

@@ -0,0 +1,175 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { optimizeRoute } from '@/lib/google-maps';
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 tour = await prisma.tour.findUnique({
where: { id: params.id },
include: {
driver: {
select: {
id: true,
name: true,
email: true,
phoneNumber: true,
vehiclePlate: true,
vehicleModel: true,
},
},
bookings: {
include: {
location: {
select: {
name: true,
city: true,
},
},
photobox: {
select: {
model: true,
serialNumber: true,
},
},
},
orderBy: {
setupTimeStart: 'asc',
},
},
},
});
if (!tour) {
return NextResponse.json({ error: 'Tour not found' }, { status: 404 });
}
if (session.user.role !== 'ADMIN' && session.user.id !== tour.driverId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ tour });
} catch (error: any) {
console.error('Tour fetch error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch tour' },
{ 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 { driverId, status, notes, bookingIds } = body;
const updateData: any = {};
if (driverId !== undefined) updateData.driverId = driverId;
if (status !== undefined) {
updateData.status = status;
if (status === 'IN_PROGRESS' && !updateData.startedAt) {
updateData.startedAt = new Date();
}
if (status === 'COMPLETED' && !updateData.completedAt) {
updateData.completedAt = new Date();
}
}
if (notes !== undefined) updateData.notes = notes;
if (bookingIds !== undefined) {
await prisma.booking.updateMany({
where: { tourId: params.id },
data: { tourId: null },
});
if (bookingIds.length > 0) {
await prisma.booking.updateMany({
where: { id: { in: bookingIds } },
data: { tourId: params.id },
});
const bookings = await prisma.booking.findMany({
where: { id: { in: bookingIds } },
select: {
eventAddress: true,
eventCity: true,
eventZip: true,
setupTimeStart: true,
},
});
try {
const routeData = await optimizeRoute(bookings);
updateData.routeOptimized = routeData;
updateData.totalDistance = routeData.totalDistance;
updateData.estimatedDuration = routeData.totalDuration;
} catch (routeError) {
console.error('Route optimization error:', routeError);
}
}
}
const tour = await prisma.tour.update({
where: { id: params.id },
data: updateData,
include: {
driver: true,
bookings: true,
},
});
return NextResponse.json({ tour });
} catch (error: any) {
console.error('Tour update error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to update tour' },
{ 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 });
}
await prisma.booking.updateMany({
where: { tourId: params.id },
data: { tourId: null },
});
await prisma.tour.delete({
where: { id: params.id },
});
return NextResponse.json({ success: true });
} catch (error: any) {
console.error('Tour deletion error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to delete tour' },
{ status: 500 }
);
}
}

206
app/api/tours/route.ts Normal file
View File

@@ -0,0 +1,206 @@
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { optimizeRoute, optimizeRouteBySchedule } from '@/lib/google-maps';
export async function GET(request: NextRequest) {
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 driverId = searchParams.get('driverId');
const status = searchParams.get('status');
const date = searchParams.get('date');
const where: any = {};
if (driverId) where.driverId = driverId;
if (status) where.status = status;
if (date) {
const startDate = new Date(date);
const endDate = new Date(date);
endDate.setDate(endDate.getDate() + 1);
where.tourDate = {
gte: startDate,
lt: endDate,
};
}
const tours = await prisma.tour.findMany({
where,
include: {
driver: {
select: {
id: true,
name: true,
email: true,
phoneNumber: true,
vehiclePlate: true,
},
},
bookings: {
select: {
id: true,
bookingNumber: true,
eventDate: true,
eventAddress: true,
eventCity: true,
eventZip: true,
eventLocation: true,
customerName: true,
setupTimeStart: true,
setupTimeLatest: true,
},
},
},
orderBy: {
tourDate: 'desc',
},
});
return NextResponse.json({ tours });
} catch (error: any) {
console.error('Tour fetch error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to fetch tours' },
{ status: 500 }
);
}
}
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 { tourDate, driverId, bookingIds, optimizationType = 'fastest' } = body;
if (!tourDate) {
return NextResponse.json(
{ error: 'Tour date is required' },
{ status: 400 }
);
}
const tourNumber = `TOUR-${new Date().getFullYear()}-${Date.now().toString().slice(-6)}`;
const tour = await prisma.tour.create({
data: {
tourDate: new Date(tourDate),
tourNumber,
driverId,
status: 'PLANNED',
},
});
if (bookingIds && bookingIds.length > 0) {
await prisma.booking.updateMany({
where: {
id: { in: bookingIds },
},
data: {
tourId: tour.id,
},
});
// Mark setup windows as selected for bookings with flexible setup times
for (const bookingId of bookingIds) {
const booking = await prisma.booking.findUnique({
where: { id: bookingId },
include: { setupWindows: true },
});
if (booking && booking.setupWindows.length > 0) {
const tourDateStr = new Date(tourDate).toISOString().split('T')[0];
const matchingWindow = booking.setupWindows.find((w: any) => {
const windowDateStr = new Date(w.setupDate).toISOString().split('T')[0];
return windowDateStr === tourDateStr && !w.selected;
});
if (matchingWindow) {
await prisma.setupWindow.update({
where: { id: matchingWindow.id },
data: { selected: true },
});
}
}
}
const bookings = await prisma.booking.findMany({
where: { id: { in: bookingIds } },
include: { setupWindows: true },
select: {
eventAddress: true,
eventCity: true,
eventZip: true,
setupTimeStart: true,
setupTimeLatest: true,
setupWindows: true,
},
});
try {
// For route optimization, use the selected setup window time if available
const stopsWithSetupTimes = bookings.map((booking: any) => {
const tourDateStr = new Date(tourDate).toISOString().split('T')[0];
const selectedWindow = booking.setupWindows?.find((w: any) => w.selected);
if (selectedWindow) {
return {
eventAddress: booking.eventAddress,
eventCity: booking.eventCity,
eventZip: booking.eventZip,
setupTimeStart: selectedWindow.setupTimeStart,
setupTimeLatest: selectedWindow.setupTimeEnd,
};
}
return {
eventAddress: booking.eventAddress,
eventCity: booking.eventCity,
eventZip: booking.eventZip,
setupTimeStart: booking.setupTimeStart,
setupTimeLatest: booking.setupTimeLatest,
};
});
const routeData = optimizationType === 'schedule'
? await optimizeRouteBySchedule(stopsWithSetupTimes)
: await optimizeRoute(stopsWithSetupTimes);
await prisma.tour.update({
where: { id: tour.id },
data: {
routeOptimized: routeData as any,
totalDistance: routeData.totalDistance,
estimatedDuration: routeData.totalDuration,
},
});
} catch (routeError) {
console.error('Route optimization error:', routeError);
}
}
const updatedTour = await prisma.tour.findUnique({
where: { id: tour.id },
include: {
driver: true,
bookings: true,
},
});
return NextResponse.json({ tour: updatedTour }, { status: 201 });
} catch (error: any) {
console.error('Tour creation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to create tour' },
{ status: 500 }
);
}
}