feat: Equipment-System, Buchungsbearbeitung, Kundenadresse, LexOffice-Fix
- Vintage Modell hinzugefuegt - Equipment Multi-Select (Neue Buchung + Bearbeitung) - Kundenadresse in Formularen - Bearbeiten-Seite fuer Buchungen - Abbau-Zeiten in Formularen und Uebersicht - Vertrag PDF nur bei Privatkunden - LexOffice Kontakt-Erstellung Fix (BUSINESS) - Zurueck-Pfeil auf Touren-Seite
This commit is contained in:
87
app/api/admin/sync-all-bookings/route.ts
Normal file
87
app/api/admin/sync-all-bookings/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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 POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
console.log('🔄 Synchronisiere bestehende Buchungen mit Nextcloud...\n');
|
||||
|
||||
// Hole alle bestätigten Buchungen
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
status: {
|
||||
in: ['RESERVED', 'CONFIRMED'],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
location: true,
|
||||
photobox: true,
|
||||
},
|
||||
orderBy: {
|
||||
eventDate: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`📊 Gefunden: ${bookings.length} Buchungen`);
|
||||
|
||||
if (bookings.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Keine Buchungen zum Synchronisieren gefunden.',
|
||||
synced: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let synced = 0;
|
||||
let failed = 0;
|
||||
const errors: any[] = [];
|
||||
|
||||
for (const booking of bookings) {
|
||||
try {
|
||||
console.log(`📅 Synchronisiere: ${booking.bookingNumber} - ${booking.customerName}`);
|
||||
|
||||
await nextcloudCalendar.syncBookingToCalendar(booking);
|
||||
synced++;
|
||||
console.log(` ✅ Erfolgreich!`);
|
||||
} catch (error: any) {
|
||||
failed++;
|
||||
console.error(` ❌ Fehler: ${error.message}`);
|
||||
errors.push({
|
||||
bookingNumber: booking.bookingNumber,
|
||||
customerName: booking.customerName,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('─'.repeat(50));
|
||||
console.log(`✅ Erfolgreich synchronisiert: ${synced}`);
|
||||
console.log(`❌ Fehlgeschlagen: ${failed}`);
|
||||
console.log(`📊 Gesamt: ${bookings.length}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
synced,
|
||||
failed,
|
||||
total: bookings.length,
|
||||
errors: errors.length > 0 ? errors : undefined,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Fehler beim Synchronisieren:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to sync bookings' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
108
app/api/admin/sync-emails/route.ts
Normal file
108
app/api/admin/sync-emails/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { emailSyncService } from '@/lib/email-sync';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
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) {
|
||||
// Sync specific location
|
||||
console.log(`🔄 Starte E-Mail-Sync für Location: ${locationId}`);
|
||||
const result = await emailSyncService.syncLocationEmails(locationId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: result.success,
|
||||
location: locationId,
|
||||
newEmails: result.newEmails,
|
||||
newBookings: result.newBookings,
|
||||
errors: result.errors,
|
||||
});
|
||||
} else {
|
||||
// Sync all locations
|
||||
console.log('🔄 Starte E-Mail-Sync für alle Locations...');
|
||||
|
||||
const locations = await prisma.location.findMany({
|
||||
where: { emailSyncEnabled: true },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const location of locations) {
|
||||
console.log(`📍 Sync: ${location.name}`);
|
||||
const result = await emailSyncService.syncLocationEmails(location.id);
|
||||
results.push({
|
||||
locationId: location.id,
|
||||
locationName: location.name,
|
||||
...result,
|
||||
});
|
||||
}
|
||||
|
||||
const totalNewEmails = results.reduce((sum, r) => sum + r.newEmails, 0);
|
||||
const totalNewBookings = results.reduce((sum, r) => sum + r.newBookings, 0);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
totalLocations: locations.length,
|
||||
totalNewEmails,
|
||||
totalNewBookings,
|
||||
results,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ E-Mail-Sync Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'E-Mail-Sync fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
// Get sync status for all locations
|
||||
const locations = await prisma.location.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
emailSyncEnabled: true,
|
||||
lastEmailSync: true,
|
||||
imapHost: true,
|
||||
imapUser: true,
|
||||
},
|
||||
});
|
||||
|
||||
const status = locations.map(loc => ({
|
||||
id: loc.id,
|
||||
name: loc.name,
|
||||
slug: loc.slug,
|
||||
syncEnabled: loc.emailSyncEnabled,
|
||||
configured: !!(loc.imapHost && loc.imapUser),
|
||||
lastSync: loc.lastEmailSync,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ locations: status });
|
||||
} catch (error: any) {
|
||||
console.error('❌ Fehler beim Abrufen des Sync-Status:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Fehler beim Abrufen des Status' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
96
app/api/admin/test-automation/route.ts
Normal file
96
app/api/admin/test-automation/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { bookingAutomationService } from '@/lib/booking-automation';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
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 { bookingId } = await request.json();
|
||||
|
||||
if (!bookingId) {
|
||||
return NextResponse.json({ error: 'bookingId required' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log(`🤖 Starte automatische Aktionen für Buchung: ${bookingId}`);
|
||||
|
||||
const result = await bookingAutomationService.runPostBookingActions(bookingId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
emailSent: result.emailSent,
|
||||
calendarSynced: result.calendarSynced,
|
||||
lexofficeCreated: result.lexofficeCreated,
|
||||
contractGenerated: result.contractGenerated,
|
||||
errors: result.errors,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Automation Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Automation fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Hole neueste Buchung und teste Automation
|
||||
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 });
|
||||
}
|
||||
|
||||
// Hole neueste Buchung
|
||||
const latestBooking = await prisma.booking.findFirst({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
bookingNumber: true,
|
||||
customerName: true,
|
||||
customerEmail: true,
|
||||
eventDate: true,
|
||||
calendarSynced: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!latestBooking) {
|
||||
return NextResponse.json({ error: 'Keine Buchung gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
console.log(`🤖 Teste Automation für: ${latestBooking.bookingNumber}`);
|
||||
|
||||
const result = await bookingAutomationService.runPostBookingActions(latestBooking.id);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
booking: {
|
||||
id: latestBooking.id,
|
||||
bookingNumber: latestBooking.bookingNumber,
|
||||
customerName: latestBooking.customerName,
|
||||
customerEmail: latestBooking.customerEmail,
|
||||
eventDate: latestBooking.eventDate,
|
||||
},
|
||||
emailSent: result.emailSent,
|
||||
calendarSynced: result.calendarSynced,
|
||||
lexofficeCreated: result.lexofficeCreated,
|
||||
contractGenerated: result.contractGenerated,
|
||||
errors: result.errors,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Test Automation Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Test fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
175
app/api/bookings/[id]/confirm/route.ts
Normal file
175
app/api/bookings/[id]/confirm/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 { lexofficeService } from '@/lib/lexoffice';
|
||||
import { nextcloudCalendar } from '@/lib/nextcloud-calendar';
|
||||
|
||||
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 bookingId = params.id;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
location: true,
|
||||
photobox: true,
|
||||
bookingEquipment: {
|
||||
include: {
|
||||
equipment: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Buchung nicht gefunden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (booking.status === 'CONFIRMED') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Buchung ist bereits bestätigt' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!booking.contractSigned) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Vertrag muss zuerst unterschrieben werden' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🔄 Bestätige Buchung ${booking.bookingNumber}...`);
|
||||
|
||||
let lexofficeConfirmationId = null;
|
||||
|
||||
if (booking.lexofficeContactId) {
|
||||
try {
|
||||
console.log(' 💼 Erstelle LexOffice Auftragsbestätigung...');
|
||||
|
||||
lexofficeConfirmationId = await lexofficeService.createConfirmationFromBooking(
|
||||
booking,
|
||||
booking.lexofficeContactId
|
||||
);
|
||||
|
||||
console.log(` ✅ Auftragsbestätigung erstellt: ${lexofficeConfirmationId}`);
|
||||
} catch (error: any) {
|
||||
console.error(' ❌ LexOffice Fehler:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedBooking = await prisma.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: {
|
||||
status: 'CONFIRMED',
|
||||
lexofficeConfirmationId,
|
||||
confirmationSentAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(' 📅 Update Nextcloud Kalender...');
|
||||
await nextcloudCalendar.syncBookingToCalendar(updatedBooking);
|
||||
console.log(' ✅ Kalender aktualisiert');
|
||||
} catch (error: any) {
|
||||
console.error(' ❌ Kalender-Update Fehler:', error.message);
|
||||
}
|
||||
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
type: 'BOOKING_CONFIRMED',
|
||||
title: 'Buchung bestätigt',
|
||||
message: `Buchung ${booking.bookingNumber} für ${booking.customerName} wurde von Admin bestätigt.`,
|
||||
metadata: {
|
||||
bookingId: booking.id,
|
||||
bookingNumber: booking.bookingNumber,
|
||||
lexofficeConfirmationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Buchung bestätigt: ${booking.bookingNumber}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
booking: {
|
||||
id: updatedBooking.id,
|
||||
bookingNumber: updatedBooking.bookingNumber,
|
||||
status: updatedBooking.status,
|
||||
confirmationSentAt: updatedBooking.confirmationSentAt,
|
||||
lexofficeConfirmationId: updatedBooking.lexofficeConfirmationId,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Bestätigungs-Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Bestätigung fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 bookingId = params.id;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
select: {
|
||||
id: true,
|
||||
bookingNumber: true,
|
||||
status: true,
|
||||
contractSigned: true,
|
||||
contractSignedAt: true,
|
||||
confirmationSentAt: true,
|
||||
lexofficeContactId: true,
|
||||
lexofficeOfferId: true,
|
||||
lexofficeConfirmationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Buchung nicht gefunden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const canConfirm = booking.status !== 'CONFIRMED' && booking.contractSigned;
|
||||
|
||||
return NextResponse.json({
|
||||
booking,
|
||||
canConfirm,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Buchungs-Status Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Fehler beim Abrufen des Status' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
54
app/api/bookings/[id]/confirmation-pdf/route.ts
Normal file
54
app/api/bookings/[id]/confirmation-pdf/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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 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 bookingId = params.id;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
select: {
|
||||
id: true,
|
||||
bookingNumber: true,
|
||||
lexofficeConfirmationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!booking.lexofficeConfirmationId) {
|
||||
return NextResponse.json({ error: 'Keine Auftragsbestätigung vorhanden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const pdfBuffer = await lexofficeService.getInvoicePDF(booking.lexofficeConfirmationId);
|
||||
|
||||
return new NextResponse(pdfBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="Auftragsbestaetigung_${booking.bookingNumber}.pdf"`,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ PDF-Download Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'PDF-Download fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
73
app/api/bookings/[id]/contract-pdf/route.ts
Normal file
73
app/api/bookings/[id]/contract-pdf/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 { generateContractFromTemplate } from '@/lib/pdf-template-service';
|
||||
|
||||
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 bookingId = params.id;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
location: true,
|
||||
photobox: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
let priceConfig = null;
|
||||
if (booking.photobox?.model && booking.locationId) {
|
||||
priceConfig = await prisma.priceConfig.findUnique({
|
||||
where: {
|
||||
locationId_model: {
|
||||
locationId: booking.locationId,
|
||||
model: booking.photobox.model,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const bookingWithPriceConfig = {
|
||||
...booking,
|
||||
priceConfig,
|
||||
};
|
||||
|
||||
const signatureData = booking.contractSignedOnline ? booking.contractSignatureData : undefined;
|
||||
|
||||
const pdfBuffer = await generateContractFromTemplate(
|
||||
bookingWithPriceConfig,
|
||||
booking.location,
|
||||
booking.photobox,
|
||||
signatureData
|
||||
);
|
||||
|
||||
return new NextResponse(pdfBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="Mietvertrag_${booking.bookingNumber}.pdf"`,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Contract-PDF-Download Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'PDF-Download fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
111
app/api/bookings/[id]/debug/route.ts
Normal file
111
app/api/bookings/[id]/debug/route.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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 bookingId = params.id;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
location: true,
|
||||
photobox: true,
|
||||
bookingEquipment: {
|
||||
include: {
|
||||
equipment: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
let priceConfig = null;
|
||||
if (booking.photobox?.model && booking.locationId) {
|
||||
priceConfig = await prisma.priceConfig.findUnique({
|
||||
where: {
|
||||
locationId_model: {
|
||||
locationId: booking.locationId,
|
||||
model: booking.photobox.model,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const lineItems: any[] = [];
|
||||
const withPrintFlat = booking.withPrintFlat !== false;
|
||||
|
||||
// Photobox LineItem
|
||||
const photoboxArticleId = withPrintFlat
|
||||
? (priceConfig?.lexofficeArticleIdWithFlat || priceConfig?.lexofficeArticleId)
|
||||
: priceConfig?.lexofficeArticleId;
|
||||
|
||||
const boxName = booking.photobox?.model || 'Fotobox';
|
||||
const flatSuffix = withPrintFlat ? ' mit Druckflatrate' : ' (nur digital)';
|
||||
|
||||
const photoboxItem: any = {
|
||||
type: (photoboxArticleId && photoboxArticleId.trim()) ? 'material' : 'custom',
|
||||
quantity: 1,
|
||||
unitName: 'Stück',
|
||||
name: `${boxName}${flatSuffix}`,
|
||||
};
|
||||
|
||||
if (photoboxArticleId && photoboxArticleId.trim()) {
|
||||
photoboxItem.id = photoboxArticleId;
|
||||
} else {
|
||||
photoboxItem.description = `Event am ${new Date(booking.eventDate).toLocaleDateString('de-DE')} in ${booking.eventCity}`;
|
||||
photoboxItem.unitPrice = {
|
||||
currency: 'EUR',
|
||||
netAmount: priceConfig?.basePrice || 1,
|
||||
taxRatePercentage: 19,
|
||||
};
|
||||
}
|
||||
|
||||
lineItems.push(photoboxItem);
|
||||
|
||||
return NextResponse.json({
|
||||
booking: {
|
||||
id: booking.id,
|
||||
bookingNumber: booking.bookingNumber,
|
||||
locationId: booking.locationId,
|
||||
photoboxModel: booking.photobox?.model,
|
||||
withPrintFlat: booking.withPrintFlat,
|
||||
distance: booking.distance,
|
||||
},
|
||||
priceConfig: priceConfig ? {
|
||||
id: priceConfig.id,
|
||||
basePrice: priceConfig.basePrice,
|
||||
kmFlatRate: priceConfig.kmFlatRate,
|
||||
kmFlatRateUpTo: priceConfig.kmFlatRateUpTo,
|
||||
pricePerKm: priceConfig.pricePerKm,
|
||||
kmMultiplier: priceConfig.kmMultiplier,
|
||||
lexofficeArticleId: priceConfig.lexofficeArticleId,
|
||||
lexofficeArticleIdWithFlat: priceConfig.lexofficeArticleIdWithFlat,
|
||||
lexofficeKmFlatArticleId: priceConfig.lexofficeKmFlatArticleId,
|
||||
lexofficeKmExtraArticleId: priceConfig.lexofficeKmExtraArticleId,
|
||||
} : null,
|
||||
lineItems,
|
||||
photoboxArticleId,
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Debug Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message, stack: error.stack },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
54
app/api/bookings/[id]/quotation-pdf/route.ts
Normal file
54
app/api/bookings/[id]/quotation-pdf/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
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 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 bookingId = params.id;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
select: {
|
||||
id: true,
|
||||
bookingNumber: true,
|
||||
lexofficeOfferId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json({ error: 'Buchung nicht gefunden' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!booking.lexofficeOfferId) {
|
||||
return NextResponse.json({ error: 'Kein LexOffice Angebot vorhanden' }, { status: 404 });
|
||||
}
|
||||
|
||||
const pdfBuffer = await lexofficeService.getQuotationPDF(booking.lexofficeOfferId);
|
||||
|
||||
return new NextResponse(pdfBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="Angebot_${booking.bookingNumber}.pdf"`,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ PDF-Download Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'PDF-Download fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,39 @@ import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { nextcloudCalendar } from '@/lib/nextcloud-calendar';
|
||||
|
||||
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 booking = await prisma.booking.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
location: true,
|
||||
photobox: true,
|
||||
bookingEquipment: {
|
||||
include: { equipment: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json({ error: 'Booking not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ booking });
|
||||
} catch (error) {
|
||||
console.error('Booking GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
@@ -65,23 +98,68 @@ export async function PATCH(
|
||||
|
||||
const body = await request.json();
|
||||
const { id } = params;
|
||||
const { status } = body;
|
||||
|
||||
if (!status) {
|
||||
return NextResponse.json({ error: 'Status is required' }, { status: 400 });
|
||||
const updateData: any = {};
|
||||
|
||||
if (body.status) updateData.status = body.status;
|
||||
if (body.customerName !== undefined) updateData.customerName = body.customerName;
|
||||
if (body.customerEmail !== undefined) updateData.customerEmail = body.customerEmail;
|
||||
if (body.customerPhone !== undefined) updateData.customerPhone = body.customerPhone;
|
||||
if (body.customerAddress !== undefined) updateData.customerAddress = body.customerAddress;
|
||||
if (body.customerCity !== undefined) updateData.customerCity = body.customerCity;
|
||||
if (body.customerZip !== undefined) updateData.customerZip = body.customerZip;
|
||||
if (body.companyName !== undefined) updateData.companyName = body.companyName;
|
||||
if (body.invoiceType !== undefined) updateData.invoiceType = body.invoiceType;
|
||||
if (body.eventDate !== undefined) updateData.eventDate = new Date(body.eventDate);
|
||||
if (body.eventAddress !== undefined) updateData.eventAddress = body.eventAddress;
|
||||
if (body.eventCity !== undefined) updateData.eventCity = body.eventCity;
|
||||
if (body.eventZip !== undefined) updateData.eventZip = body.eventZip;
|
||||
if (body.eventLocation !== undefined) updateData.eventLocation = body.eventLocation;
|
||||
if (body.setupTimeStart !== undefined) updateData.setupTimeStart = body.setupTimeStart ? new Date(body.setupTimeStart) : null;
|
||||
if (body.setupTimeLatest !== undefined) updateData.setupTimeLatest = body.setupTimeLatest ? new Date(body.setupTimeLatest) : null;
|
||||
if (body.dismantleTimeEarliest !== undefined) updateData.dismantleTimeEarliest = body.dismantleTimeEarliest ? new Date(body.dismantleTimeEarliest) : null;
|
||||
if (body.dismantleTimeLatest !== undefined) updateData.dismantleTimeLatest = body.dismantleTimeLatest ? new Date(body.dismantleTimeLatest) : null;
|
||||
if (body.calculatedPrice !== undefined) updateData.calculatedPrice = body.calculatedPrice;
|
||||
if (body.notes !== undefined) updateData.notes = body.notes;
|
||||
if (body.withPrintFlat !== undefined) updateData.withPrintFlat = body.withPrintFlat;
|
||||
|
||||
const hasEquipmentUpdate = Array.isArray(body.equipmentIds);
|
||||
const hasModelUpdate = body.model !== undefined;
|
||||
|
||||
if (Object.keys(updateData).length === 0 && !hasEquipmentUpdate && !hasModelUpdate) {
|
||||
return NextResponse.json({ error: 'Keine Änderungen angegeben' }, { status: 400 });
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.update({
|
||||
where: { id },
|
||||
data: { status },
|
||||
data: updateData,
|
||||
include: {
|
||||
location: true,
|
||||
photobox: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (hasModelUpdate && booking.photoboxId) {
|
||||
await prisma.photobox.update({
|
||||
where: { id: booking.photoboxId },
|
||||
data: { model: body.model },
|
||||
});
|
||||
}
|
||||
|
||||
if (hasEquipmentUpdate) {
|
||||
await prisma.bookingEquipment.deleteMany({ where: { bookingId: id } });
|
||||
if (body.equipmentIds.length > 0) {
|
||||
await prisma.bookingEquipment.createMany({
|
||||
data: body.equipmentIds.map((eqId: string) => ({
|
||||
bookingId: id,
|
||||
equipmentId: eqId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (status === 'CANCELLED') {
|
||||
if (updateData.status === 'CANCELLED') {
|
||||
await nextcloudCalendar.removeBookingFromCalendar(booking.id);
|
||||
} else {
|
||||
await nextcloudCalendar.syncBookingToCalendar(booking);
|
||||
|
||||
165
app/api/bookings/[id]/sign/route.ts
Normal file
165
app/api/bookings/[id]/sign/route.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { generateContractFromTemplate } from '@/lib/pdf-template-service';
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const bookingId = params.id;
|
||||
const body = await request.json();
|
||||
const { signatureData } = body;
|
||||
|
||||
if (!signatureData) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Signatur-Daten fehlen' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
location: true,
|
||||
photobox: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Buchung nicht gefunden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (booking.contractSigned) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Vertrag wurde bereits unterschrieben' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const clientIp = request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
'unknown';
|
||||
|
||||
let priceConfig = null;
|
||||
if (booking.photobox?.model && booking.locationId) {
|
||||
priceConfig = await prisma.priceConfig.findUnique({
|
||||
where: {
|
||||
locationId_model: {
|
||||
locationId: booking.locationId,
|
||||
model: booking.photobox.model,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const bookingWithPriceConfig = {
|
||||
...booking,
|
||||
priceConfig,
|
||||
};
|
||||
|
||||
const contractPdf = await generateContractFromTemplate(
|
||||
bookingWithPriceConfig,
|
||||
booking.location,
|
||||
booking.photobox,
|
||||
signatureData
|
||||
);
|
||||
|
||||
const updatedBooking = await prisma.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: {
|
||||
contractSigned: true,
|
||||
contractSignedAt: new Date(),
|
||||
contractSignedOnline: true,
|
||||
contractSignatureData: signatureData,
|
||||
contractSignedBy: booking.customerName,
|
||||
contractSignedIp: clientIp,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
type: 'CONTRACT_SIGNED',
|
||||
title: 'Vertrag unterschrieben',
|
||||
message: `${booking.customerName} hat den Vertrag für Buchung ${booking.bookingNumber} online unterschrieben.`,
|
||||
metadata: {
|
||||
bookingId: booking.id,
|
||||
bookingNumber: booking.bookingNumber,
|
||||
signedOnline: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Vertrag online unterschrieben: ${booking.bookingNumber}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
booking: {
|
||||
id: updatedBooking.id,
|
||||
bookingNumber: updatedBooking.bookingNumber,
|
||||
contractSigned: updatedBooking.contractSigned,
|
||||
contractSignedAt: updatedBooking.contractSignedAt,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Signatur-Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Signatur fehlgeschlagen' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const bookingId = params.id;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
select: {
|
||||
id: true,
|
||||
bookingNumber: true,
|
||||
customerName: true,
|
||||
eventDate: true,
|
||||
eventLocation: true,
|
||||
contractSigned: true,
|
||||
contractSignedAt: true,
|
||||
contractSignedOnline: true,
|
||||
calculatedPrice: true,
|
||||
photobox: {
|
||||
select: {
|
||||
model: true,
|
||||
},
|
||||
},
|
||||
location: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Buchung nicht gefunden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ booking });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Buchungs-Abruf Fehler:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Fehler beim Abrufen der Buchung' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,15 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
if (Array.isArray(body.equipmentIds) && body.equipmentIds.length > 0) {
|
||||
await prisma.bookingEquipment.createMany({
|
||||
data: body.equipmentIds.map((eqId: string) => ({
|
||||
bookingId: booking.id,
|
||||
equipmentId: eqId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await nextcloudCalendar.syncBookingToCalendar(booking);
|
||||
} catch (calError) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { z } from 'zod';
|
||||
import { DistanceCalculator } from '@/lib/distance-calculator';
|
||||
import { PriceCalculator } from '@/lib/price-calculator';
|
||||
import { bookingAutomationService } from '@/lib/booking-automation';
|
||||
|
||||
const bookingSchema = z.object({
|
||||
locationSlug: z.string(),
|
||||
@@ -92,7 +95,62 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
const calculatedPrice = priceConfig ? priceConfig.basePrice : 0;
|
||||
if (!priceConfig) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Preiskonfiguration nicht gefunden' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
let distance: number | null = null;
|
||||
let calculatedPrice = priceConfig.basePrice;
|
||||
|
||||
if (location.warehouseAddress && location.warehouseZip && location.warehouseCity) {
|
||||
const warehouseAddress = DistanceCalculator.formatAddress(
|
||||
location.warehouseAddress,
|
||||
location.warehouseZip,
|
||||
location.warehouseCity
|
||||
);
|
||||
const eventAddress = DistanceCalculator.formatAddress(
|
||||
data.eventAddress,
|
||||
data.eventZip,
|
||||
data.eventCity
|
||||
);
|
||||
|
||||
const distanceResult = await DistanceCalculator.calculateDistance(
|
||||
warehouseAddress,
|
||||
eventAddress
|
||||
);
|
||||
|
||||
if (distanceResult) {
|
||||
distance = distanceResult.distance;
|
||||
|
||||
const priceBreakdown = PriceCalculator.calculateTotalPrice(
|
||||
priceConfig.basePrice,
|
||||
distance,
|
||||
{
|
||||
basePrice: priceConfig.basePrice,
|
||||
kmFlatRate: priceConfig.kmFlatRate,
|
||||
kmFlatRateUpTo: priceConfig.kmFlatRateUpTo,
|
||||
pricePerKm: priceConfig.pricePerKm,
|
||||
kmMultiplier: priceConfig.kmMultiplier,
|
||||
}
|
||||
);
|
||||
|
||||
calculatedPrice = priceBreakdown.totalPrice;
|
||||
|
||||
console.log('📍 Distanzberechnung:', {
|
||||
from: warehouseAddress,
|
||||
to: eventAddress,
|
||||
distance: `${distance}km`,
|
||||
breakdown: PriceCalculator.formatPriceBreakdown(priceBreakdown),
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ Distanzberechnung fehlgeschlagen, verwende nur Grundpreis');
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Keine Lager-Adresse konfiguriert, verwende nur Grundpreis');
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.create({
|
||||
data: {
|
||||
@@ -117,6 +175,7 @@ export async function POST(request: NextRequest) {
|
||||
setupTimeLatest: new Date(data.setupTimeLatest),
|
||||
dismantleTimeEarliest: data.dismantleTimeEarliest ? new Date(data.dismantleTimeEarliest) : null,
|
||||
dismantleTimeLatest: data.dismantleTimeLatest ? new Date(data.dismantleTimeLatest) : null,
|
||||
distance,
|
||||
calculatedPrice,
|
||||
notes: data.notes,
|
||||
},
|
||||
@@ -138,6 +197,12 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
// 🤖 Automatische Post-Booking Aktionen (E-Mail + Kalender)
|
||||
console.log('📢 Starte automatische Aktionen...');
|
||||
bookingAutomationService.runPostBookingActions(booking.id).catch(err => {
|
||||
console.error('⚠️ Automatische Aktionen fehlgeschlagen:', err);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
booking: {
|
||||
@@ -174,7 +239,13 @@ export async function GET(request: NextRequest) {
|
||||
const where: any = {};
|
||||
|
||||
if (status) {
|
||||
where.status = status;
|
||||
// Support multiple statuses separated by comma
|
||||
const statuses = status.split(',').map(s => s.trim());
|
||||
if (statuses.length > 1) {
|
||||
where.status = { in: statuses };
|
||||
} else {
|
||||
where.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
if (locationSlug) {
|
||||
|
||||
76
app/api/driver/tour-stops/[id]/status/route.ts
Normal file
76
app/api/driver/tour-stops/[id]/status/route.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session || session.user.role !== 'DRIVER') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { status } = body;
|
||||
|
||||
if (!status) {
|
||||
return NextResponse.json({ error: 'Status is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const tourStop = await prisma.tourStop.findUnique({
|
||||
where: { id: params.id },
|
||||
include: {
|
||||
tour: {
|
||||
select: {
|
||||
driverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tourStop) {
|
||||
return NextResponse.json({ error: 'Tour stop not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (tourStop.tour.driverId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updateData: any = { status };
|
||||
|
||||
switch (status) {
|
||||
case 'ARRIVED':
|
||||
updateData.arrivedAt = new Date();
|
||||
break;
|
||||
case 'SETUP_IN_PROGRESS':
|
||||
updateData.setupStartedAt = new Date();
|
||||
break;
|
||||
case 'SETUP_COMPLETE':
|
||||
updateData.setupCompleteAt = new Date();
|
||||
break;
|
||||
case 'PICKUP_IN_PROGRESS':
|
||||
updateData.pickupStartedAt = new Date();
|
||||
break;
|
||||
case 'PICKUP_COMPLETE':
|
||||
updateData.pickupCompleteAt = new Date();
|
||||
break;
|
||||
}
|
||||
|
||||
const updatedStop = await prisma.tourStop.update({
|
||||
where: { id: params.id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json({ tourStop: updatedStop });
|
||||
} catch (error: any) {
|
||||
console.error('Status update error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to update status' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
62
app/api/driver/tours/[id]/route.ts
Normal file
62
app/api/driver/tours/[id]/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(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session || session.user.role !== 'DRIVER') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const tour = await prisma.tour.findUnique({
|
||||
where: {
|
||||
id: params.id,
|
||||
driverId: session.user.id,
|
||||
},
|
||||
include: {
|
||||
tourStops: {
|
||||
include: {
|
||||
booking: {
|
||||
include: {
|
||||
photobox: {
|
||||
select: {
|
||||
model: true,
|
||||
serialNumber: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
photos: {
|
||||
select: {
|
||||
id: true,
|
||||
photoType: true,
|
||||
fileName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
stopOrder: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!tour) {
|
||||
return NextResponse.json({ error: 'Tour not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
30
app/api/equipment/route.ts
Normal file
30
app/api/equipment/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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 || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const equipment = await prisma.equipment.findMany({
|
||||
where: { status: 'AVAILABLE' },
|
||||
orderBy: { name: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
type: true,
|
||||
price: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ equipment });
|
||||
} catch (error) {
|
||||
console.error('Equipment GET error:', error);
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
data: {
|
||||
tourId: tour.id,
|
||||
status: 'ASSIGNED',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -145,6 +146,13 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
// Create TourStops for each booking
|
||||
const fullBookings = await prisma.booking.findMany({
|
||||
where: { id: { in: bookingIds } },
|
||||
include: { setupWindows: true },
|
||||
orderBy: { setupTimeStart: 'asc' },
|
||||
});
|
||||
|
||||
try {
|
||||
// For route optimization, use the selected setup window time if available
|
||||
const stopsWithSetupTimes = bookings.map((booking: any) => {
|
||||
@@ -182,8 +190,38 @@ export async function POST(request: NextRequest) {
|
||||
estimatedDuration: routeData.totalDuration,
|
||||
},
|
||||
});
|
||||
|
||||
// Create TourStops based on optimized order
|
||||
const optimizedOrder = routeData.optimizedOrder || fullBookings.map((_, i) => i);
|
||||
for (let i = 0; i < optimizedOrder.length; i++) {
|
||||
const orderIndex = optimizedOrder[i];
|
||||
const booking = fullBookings[orderIndex];
|
||||
|
||||
await prisma.tourStop.create({
|
||||
data: {
|
||||
tourId: tour.id,
|
||||
bookingId: booking.id,
|
||||
stopOrder: i + 1,
|
||||
stopType: 'DELIVERY',
|
||||
status: 'PENDING',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (routeError) {
|
||||
console.error('Route optimization error:', routeError);
|
||||
|
||||
// If route optimization fails, create TourStops in simple order
|
||||
for (let i = 0; i < fullBookings.length; i++) {
|
||||
await prisma.tourStop.create({
|
||||
data: {
|
||||
tourId: tour.id,
|
||||
bookingId: fullBookings[i].id,
|
||||
stopOrder: i + 1,
|
||||
stopType: 'DELIVERY',
|
||||
status: 'PENDING',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user