Initial commit - SaveTheMoment Atlas Basis-Setup
This commit is contained in:
6
app/api/auth/[...nextauth]/route.ts
Normal file
6
app/api/auth/[...nextauth]/route.ts
Normal 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 };
|
||||
81
app/api/availability/route.ts
Normal file
81
app/api/availability/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
78
app/api/bookings/[id]/ai-analyze/route.ts
Normal file
78
app/api/bookings/[id]/ai-analyze/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
82
app/api/bookings/[id]/assign-driver/route.ts
Normal file
82
app/api/bookings/[id]/assign-driver/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
99
app/api/bookings/[id]/availability/route.ts
Normal file
99
app/api/bookings/[id]/availability/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
114
app/api/bookings/[id]/contract/route.ts
Normal file
114
app/api/bookings/[id]/contract/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
75
app/api/bookings/[id]/contract/send/route.ts
Normal file
75
app/api/bookings/[id]/contract/send/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
73
app/api/bookings/[id]/contract/upload/route.ts
Normal file
73
app/api/bookings/[id]/contract/upload/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
72
app/api/bookings/[id]/create-quotation/route.ts
Normal file
72
app/api/bookings/[id]/create-quotation/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
44
app/api/bookings/[id]/release-to-drivers/route.ts
Normal file
44
app/api/bookings/[id]/release-to-drivers/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
101
app/api/bookings/[id]/route.ts
Normal file
101
app/api/bookings/[id]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
158
app/api/bookings/[id]/setup-windows/route.ts
Normal file
158
app/api/bookings/[id]/setup-windows/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
46
app/api/bookings/[id]/status/route.ts
Normal file
46
app/api/bookings/[id]/status/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
125
app/api/bookings/create/route.ts
Normal file
125
app/api/bookings/create/route.ts
Normal 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
212
app/api/bookings/route.ts
Normal 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
62
app/api/calendar/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
114
app/api/calendar/sync/route.ts
Normal file
114
app/api/calendar/sync/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
96
app/api/contract/sign/route.ts
Normal file
96
app/api/contract/sign/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
74
app/api/contract/upload/route.ts
Normal file
74
app/api/contract/upload/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
83
app/api/cron/check-contracts/route.ts
Normal file
83
app/api/cron/check-contracts/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
60
app/api/cron/email-sync/route.ts
Normal file
60
app/api/cron/email-sync/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
173
app/api/cron/process-pending-bookings/route.ts
Normal file
173
app/api/cron/process-pending-bookings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
145
app/api/drivers/[id]/route.ts
Normal file
145
app/api/drivers/[id]/route.ts
Normal 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
124
app/api/drivers/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
30
app/api/email-sync/route.ts
Normal file
30
app/api/email-sync/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
102
app/api/inventory/[id]/route.ts
Normal file
102
app/api/inventory/[id]/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
67
app/api/inventory/route.ts
Normal file
67
app/api/inventory/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
45
app/api/locations/[id]/email-settings/route.ts
Normal file
45
app/api/locations/[id]/email-settings/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
23
app/api/locations/route.ts
Normal file
23
app/api/locations/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
119
app/api/photoboxes/[id]/route.ts
Normal file
119
app/api/photoboxes/[id]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
98
app/api/photoboxes/route.ts
Normal file
98
app/api/photoboxes/route.ts
Normal 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
41
app/api/prices/route.ts
Normal 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
27
app/api/projects/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
89
app/api/tours/[id]/optimize-route/route.ts
Normal file
89
app/api/tours/[id]/optimize-route/route.ts
Normal 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
175
app/api/tours/[id]/route.ts
Normal 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
206
app/api/tours/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user