Initial commit - SaveTheMoment Atlas Basis-Setup
This commit is contained in:
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user