Initial commit - SaveTheMoment Atlas Basis-Setup
This commit is contained in:
727
components/BookingDetail.tsx
Normal file
727
components/BookingDetail.tsx
Normal file
@@ -0,0 +1,727 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FiCalendar, FiMapPin, FiUser, FiMail, FiPhone, FiCamera, FiTruck, FiClock, FiPlus, FiX, FiCheck, FiStar, FiFileText, FiDollarSign, FiRefreshCw, FiEdit } from 'react-icons/fi';
|
||||
|
||||
interface BookingDetailProps {
|
||||
booking: any;
|
||||
emails: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function BookingDetail({ booking, emails, user }: BookingDetailProps) {
|
||||
const [setupWindows, setSetupWindows] = useState<any[]>([]);
|
||||
const [showAddWindow, setShowAddWindow] = useState(false);
|
||||
const [newWindow, setNewWindow] = useState({
|
||||
setupDate: '',
|
||||
setupTimeStart: '',
|
||||
setupTimeEnd: '',
|
||||
preferred: false,
|
||||
notes: '',
|
||||
});
|
||||
const [selectedStatus, setSelectedStatus] = useState(booking.status);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [actionMessage, setActionMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchSetupWindows();
|
||||
}, [booking.id]);
|
||||
|
||||
const fetchSetupWindows = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/setup-windows`);
|
||||
const data = await res.json();
|
||||
setSetupWindows(data.setupWindows || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch setup windows:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
if (!confirm(`Status wirklich zu "${getStatusLabel(newStatus)}" ändern?`)) {
|
||||
setSelectedStatus(booking.status);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setActionMessage('✅ Status erfolgreich geändert');
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
alert('Fehler beim Ändern des Status');
|
||||
setSelectedStatus(booking.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Status update error:', error);
|
||||
alert('Fehler beim Ändern des Status');
|
||||
setSelectedStatus(booking.status);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCalendarSync = async () => {
|
||||
setSyncing(true);
|
||||
setActionMessage('');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/calendar/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'sync-booking', bookingId: booking.id }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
setActionMessage('✅ Mit Kalender synchronisiert');
|
||||
setTimeout(() => setActionMessage(''), 3000);
|
||||
} else {
|
||||
setActionMessage('❌ Sync fehlgeschlagen: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Calendar sync error:', error);
|
||||
setActionMessage('❌ Sync fehlgeschlagen');
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateContract = async () => {
|
||||
setActionMessage('📄 Vertrag wird generiert...');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/contract`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
|
||||
if (data.contractUrl) {
|
||||
setActionMessage('✅ Vertrag generiert! Wird heruntergeladen...');
|
||||
|
||||
const downloadRes = await fetch(data.contractUrl);
|
||||
const blob = await downloadRes.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = data.filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
setTimeout(() => setActionMessage(''), 3000);
|
||||
}
|
||||
} else {
|
||||
const error = await res.json();
|
||||
setActionMessage('❌ Fehler: ' + (error.error || 'Unbekannter Fehler'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Contract generation error:', error);
|
||||
setActionMessage('❌ Fehler beim Generieren');
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateInvoice = async () => {
|
||||
setActionMessage('💰 Rechnung wird erstellt...');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/invoice`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setActionMessage('✅ Rechnung in lexoffice erstellt');
|
||||
if (data.invoiceUrl) {
|
||||
window.open(data.invoiceUrl, '_blank');
|
||||
}
|
||||
setTimeout(() => setActionMessage(''), 3000);
|
||||
} else {
|
||||
setActionMessage('❌ Fehler beim Erstellen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invoice generation error:', error);
|
||||
setActionMessage('❌ Fehler beim Erstellen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendContract = async () => {
|
||||
if (!booking.contractGenerated) {
|
||||
alert('Bitte generieren Sie zuerst den Vertrag!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Vertrag wirklich an ${booking.customerEmail} senden?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActionMessage('📧 Vertrag wird versendet...');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/contract/send`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
setActionMessage('✅ Vertrag per E-Mail versendet!');
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
setActionMessage('❌ ' + (data.error || 'Fehler beim Versenden'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contract send error:', error);
|
||||
setActionMessage('❌ Fehler beim Versenden');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddWindow = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const setupDateObj = new Date(newWindow.setupDate);
|
||||
const [startHours, startMinutes] = newWindow.setupTimeStart.split(':');
|
||||
const [endHours, endMinutes] = newWindow.setupTimeEnd.split(':');
|
||||
|
||||
const setupTimeStart = new Date(setupDateObj);
|
||||
setupTimeStart.setHours(parseInt(startHours), parseInt(startMinutes), 0, 0);
|
||||
|
||||
const setupTimeEnd = new Date(setupDateObj);
|
||||
setupTimeEnd.setHours(parseInt(endHours), parseInt(endMinutes), 0, 0);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/setup-windows`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
setupDate: setupDateObj.toISOString(),
|
||||
setupTimeStart: setupTimeStart.toISOString(),
|
||||
setupTimeEnd: setupTimeEnd.toISOString(),
|
||||
preferred: newWindow.preferred,
|
||||
notes: newWindow.notes,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setShowAddWindow(false);
|
||||
setNewWindow({
|
||||
setupDate: '',
|
||||
setupTimeStart: '',
|
||||
setupTimeEnd: '',
|
||||
preferred: false,
|
||||
notes: '',
|
||||
});
|
||||
fetchSetupWindows();
|
||||
} else {
|
||||
alert('Fehler beim Hinzufügen des Zeitfensters');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add setup window:', error);
|
||||
alert('Fehler beim Hinzufügen des Zeitfensters');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteWindow = async (windowId: string) => {
|
||||
if (!confirm('Zeitfenster wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/setup-windows?windowId=${windowId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
fetchSetupWindows();
|
||||
} else {
|
||||
alert('Fehler beim Löschen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete setup window:', error);
|
||||
alert('Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTogglePreferred = async (windowId: string, currentPreferred: boolean) => {
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/setup-windows`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
windowId,
|
||||
preferred: !currentPreferred,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
fetchSetupWindows();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update setup window:', error);
|
||||
}
|
||||
};
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
||||
case 'CONFIRMED': return 'bg-green-500/20 text-green-400 border-green-500/50';
|
||||
case 'COMPLETED': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
|
||||
case 'CANCELLED': return 'bg-red-500/20 text-red-400 border-red-500/50';
|
||||
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: any = {
|
||||
RESERVED: 'Reserviert',
|
||||
CONFIRMED: 'Bestätigt',
|
||||
COMPLETED: 'Abgeschlossen',
|
||||
CANCELLED: 'Storniert',
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<Link href="/dashboard/bookings" className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors">
|
||||
← Zurück zu Buchungen
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">Buchung {booking.bookingNumber}</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Status Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-white">Status</h3>
|
||||
<div className={`px-4 py-2 border-2 rounded-lg font-semibold ${getStatusColor(booking.status)}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Customer Data Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiUser /> Kundendaten
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
<p className="text-white">{booking.customerName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">E-Mail</label>
|
||||
<p className="text-white">{booking.customerEmail}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Telefon</label>
|
||||
<p className="text-white">{booking.customerPhone}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Rechnungsart</label>
|
||||
<p className="text-white">{booking.invoiceType === 'PRIVATE' ? 'Privat' : 'Firma'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Details Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCalendar /> Event-Details
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Datum</label>
|
||||
<p className="text-white">{new Date(booking.eventDate).toLocaleDateString('de-DE')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Standort</label>
|
||||
<p className="text-white">{booking.location?.name || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Event-Adresse</label>
|
||||
<p className="text-white">{booking.eventAddress}</p>
|
||||
<p className="text-gray-400 text-sm">{booking.eventZip} {booking.eventCity}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Event-Location</label>
|
||||
<p className="text-white">{booking.eventLocation}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Setup Windows Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<FiClock /> Aufbau-Zeitfenster
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowAddWindow(true)}
|
||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-all flex items-center gap-1"
|
||||
>
|
||||
<FiPlus size={14} /> Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{setupWindows.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-4">
|
||||
Keine Aufbau-Zeitfenster definiert. Standard-Aufbauzeit am Event-Tag.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{setupWindows.map((window: any) => (
|
||||
<div
|
||||
key={window.id}
|
||||
className={`p-4 rounded-lg border-2 ${
|
||||
window.selected
|
||||
? 'bg-green-500/10 border-green-500/50'
|
||||
: window.preferred
|
||||
? 'bg-purple-500/10 border-purple-500/50'
|
||||
: 'bg-gray-700/30 border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-white font-semibold">
|
||||
{new Date(window.setupDate).toLocaleDateString('de-DE')}
|
||||
</p>
|
||||
{window.selected && (
|
||||
<span className="px-2 py-0.5 bg-green-600 text-white text-xs rounded-full flex items-center gap-1">
|
||||
<FiCheck size={12} /> Ausgewählt
|
||||
</span>
|
||||
)}
|
||||
{window.preferred && !window.selected && (
|
||||
<span className="px-2 py-0.5 bg-purple-600 text-white text-xs rounded-full flex items-center gap-1">
|
||||
<FiStar size={12} /> Bevorzugt
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-300 text-sm">
|
||||
{new Date(window.setupTimeStart).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}{' '}
|
||||
-{' '}
|
||||
{new Date(window.setupTimeEnd).toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</p>
|
||||
{window.notes && (
|
||||
<p className="text-gray-400 text-xs mt-1">{window.notes}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleTogglePreferred(window.id, window.preferred)}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
window.preferred
|
||||
? 'text-purple-400 hover:text-purple-300'
|
||||
: 'text-gray-500 hover:text-purple-400'
|
||||
}`}
|
||||
title={window.preferred ? 'Als bevorzugt entfernen' : 'Als bevorzugt markieren'}
|
||||
>
|
||||
<FiStar size={16} fill={window.preferred ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteWindow(window.id)}
|
||||
className="p-1.5 text-red-400 hover:text-red-300 rounded transition-colors"
|
||||
title="Löschen"
|
||||
>
|
||||
<FiX size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddWindow && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 rounded-xl shadow-2xl max-w-lg w-full p-6 border border-gray-700">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Aufbau-Zeitfenster hinzufügen</h3>
|
||||
<form onSubmit={handleAddWindow} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Aufbau-Datum *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={newWindow.setupDate}
|
||||
onChange={(e) => setNewWindow({ ...newWindow, setupDate: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Start-Zeit *
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={newWindow.setupTimeStart}
|
||||
onChange={(e) => setNewWindow({ ...newWindow, setupTimeStart: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
End-Zeit *
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={newWindow.setupTimeEnd}
|
||||
onChange={(e) => setNewWindow({ ...newWindow, setupTimeEnd: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-gray-300 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newWindow.preferred}
|
||||
onChange={(e) => setNewWindow({ ...newWindow, preferred: e.target.checked })}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span className="text-sm">Als bevorzugtes Zeitfenster markieren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Notizen (optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={newWindow.notes}
|
||||
onChange={(e) => setNewWindow({ ...newWindow, notes: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
rows={2}
|
||||
placeholder="z.B. Besondere Zugangshinweise..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddWindow(false);
|
||||
setNewWindow({
|
||||
setupDate: '',
|
||||
setupTimeStart: '',
|
||||
setupTimeEnd: '',
|
||||
preferred: false,
|
||||
notes: '',
|
||||
});
|
||||
}}
|
||||
className="flex-1 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 font-medium"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notes Card */}
|
||||
{booking.notes && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Notizen</h3>
|
||||
<p className="text-gray-300 whitespace-pre-wrap">{booking.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Correspondence Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<FiMail /> E-Mail-Korrespondenz
|
||||
</h3>
|
||||
<span className="text-sm text-gray-400">{emails.length} E-Mails</span>
|
||||
</div>
|
||||
|
||||
{emails.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-4">Keine E-Mails vorhanden</p>
|
||||
) : (
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto">
|
||||
{emails.map((email: any) => (
|
||||
<div key={email.id} className="bg-gray-700/30 rounded-lg p-4 border border-gray-600">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-semibold">{email.subject}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Von: {email.fromAddress} • {new Date(email.receivedAt).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-300 mt-2 line-clamp-3">
|
||||
{email.textContent?.substring(0, 200)}...
|
||||
</div>
|
||||
<button className="mt-2 text-sm text-red-400 hover:text-red-300 transition-colors">
|
||||
Details anzeigen →
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-600">
|
||||
<button className="w-full px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 shadow-lg transition-all font-medium">
|
||||
<FiMail className="inline mr-2" />
|
||||
Neue E-Mail schreiben
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Photobox Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCamera /> Fotobox
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Modell</label>
|
||||
<p className="text-white">{booking.photobox?.model || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Seriennummer</label>
|
||||
<p className="text-white">{booking.photobox?.serialNumber || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Preis</h3>
|
||||
<p className="text-3xl font-bold text-green-400">{booking.calculatedPrice} €</p>
|
||||
</div>
|
||||
|
||||
{/* Actions Card */}
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiEdit /> Aktionen
|
||||
</h3>
|
||||
|
||||
{actionMessage && (
|
||||
<div className="mb-4 p-3 bg-blue-500/20 border border-blue-500/50 rounded-lg text-sm text-blue-300">
|
||||
{actionMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Status ändern</label>
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => {
|
||||
setSelectedStatus(e.target.value);
|
||||
handleStatusChange(e.target.value);
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 text-sm"
|
||||
>
|
||||
<option value="RESERVED">Reserviert</option>
|
||||
<option value="CONFIRMED">Bestätigt</option>
|
||||
<option value="COMPLETED">Abgeschlossen</option>
|
||||
<option value="CANCELLED">Storniert</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCalendarSync}
|
||||
disabled={syncing}
|
||||
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{syncing ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Synchronisiere...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FiRefreshCw />
|
||||
Kalender-Sync
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleGenerateContract}
|
||||
className="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiFileText />
|
||||
Vertrag generieren
|
||||
</button>
|
||||
|
||||
{booking.contractGenerated && (
|
||||
<button
|
||||
onClick={handleSendContract}
|
||||
className="w-full px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiMail />
|
||||
Vertrag per E-Mail senden
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleGenerateInvoice}
|
||||
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiDollarSign />
|
||||
Rechnung erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tour Card */}
|
||||
{booking.tour && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiTruck /> Tour
|
||||
</h3>
|
||||
<div>
|
||||
{booking.tour.driver ? (
|
||||
<p className="text-white">Fahrer: {booking.tour.driver.name}</p>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch kein Fahrer zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
375
components/BookingDetail.tsx.backup
Normal file
375
components/BookingDetail.tsx.backup
Normal file
@@ -0,0 +1,375 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FiCalendar, FiMapPin, FiUser, FiMail, FiPhone, FiEdit, FiSave, FiX, FiCamera, FiTruck, FiFileText } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { formatDate, formatDateTime } from '@/lib/date-utils';
|
||||
import ContractSection from './ContractSection';
|
||||
|
||||
interface BookingDetailProps {
|
||||
booking: any;
|
||||
emails: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function BookingDetail({ booking, emails, user }: BookingDetailProps) {
|
||||
const router = useRouter();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [formData, setFormData] = useState(booking);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Gespeichert!');
|
||||
setEditing(false);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Speichern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Speichern');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
if (!confirm(`Status zu "${newStatus}" ändern?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Status geändert!');
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
||||
case 'CONFIRMED': return 'bg-green-500/20 text-green-400 border-green-500/50';
|
||||
case 'COMPLETED': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
|
||||
case 'CANCELLED': return 'bg-red-500/20 text-red-400 border-red-500/50';
|
||||
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: any = {
|
||||
RESERVED: 'Reserviert',
|
||||
CONFIRMED: 'Bestätigt',
|
||||
COMPLETED: 'Abgeschlossen',
|
||||
CANCELLED: 'Storniert',
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<Link href="/dashboard/bookings" className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors">
|
||||
← Zurück zu Buchungen
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">Buchung {booking.bookingNumber}</h2>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{!editing ? (
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-gray-600 to-gray-700 text-white rounded-lg hover:from-gray-700 hover:to-gray-800 shadow-lg transition-all"
|
||||
>
|
||||
<FiEdit /> Bearbeiten
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditing(false);
|
||||
setFormData(booking);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<FiX /> Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 disabled:opacity-50 shadow-lg transition-all"
|
||||
>
|
||||
<FiSave /> {saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-bold text-white">Status</h3>
|
||||
<div className={`px-4 py-2 border-2 rounded-lg font-semibold ${getStatusColor(booking.status)}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{booking.status === 'RESERVED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CONFIRMED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-lg hover:from-green-700 hover:to-emerald-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Bestätigen
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'CONFIRMED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('COMPLETED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg hover:from-blue-700 hover:to-cyan-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Abschließen
|
||||
</button>
|
||||
)}
|
||||
{['RESERVED', 'CONFIRMED'].includes(booking.status) && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CANCELLED')}
|
||||
className="px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 shadow-lg transition-all"
|
||||
>
|
||||
Stornieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiUser /> Kundendaten
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={formData.customerName}
|
||||
onChange={(e) => setFormData({ ...formData, customerName: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">E-Mail</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="email"
|
||||
value={formData.customerEmail}
|
||||
onChange={(e) => setFormData({ ...formData, customerEmail: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Telefon</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.customerPhone}
|
||||
onChange={(e) => setFormData({ ...formData, customerPhone: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerPhone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Rechnungsart</label>
|
||||
<p className="text-white">{booking.invoiceType === 'BUSINESS' ? 'Geschäftlich' : 'Privat'}</p>
|
||||
</div>
|
||||
|
||||
{booking.companyName && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Firma</label>
|
||||
<p className="text-white">{booking.companyName}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCalendar /> Event-Details
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Datum</label>
|
||||
<p className="text-white">{formatDate(booking.eventDate)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Standort</label>
|
||||
<p className="text-white">{booking.location.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Event-Adresse</label>
|
||||
<p className="text-white">{booking.eventAddress}</p>
|
||||
<p className="text-gray-400 text-sm">{booking.eventZip} {booking.eventCity}</p>
|
||||
</div>
|
||||
|
||||
{booking.eventLocation && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Location-Name</label>
|
||||
<p className="text-white">{booking.eventLocation}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau ab</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeStart)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau spätestens</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeLatest)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.notes && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiFileText /> Notizen
|
||||
</h3>
|
||||
<p className="text-gray-300 whitespace-pre-wrap">{booking.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{emails.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiMail /> E-Mail-Verlauf
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{emails.map((email) => (
|
||||
<div key={email.id} className="border border-gray-600 bg-gray-700/50 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-white">{email.subject}</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
{formatDate(email.receivedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-2">Von: {email.from}</p>
|
||||
{email.parsed && (
|
||||
<span className="inline-block px-2 py-1 bg-green-500/20 text-green-400 border border-green-500/50 text-xs rounded">
|
||||
Automatisch verarbeitet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCamera /> Fotobox
|
||||
</h3>
|
||||
{booking.photobox ? (
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm mb-2">Modell</p>
|
||||
<p className="text-white font-semibold mb-4">{booking.photobox.model}</p>
|
||||
<p className="text-gray-400 text-sm mb-2">Seriennummer</p>
|
||||
<p className="text-white">{booking.photobox.serialNumber}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch nicht zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ContractSection booking={booking} onRefresh={() => router.refresh()} />
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Preis</h3>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-white">
|
||||
{booking.calculatedPrice || 0}€
|
||||
</p>
|
||||
{booking.distance && (
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
+ {booking.distance} km Anfahrt
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Vertrag</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-300">Unterschrieben</span>
|
||||
<span className={`font-semibold ${booking.contractSigned ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{booking.contractSigned ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
</div>
|
||||
{booking.contractSignedAt && (
|
||||
<p className="text-sm text-gray-400">
|
||||
am {formatDate(booking.contractSignedAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.tours && booking.tours.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiTruck /> Tour
|
||||
</h3>
|
||||
{booking.tours.map((tour: any) => (
|
||||
<div key={tour.id}>
|
||||
{tour.driver ? (
|
||||
<p className="text-white">Fahrer: {tour.driver.name}</p>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch kein Fahrer zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
375
components/BookingDetail.tsx.bak
Normal file
375
components/BookingDetail.tsx.bak
Normal file
@@ -0,0 +1,375 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FiCalendar, FiMapPin, FiUser, FiMail, FiPhone, FiEdit, FiSave, FiX, FiCamera, FiTruck, FiFileText } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { formatDate, formatDateTime } from '@/lib/date-utils';
|
||||
import ContractSection from './ContractSection';
|
||||
|
||||
interface BookingDetailProps {
|
||||
booking: any;
|
||||
emails: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function BookingDetail({ booking, emails, user }: BookingDetailProps) {
|
||||
const router = useRouter();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [formData, setFormData] = useState(booking);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Gespeichert!');
|
||||
setEditing(false);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Speichern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Speichern');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
if (!confirm(`Status zu "${newStatus}" ändern?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Status geändert!');
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
||||
case 'CONFIRMED': return 'bg-green-500/20 text-green-400 border-green-500/50';
|
||||
case 'COMPLETED': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
|
||||
case 'CANCELLED': return 'bg-red-500/20 text-red-400 border-red-500/50';
|
||||
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: any = {
|
||||
RESERVED: 'Reserviert',
|
||||
CONFIRMED: 'Bestätigt',
|
||||
COMPLETED: 'Abgeschlossen',
|
||||
CANCELLED: 'Storniert',
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<Link href="/dashboard/bookings" className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors">
|
||||
← Zurück zu Buchungen
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">Buchung {booking.bookingNumber}</h2>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{!editing ? (
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-gray-600 to-gray-700 text-white rounded-lg hover:from-gray-700 hover:to-gray-800 shadow-lg transition-all"
|
||||
>
|
||||
<FiEdit /> Bearbeiten
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditing(false);
|
||||
setFormData(booking);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<FiX /> Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 disabled:opacity-50 shadow-lg transition-all"
|
||||
>
|
||||
<FiSave /> {saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-bold text-white">Status</h3>
|
||||
<div className={`px-4 py-2 border-2 rounded-lg font-semibold ${getStatusColor(booking.status)}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{booking.status === 'RESERVED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CONFIRMED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-lg hover:from-green-700 hover:to-emerald-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Bestätigen
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'CONFIRMED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('COMPLETED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg hover:from-blue-700 hover:to-cyan-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Abschließen
|
||||
</button>
|
||||
)}
|
||||
{['RESERVED', 'CONFIRMED'].includes(booking.status) && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CANCELLED')}
|
||||
className="px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 shadow-lg transition-all"
|
||||
>
|
||||
Stornieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiUser /> Kundendaten
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={formData.customerName}
|
||||
onChange={(e) => setFormData({ ...formData, customerName: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">E-Mail</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="email"
|
||||
value={formData.customerEmail}
|
||||
onChange={(e) => setFormData({ ...formData, customerEmail: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Telefon</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.customerPhone}
|
||||
onChange={(e) => setFormData({ ...formData, customerPhone: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerPhone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Rechnungsart</label>
|
||||
<p className="text-white">{booking.invoiceType === 'BUSINESS' ? 'Geschäftlich' : 'Privat'}</p>
|
||||
</div>
|
||||
|
||||
{booking.companyName && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Firma</label>
|
||||
<p className="text-white">{booking.companyName}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCalendar /> Event-Details
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Datum</label>
|
||||
<p className="text-white">{formatDate(booking.eventDate)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Standort</label>
|
||||
<p className="text-white">{booking.location.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Event-Adresse</label>
|
||||
<p className="text-white">{booking.eventAddress}</p>
|
||||
<p className="text-gray-400 text-sm">{booking.eventZip} {booking.eventCity}</p>
|
||||
</div>
|
||||
|
||||
{booking.eventLocation && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Location-Name</label>
|
||||
<p className="text-white">{booking.eventLocation}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau ab</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeStart)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau spätestens</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeLatest)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.notes && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiFileText /> Notizen
|
||||
</h3>
|
||||
<p className="text-gray-300 whitespace-pre-wrap">{booking.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{emails.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiMail /> E-Mail-Verlauf
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{emails.map((email) => (
|
||||
<div key={email.id} className="border border-gray-600 bg-gray-700/50 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-white">{email.subject}</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
{formatDate(email.receivedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-2">Von: {email.from}</p>
|
||||
{email.parsed && (
|
||||
<span className="inline-block px-2 py-1 bg-green-500/20 text-green-400 border border-green-500/50 text-xs rounded">
|
||||
Automatisch verarbeitet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCamera /> Fotobox
|
||||
</h3>
|
||||
{booking.photobox ? (
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm mb-2">Modell</p>
|
||||
<p className="text-white font-semibold mb-4">{booking.photobox.model}</p>
|
||||
<p className="text-gray-400 text-sm mb-2">Seriennummer</p>
|
||||
<p className="text-white">{booking.photobox.serialNumber}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch nicht zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ContractSection booking={booking} onRefresh={() => router.refresh()} />
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Preis</h3>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-white">
|
||||
{booking.calculatedPrice || 0}€
|
||||
</p>
|
||||
{booking.distance && (
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
+ {booking.distance} km Anfahrt
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Vertrag</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-300">Unterschrieben</span>
|
||||
<span className={`font-semibold ${booking.contractSigned ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{booking.contractSigned ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
</div>
|
||||
{booking.contractSignedAt && (
|
||||
<p className="text-sm text-gray-400">
|
||||
am {formatDate(booking.contractSignedAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.tours && booking.tours.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiTruck /> Tour
|
||||
</h3>
|
||||
{booking.tours.map((tour: any) => (
|
||||
<div key={tour.id}>
|
||||
{tour.driver ? (
|
||||
<p className="text-white">Fahrer: {tour.driver.name}</p>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch kein Fahrer zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
372
components/BookingDetail.tsx.bak2
Normal file
372
components/BookingDetail.tsx.bak2
Normal file
@@ -0,0 +1,372 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FiCalendar, FiMapPin, FiUser, FiMail, FiPhone, FiEdit, FiSave, FiX, FiCamera, FiTruck, FiFileText } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { formatDate, formatDateTime } from '@/lib/date-utils';
|
||||
import ContractSection from './ContractSection';
|
||||
|
||||
interface BookingDetailProps {
|
||||
booking: any;
|
||||
emails: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function BookingDetail({ booking, emails, user }: BookingDetailProps) {
|
||||
const router = useRouter();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [formData, setFormData] = useState(booking);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Gespeichert!');
|
||||
setEditing(false);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Speichern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Speichern');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
if (!confirm(`Status zu "${newStatus}" ändern?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Status geändert!');
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
||||
case 'CONFIRMED': return 'bg-green-500/20 text-green-400 border-green-500/50';
|
||||
case 'COMPLETED': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
|
||||
case 'CANCELLED': return 'bg-red-500/20 text-red-400 border-red-500/50';
|
||||
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: any = {
|
||||
RESERVED: 'Reserviert',
|
||||
CONFIRMED: 'Bestätigt',
|
||||
COMPLETED: 'Abgeschlossen',
|
||||
CANCELLED: 'Storniert',
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<Link href="/dashboard/bookings" className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors">
|
||||
← Zurück zu Buchungen
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">Buchung {booking.bookingNumber}</h2>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{!editing ? (
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-gray-600 to-gray-700 text-white rounded-lg hover:from-gray-700 hover:to-gray-800 shadow-lg transition-all"
|
||||
>
|
||||
<FiEdit /> Bearbeiten
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditing(false);
|
||||
setFormData(booking);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<FiX /> Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 disabled:opacity-50 shadow-lg transition-all"
|
||||
>
|
||||
<FiSave /> {saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-bold text-white">Status</h3>
|
||||
<div className={`px-4 py-2 border-2 rounded-lg font-semibold ${getStatusColor(booking.status)}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{booking.status === 'RESERVED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CONFIRMED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-lg hover:from-green-700 hover:to-emerald-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Bestätigen
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'CONFIRMED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('COMPLETED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg hover:from-blue-700 hover:to-cyan-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Abschließen
|
||||
</button>
|
||||
)}
|
||||
{['RESERVED', 'CONFIRMED'].includes(booking.status) && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CANCELLED')}
|
||||
className="px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 shadow-lg transition-all"
|
||||
>
|
||||
Stornieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiUser /> Kundendaten
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={formData.customerName}
|
||||
onChange={(e) => setFormData({ ...formData, customerName: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">E-Mail</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="email"
|
||||
value={formData.customerEmail}
|
||||
onChange={(e) => setFormData({ ...formData, customerEmail: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Telefon</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.customerPhone}
|
||||
onChange={(e) => setFormData({ ...formData, customerPhone: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerPhone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Rechnungsart</label>
|
||||
<p className="text-white">{booking.invoiceType === 'BUSINESS' ? 'Geschäftlich' : 'Privat'}</p>
|
||||
</div>
|
||||
|
||||
{booking.companyName && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Firma</label>
|
||||
<p className="text-white">{booking.companyName}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCalendar /> Event-Details
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Datum</label>
|
||||
<p className="text-white">{formatDate(booking.eventDate)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Standort</label>
|
||||
<p className="text-white">{booking.location.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Event-Adresse</label>
|
||||
<p className="text-white">{booking.eventAddress}</p>
|
||||
<p className="text-gray-400 text-sm">{booking.eventZip} {booking.eventCity}</p>
|
||||
</div>
|
||||
|
||||
{booking.eventLocation && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Location-Name</label>
|
||||
<p className="text-white">{booking.eventLocation}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau ab</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeStart)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau spätestens</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeLatest)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.notes && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiFileText /> Notizen
|
||||
</h3>
|
||||
<p className="text-gray-300 whitespace-pre-wrap">{booking.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{emails.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiMail /> E-Mail-Verlauf
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{emails.map((email) => (
|
||||
<div key={email.id} className="border border-gray-600 bg-gray-700/50 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-white">{email.subject}</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
{formatDate(email.receivedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-2">Von: {email.from}</p>
|
||||
{email.parsed && (
|
||||
<span className="inline-block px-2 py-1 bg-green-500/20 text-green-400 border border-green-500/50 text-xs rounded">
|
||||
Automatisch verarbeitet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCamera /> Fotobox
|
||||
</h3>
|
||||
{booking.photobox ? (
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm mb-2">Modell</p>
|
||||
<p className="text-white font-semibold mb-4">{booking.photobox.model}</p>
|
||||
<p className="text-gray-400 text-sm mb-2">Seriennummer</p>
|
||||
<p className="text-white">{booking.photobox.serialNumber}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch nicht zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ContractSection booking={booking} onRefresh={() => router.refresh()} />
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Preis</h3>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-white">
|
||||
{booking.calculatedPrice || 0}€
|
||||
</p>
|
||||
{booking.distance && (
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
+ {booking.distance} km Anfahrt
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Vertrag</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-300">Unterschrieben</span>
|
||||
<span className={`font-semibold ${booking.contractSigned ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{booking.contractSigned ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
</div>
|
||||
{booking.contractSignedAt && (
|
||||
<p className="text-sm text-gray-400">
|
||||
am {formatDate(booking.contractSignedAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.tours && booking.tours.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiTruck /> Tour
|
||||
</h3>
|
||||
{booking.tours.map((tour: any) => (
|
||||
<div key={tour.id}>
|
||||
{tour.driver ? (
|
||||
<p className="text-white">Fahrer: {tour.driver.name}</p>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch kein Fahrer zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
376
components/BookingDetail.tsx.bak3
Normal file
376
components/BookingDetail.tsx.bak3
Normal file
@@ -0,0 +1,376 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FiCalendar, FiMapPin, FiUser, FiMail, FiPhone, FiEdit, FiSave, FiX, FiCamera, FiTruck, FiFileText } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { formatDate, formatDateTime } from '@/lib/date-utils';
|
||||
import ContractSection from './ContractSection';
|
||||
|
||||
interface BookingDetailProps {
|
||||
booking: any;
|
||||
emails: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function BookingDetail({ booking, emails, user }: BookingDetailProps) {
|
||||
const router = useRouter();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [formData, setFormData] = useState(booking);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Gespeichert!');
|
||||
setEditing(false);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Speichern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Speichern');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
if (!confirm(`Status zu "${newStatus}" ändern?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/status`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: newStatus }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Status geändert!');
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Ändern');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
||||
case 'CONFIRMED': return 'bg-green-500/20 text-green-400 border-green-500/50';
|
||||
case 'COMPLETED': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
|
||||
case 'CANCELLED': return 'bg-red-500/20 text-red-400 border-red-500/50';
|
||||
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: any = {
|
||||
RESERVED: 'Reserviert',
|
||||
CONFIRMED: 'Bestätigt',
|
||||
COMPLETED: 'Abgeschlossen',
|
||||
CANCELLED: 'Storniert',
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<Link href="/dashboard/bookings" className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors">
|
||||
← Zurück zu Buchungen
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">Buchung {booking.bookingNumber}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{!editing ? (
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-gray-600 to-gray-700 text-white rounded-lg hover:from-gray-700 hover:to-gray-800 shadow-lg transition-all"
|
||||
>
|
||||
<FiEdit /> Bearbeiten
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditing(false);
|
||||
setFormData(booking);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<FiX /> Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 disabled:opacity-50 shadow-lg transition-all"
|
||||
>
|
||||
<FiSave /> {saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-bold text-white">Status</h3>
|
||||
<div className={`px-4 py-2 border-2 rounded-lg font-semibold ${getStatusColor(booking.status)}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{booking.status === 'RESERVED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CONFIRMED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-lg hover:from-green-700 hover:to-emerald-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Bestätigen
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'CONFIRMED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('COMPLETED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg hover:from-blue-700 hover:to-cyan-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Abschließen
|
||||
</button>
|
||||
)}
|
||||
{['RESERVED', 'CONFIRMED'].includes(booking.status) && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CANCELLED')}
|
||||
className="px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 shadow-lg transition-all"
|
||||
>
|
||||
Stornieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiUser /> Kundendaten
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={formData.customerName}
|
||||
onChange={(e) => setFormData({ ...formData, customerName: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">E-Mail</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="email"
|
||||
value={formData.customerEmail}
|
||||
onChange={(e) => setFormData({ ...formData, customerEmail: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Telefon</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.customerPhone}
|
||||
onChange={(e) => setFormData({ ...formData, customerPhone: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerPhone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Rechnungsart</label>
|
||||
<p className="text-white">{booking.invoiceType === 'BUSINESS' ? 'Geschäftlich' : 'Privat'}</p>
|
||||
</div>
|
||||
|
||||
{booking.companyName && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Firma</label>
|
||||
<p className="text-white">{booking.companyName}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCalendar /> Event-Details
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Datum</label>
|
||||
<p className="text-white">{formatDate(booking.eventDate)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Standort</label>
|
||||
<p className="text-white">{booking.location.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Event-Adresse</label>
|
||||
<p className="text-white">{booking.eventAddress}</p>
|
||||
<p className="text-gray-400 text-sm">{booking.eventZip} {booking.eventCity}</p>
|
||||
</div>
|
||||
|
||||
{booking.eventLocation && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Location-Name</label>
|
||||
<p className="text-white">{booking.eventLocation}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau ab</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeStart)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau spätestens</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeLatest)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.notes && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiFileText /> Notizen
|
||||
</h3>
|
||||
<p className="text-gray-300 whitespace-pre-wrap">{booking.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{emails.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiMail /> E-Mail-Verlauf
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{emails.map((email) => (
|
||||
<div key={email.id} className="border border-gray-600 bg-gray-700/50 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-white">{email.subject}</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
{formatDate(email.receivedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-2">Von: {email.from}</p>
|
||||
{email.parsed && (
|
||||
<span className="inline-block px-2 py-1 bg-green-500/20 text-green-400 border border-green-500/50 text-xs rounded">
|
||||
Automatisch verarbeitet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCamera /> Fotobox
|
||||
</h3>
|
||||
{booking.photobox ? (
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm mb-2">Modell</p>
|
||||
<p className="text-white font-semibold mb-4">{booking.photobox.model}</p>
|
||||
<p className="text-gray-400 text-sm mb-2">Seriennummer</p>
|
||||
<p className="text-white">{booking.photobox.serialNumber}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch nicht zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ContractSection booking={booking} onRefresh={() => router.refresh()} />
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Preis</h3>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-white">
|
||||
{booking.calculatedPrice || 0}€
|
||||
</p>
|
||||
{booking.distance && (
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
+ {booking.distance} km Anfahrt
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Vertrag</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-300">Unterschrieben</span>
|
||||
<span className={`font-semibold ${booking.contractSigned ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{booking.contractSigned ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
</div>
|
||||
{booking.contractSignedAt && (
|
||||
<p className="text-sm text-gray-400">
|
||||
am {formatDate(booking.contractSignedAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.tours && booking.tours.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiTruck /> Tour
|
||||
</h3>
|
||||
{booking.tours.map((tour: any) => (
|
||||
<div key={tour.id}>
|
||||
{tour.driver ? (
|
||||
<p className="text-white">Fahrer: {tour.driver.name}</p>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch kein Fahrer zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
305
components/BookingDetail_BROKEN.tsx
Normal file
305
components/BookingDetail_BROKEN.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FiCalendar, FiMapPin, FiUser, FiMail, FiPhone, FiEdit, FiSave, FiX, FiCamera, FiTruck, FiFileText } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { formatDate, formatDateTime } from '@/lib/date-utils';
|
||||
import ContractSection from './ContractSection';
|
||||
|
||||
interface BookingDetailProps {
|
||||
booking: any;
|
||||
emails: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function BookingDetail({ booking, emails, user }: BookingDetailProps) {
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<Link href="/dashboard/bookings" className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors">
|
||||
← Zurück zu Buchungen
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">Buchung {booking.bookingNumber}</h2>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{!editing ? (
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-gray-600 to-gray-700 text-white rounded-lg hover:from-gray-700 hover:to-gray-800 shadow-lg transition-all"
|
||||
>
|
||||
<FiEdit /> Bearbeiten
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditing(false);
|
||||
setFormData(booking);
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<FiX /> Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 disabled:opacity-50 shadow-lg transition-all"
|
||||
>
|
||||
<FiSave /> {saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-bold text-white">Status</h3>
|
||||
<div className={`px-4 py-2 border-2 rounded-lg font-semibold ${getStatusColor(booking.status)}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{booking.status === 'RESERVED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CONFIRMED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-lg hover:from-green-700 hover:to-emerald-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Bestätigen
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'CONFIRMED' && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('COMPLETED')}
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg hover:from-blue-700 hover:to-cyan-700 shadow-lg transition-all"
|
||||
>
|
||||
→ Abschließen
|
||||
</button>
|
||||
)}
|
||||
{['RESERVED', 'CONFIRMED'].includes(booking.status) && (
|
||||
<button
|
||||
onClick={() => handleStatusChange('CANCELLED')}
|
||||
className="px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 shadow-lg transition-all"
|
||||
>
|
||||
Stornieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiUser /> Kundendaten
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Name</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={formData.customerName}
|
||||
onChange={(e) => setFormData({ ...formData, customerName: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">E-Mail</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="email"
|
||||
value={formData.customerEmail}
|
||||
onChange={(e) => setFormData({ ...formData, customerEmail: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Telefon</label>
|
||||
{editing ? (
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.customerPhone}
|
||||
onChange={(e) => setFormData({ ...formData, customerPhone: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-white">{booking.customerPhone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Rechnungsart</label>
|
||||
<p className="text-white">{booking.invoiceType === 'BUSINESS' ? 'Geschäftlich' : 'Privat'}</p>
|
||||
</div>
|
||||
|
||||
{booking.companyName && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Firma</label>
|
||||
<p className="text-white">{booking.companyName}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCalendar /> Event-Details
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Datum</label>
|
||||
<p className="text-white">{formatDate(booking.eventDate)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Standort</label>
|
||||
<p className="text-white">{booking.location.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Event-Adresse</label>
|
||||
<p className="text-white">{booking.eventAddress}</p>
|
||||
<p className="text-gray-400 text-sm">{booking.eventZip} {booking.eventCity}</p>
|
||||
</div>
|
||||
|
||||
{booking.eventLocation && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Location-Name</label>
|
||||
<p className="text-white">{booking.eventLocation}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau ab</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeStart)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Aufbau spätestens</label>
|
||||
<p className="text-white">{formatDateTime(booking.setupTimeLatest)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.notes && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiFileText /> Notizen
|
||||
</h3>
|
||||
<p className="text-gray-300 whitespace-pre-wrap">{booking.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{emails.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiMail /> E-Mail-Verlauf
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{emails.map((email) => (
|
||||
<div key={email.id} className="border border-gray-600 bg-gray-700/50 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-white">{email.subject}</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
{formatDate(email.receivedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-2">Von: {email.from}</p>
|
||||
{email.parsed && (
|
||||
<span className="inline-block px-2 py-1 bg-green-500/20 text-green-400 border border-green-500/50 text-xs rounded">
|
||||
Automatisch verarbeitet
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiCamera /> Fotobox
|
||||
</h3>
|
||||
{booking.photobox ? (
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm mb-2">Modell</p>
|
||||
<p className="text-white font-semibold mb-4">{booking.photobox.model}</p>
|
||||
<p className="text-gray-400 text-sm mb-2">Seriennummer</p>
|
||||
<p className="text-white">{booking.photobox.serialNumber}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch nicht zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ContractSection booking={booking} onRefresh={() => router.refresh()} />
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Preis</h3>
|
||||
<div className="text-center">
|
||||
<p className="text-4xl font-bold text-white">
|
||||
{booking.calculatedPrice || 0}€
|
||||
</p>
|
||||
{booking.distance && (
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
+ {booking.distance} km Anfahrt
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Vertrag</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-300">Unterschrieben</span>
|
||||
<span className={`font-semibold ${booking.contractSigned ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{booking.contractSigned ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
</div>
|
||||
{booking.contractSignedAt && (
|
||||
<p className="text-sm text-gray-400">
|
||||
am {formatDate(booking.contractSignedAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.tours && booking.tours.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<h3 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<FiTruck /> Tour
|
||||
</h3>
|
||||
{booking.tours.map((tour: any) => (
|
||||
<div key={tour.id}>
|
||||
{tour.driver ? (
|
||||
<p className="text-white">Fahrer: {tour.driver.name}</p>
|
||||
) : (
|
||||
<p className="text-gray-400">Noch kein Fahrer zugewiesen</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
213
components/BookingsTable.tsx
Normal file
213
components/BookingsTable.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { FiPlus, FiEye, FiEdit } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { formatDate } from '@/lib/date-utils';
|
||||
|
||||
interface BookingsTableProps {
|
||||
bookings: any[];
|
||||
locations: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function BookingsTable({ bookings, locations, user }: BookingsTableProps) {
|
||||
const [statusFilter, setStatusFilter] = useState('ALL');
|
||||
const [locationFilter, setLocationFilter] = useState('ALL');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredBookings = bookings.filter(booking => {
|
||||
if (statusFilter !== 'ALL' && booking.status !== statusFilter) return false;
|
||||
if (locationFilter !== 'ALL' && booking.location.slug !== locationFilter) return false;
|
||||
if (searchTerm) {
|
||||
const search = searchTerm.toLowerCase();
|
||||
return (
|
||||
booking.customerName.toLowerCase().includes(search) ||
|
||||
booking.customerEmail.toLowerCase().includes(search) ||
|
||||
booking.bookingNumber.toLowerCase().includes(search) ||
|
||||
booking.eventCity.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/50';
|
||||
case 'CONFIRMED': return 'bg-green-500/20 text-green-400 border border-green-500/50';
|
||||
case 'COMPLETED': return 'bg-blue-500/20 text-blue-400 border border-blue-500/50';
|
||||
case 'CANCELLED': return 'bg-red-500/20 text-red-400 border border-red-500/50';
|
||||
default: return 'bg-gray-500/20 text-gray-400 border border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'Reserviert';
|
||||
case 'CONFIRMED': return 'Bestätigt';
|
||||
case 'COMPLETED': return 'Abgeschlossen';
|
||||
case 'CANCELLED': return 'Storniert';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const stats = {
|
||||
total: bookings.length,
|
||||
reserved: bookings.filter(b => b.status === 'RESERVED').length,
|
||||
confirmed: bookings.filter(b => b.status === 'CONFIRMED').length,
|
||||
completed: bookings.filter(b => b.status === 'COMPLETED').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-white">Buchungen</h2>
|
||||
<p className="text-gray-400 mt-1">Alle Buchungsanfragen im Überblick</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/bookings/new"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 shadow-lg transition-all"
|
||||
>
|
||||
<FiPlus /> Neue Buchung
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<p className="text-sm text-gray-400">Gesamt</p>
|
||||
<p className="text-3xl font-bold text-white mt-2">{stats.total}</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<p className="text-sm text-gray-400">Reserviert</p>
|
||||
<p className="text-3xl font-bold text-yellow-400 mt-2">{stats.reserved}</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<p className="text-sm text-gray-400">Bestätigt</p>
|
||||
<p className="text-3xl font-bold text-green-400 mt-2">{stats.confirmed}</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<p className="text-sm text-gray-400">Abgeschlossen</p>
|
||||
<p className="text-3xl font-bold text-blue-400 mt-2">{stats.completed}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-6">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suche nach Name, E-Mail, Buchungsnummer..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
>
|
||||
<option value="ALL">Alle Status</option>
|
||||
<option value="RESERVED">Reserviert</option>
|
||||
<option value="CONFIRMED">Bestätigt</option>
|
||||
<option value="COMPLETED">Abgeschlossen</option>
|
||||
<option value="CANCELLED">Storniert</option>
|
||||
</select>
|
||||
<select
|
||||
value={locationFilter}
|
||||
onChange={(e) => setLocationFilter(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
>
|
||||
<option value="ALL">Alle Standorte</option>
|
||||
{locations.map(loc => (
|
||||
<option key={loc.id} value={loc.slug}>{loc.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredBookings.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-400">Keine Buchungen gefunden</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-700">
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-300">Buchungsnr.</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-300">Kunde</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-300">Event</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-300">Standort</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-300">Datum</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-300">Status</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-300">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredBookings.map(booking => (
|
||||
<tr
|
||||
key={booking.id}
|
||||
onClick={() => window.location.href = `/dashboard/bookings/${booking.id}`}
|
||||
className="border-b border-gray-700 hover:bg-gray-700/50 transition-colors cursor-pointer"
|
||||
>
|
||||
<td className="py-4 px-4">
|
||||
<span className="font-mono text-sm text-white">{booking.bookingNumber}</span>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<div>
|
||||
<p className="font-semibold text-white">{booking.customerName}</p>
|
||||
<p className="text-sm text-gray-400">{booking.customerEmail}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<div>
|
||||
<p className="text-white">{booking.eventCity}</p>
|
||||
{booking.eventLocation && (
|
||||
<p className="text-sm text-gray-400">{booking.eventLocation}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<span className="text-white">{booking.location.name}</span>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<span className="text-white">
|
||||
{formatDate(booking.eventDate)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<span className={`inline-block px-3 py-1 text-xs font-semibold rounded-full ${getStatusColor(booking.status)}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-4" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex gap-2">
|
||||
<Link
|
||||
href={`/dashboard/bookings/${booking.id}`}
|
||||
className="p-2 text-blue-400 hover:bg-blue-500/20 rounded-lg transition-colors"
|
||||
title="Details anzeigen"
|
||||
>
|
||||
<FiEye />
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/bookings/${booking.id}`}
|
||||
className="p-2 text-gray-400 hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
<FiEdit />
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
218
components/ContractSection.tsx
Normal file
218
components/ContractSection.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { FiFileText, FiDownload, FiMail, FiUpload, FiCheck, FiClock } from 'react-icons/fi';
|
||||
import { formatDateTime } from '@/lib/date-utils';
|
||||
|
||||
interface ContractSectionProps {
|
||||
booking: any;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export default function ContractSection({ booking, onRefresh }: ContractSectionProps) {
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerating(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/contract`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Vertrag erfolgreich erstellt!');
|
||||
onRefresh();
|
||||
} else {
|
||||
alert('Fehler beim Erstellen des Vertrags');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Erstellen des Vertrags');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
window.open(`/api/bookings/${booking.id}/contract`, '_blank');
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
setSending(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/contract/send`, {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
alert(`Vertrag versendet! Link: ${data.signUrl}`);
|
||||
onRefresh();
|
||||
} else {
|
||||
alert('Fehler beim Versenden des Vertrags');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Versenden des Vertrags');
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${booking.id}/contract/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Vertrag erfolgreich hochgeladen!');
|
||||
onRefresh();
|
||||
} else {
|
||||
alert('Fehler beim Hochladen');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Hochladen');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatus = () => {
|
||||
if (booking.contractSigned) {
|
||||
return {
|
||||
icon: <FiCheck className="text-green-600" />,
|
||||
text: 'Unterschrieben',
|
||||
color: 'bg-green-50 border-green-200',
|
||||
};
|
||||
}
|
||||
if (booking.contractSentAt) {
|
||||
return {
|
||||
icon: <FiClock className="text-yellow-600" />,
|
||||
text: 'Versendet',
|
||||
color: 'bg-yellow-50 border-yellow-200',
|
||||
};
|
||||
}
|
||||
if (booking.contractGenerated) {
|
||||
return {
|
||||
icon: <FiFileText className="text-blue-600" />,
|
||||
text: 'Erstellt',
|
||||
color: 'bg-blue-50 border-blue-200',
|
||||
};
|
||||
}
|
||||
return {
|
||||
icon: <FiClock className="text-gray-600" />,
|
||||
text: 'Nicht erstellt',
|
||||
color: 'bg-gray-50 border-gray-200',
|
||||
};
|
||||
};
|
||||
|
||||
const status = getStatus();
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-xl font-bold text-gray-900">📋 Vertrag</h3>
|
||||
<div className={`flex items-center gap-2 px-3 py-1 rounded-full border ${status.color}`}>
|
||||
{status.icon}
|
||||
<span className="text-sm font-semibold">{status.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{booking.contractSigned && (
|
||||
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<p className="text-green-800 font-semibold mb-2">✓ Vertrag unterschrieben</p>
|
||||
<div className="text-sm text-green-700 space-y-1">
|
||||
<p><span className="font-medium">Unterschrieben von:</span> {booking.contractSignedBy}</p>
|
||||
<p><span className="font-medium">Datum:</span> {formatDateTime(booking.contractSignedAt)}</p>
|
||||
<p><span className="font-medium">Art:</span> {booking.contractSignedOnline ? 'Online-Signatur' : 'Analog hochgeladen'}</p>
|
||||
{booking.contractSignedIp && (
|
||||
<p><span className="font-medium">IP-Adresse:</span> {booking.contractSignedIp}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{booking.contractGenerated && !booking.contractSigned && (
|
||||
<div className="mb-4 space-y-2 text-sm text-gray-600">
|
||||
{booking.contractGeneratedAt && (
|
||||
<p>Erstellt am: {formatDateTime(booking.contractGeneratedAt)}</p>
|
||||
)}
|
||||
{booking.contractSentAt && (
|
||||
<p>Versendet am: {formatDateTime(booking.contractSentAt)}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{!booking.contractGenerated ? (
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:bg-gray-400"
|
||||
>
|
||||
<FiFileText /> {generating ? 'Wird erstellt...' : 'Vertrag erstellen'}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FiDownload /> PDF herunterladen
|
||||
</button>
|
||||
|
||||
{!booking.contractSigned && (
|
||||
<>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={sending}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:bg-gray-400"
|
||||
>
|
||||
<FiMail /> {sending ? 'Wird versendet...' : 'Per E-Mail versenden'}
|
||||
</button>
|
||||
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
onChange={handleUpload}
|
||||
disabled={uploading}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
disabled={uploading}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors disabled:bg-gray-400"
|
||||
>
|
||||
<FiUpload /> {uploading ? 'Wird hochgeladen...' : 'Signiertes PDF hochladen'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{booking.contractSigned && booking.contractPdfUrl && (
|
||||
<a
|
||||
href={booking.contractPdfUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
<FiDownload /> Signiertes PDF herunterladen
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
325
components/ContractSigningForm.tsx
Normal file
325
components/ContractSigningForm.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import SignaturePad from './SignaturePad';
|
||||
import { formatDate } from '@/lib/date-utils';
|
||||
import { FiUpload, FiDownload } from 'react-icons/fi';
|
||||
|
||||
interface ContractSigningFormProps {
|
||||
booking: any;
|
||||
location: any;
|
||||
photobox: any;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export default function ContractSigningForm({ booking, location, photobox, token }: ContractSigningFormProps) {
|
||||
const router = useRouter();
|
||||
const [signatureData, setSignatureData] = useState<string | null>(null);
|
||||
const [acceptTerms, setAcceptTerms] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [showSignature, setShowSignature] = useState(false);
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!signatureData || !acceptTerms) return;
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contract/sign', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
signatureData,
|
||||
name: booking.customerName,
|
||||
email: booking.customerEmail,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
router.push(`/contract/success`);
|
||||
} else {
|
||||
alert('Fehler beim Unterschreiben des Vertrags');
|
||||
setSubmitting(false);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Unterschreiben des Vertrags');
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
const file = e.target.files[0];
|
||||
if (file.type === 'application/pdf') {
|
||||
setSelectedFile(file);
|
||||
} else {
|
||||
alert('Bitte wählen Sie eine PDF-Datei aus');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile || !acceptTerms) return;
|
||||
|
||||
setUploadProgress(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', selectedFile);
|
||||
formData.append('token', token);
|
||||
|
||||
const res = await fetch('/api/contract/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
router.push(`/contract/success`);
|
||||
} else {
|
||||
alert('Fehler beim Hochladen des Vertrags');
|
||||
setUploadProgress(false);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Hochladen des Vertrags');
|
||||
setUploadProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadContract = async () => {
|
||||
try {
|
||||
const decoded = Buffer.from(token, 'base64url').toString();
|
||||
const bookingId = decoded.split('-')[0];
|
||||
|
||||
const res = await fetch(`/api/bookings/${bookingId}/contract`);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `Vertrag-${booking.bookingNumber}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} else {
|
||||
alert('Fehler beim Herunterladen des Vertrags');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Herunterladen des Vertrags');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-red-600 text-white p-6">
|
||||
<h1 className="text-2xl font-bold">SaveTheMoment Atlas</h1>
|
||||
<p className="text-red-100">Mietvertrag für Fotobox</p>
|
||||
</div>
|
||||
|
||||
{/* Contract Details */}
|
||||
<div className="p-8 space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-4">Vertragsdaten</h2>
|
||||
<div className="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Buchungsnummer</p>
|
||||
<p className="font-semibold">{booking.bookingNumber}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Event-Datum</p>
|
||||
<p className="font-semibold">{formatDate(booking.eventDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Location</p>
|
||||
<p className="font-semibold">{booking.eventLocation || 'Nicht angegeben'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Gesamtpreis</p>
|
||||
<p className="font-semibold">{booking.calculatedPrice?.toFixed(2) || '0.00'} €</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Kundendaten</h3>
|
||||
<div className="bg-gray-50 p-4 rounded-lg space-y-2">
|
||||
<p><span className="text-gray-600">Name:</span> {booking.customerName}</p>
|
||||
<p><span className="text-gray-600">E-Mail:</span> {booking.customerEmail}</p>
|
||||
<p><span className="text-gray-600">Telefon:</span> {booking.customerPhone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">Vertragsbedingungen</h3>
|
||||
<div className="bg-gray-50 p-4 rounded-lg space-y-2 text-sm">
|
||||
<p>1. Der Vermieter verpflichtet sich, die Fotobox am Veranstaltungsort aufzubauen und nach der Veranstaltung wieder abzubauen.</p>
|
||||
<p>2. Die Mietdauer beginnt mit dem Aufbau und endet mit dem Abbau der Fotobox.</p>
|
||||
<p>3. Der Mieter haftet für Schäden, die während der Mietzeit an der Fotobox entstehen.</p>
|
||||
<p>4. Der vereinbarte Mietpreis ist spätestens 7 Tage vor der Veranstaltung fällig.</p>
|
||||
<p>5. Bei Stornierung weniger als 14 Tage vor der Veranstaltung wird eine Bearbeitungsgebühr von 50% erhoben.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AGB Checkbox */}
|
||||
<div className="border-t pt-6">
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={acceptTerms}
|
||||
onChange={(e) => setAcceptTerms(e.target.checked)}
|
||||
className="w-5 h-5 text-red-600 rounded mt-1"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
Ich habe die Vertragsbedingungen gelesen und akzeptiere diese. Mir ist bewusst, dass dieser Vertrag rechtsverbindlich ist.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Signature Section */}
|
||||
{!showSignature && !showUpload ? (
|
||||
<div className="border-t pt-6 space-y-4">
|
||||
<button
|
||||
onClick={() => setShowSignature(true)}
|
||||
disabled={!acceptTerms}
|
||||
className="w-full px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed font-semibold"
|
||||
>
|
||||
Online unterschreiben
|
||||
</button>
|
||||
|
||||
<div className="text-center text-gray-500 font-medium">
|
||||
oder
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={handleDownloadContract}
|
||||
className="w-full px-6 py-3 bg-white border-2 border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-semibold flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiDownload />
|
||||
Vertrag herunterladen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowUpload(true)}
|
||||
disabled={!acceptTerms}
|
||||
className="w-full px-6 py-3 bg-white border-2 border-red-600 text-red-600 rounded-lg hover:bg-red-50 transition-colors disabled:border-gray-400 disabled:text-gray-400 disabled:cursor-not-allowed font-semibold flex items-center justify-center gap-2"
|
||||
>
|
||||
<FiUpload />
|
||||
Unterschriebenen Vertrag hochladen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 text-center mt-3">
|
||||
Laden Sie den Vertrag herunter, unterschreiben Sie ihn und laden Sie ihn dann wieder hoch
|
||||
</p>
|
||||
</div>
|
||||
) : showSignature ? (
|
||||
<div className="border-t pt-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Unterschrift</h3>
|
||||
{!signatureData ? (
|
||||
<SignaturePad
|
||||
onSave={(data) => setSignatureData(data)}
|
||||
onCancel={() => setShowSignature(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-green-500 rounded-lg p-4 bg-green-50">
|
||||
<p className="text-green-800 font-semibold mb-2">✓ Unterschrift erfasst</p>
|
||||
<img src={signatureData} alt="Unterschrift" className="max-h-32 border-t border-gray-300 pt-2" />
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setSignatureData(null)}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Neue Unterschrift
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
className="flex-1 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:bg-gray-400 font-semibold"
|
||||
>
|
||||
{submitting ? 'Wird übermittelt...' : 'Vertrag jetzt unterschreiben'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-t pt-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Unterschriebenen Vertrag hochladen</h3>
|
||||
{!selectedFile ? (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
|
||||
<FiUpload className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-gray-600 mb-4">
|
||||
Wählen Sie die unterschriebene PDF-Datei aus
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
id="contract-upload"
|
||||
/>
|
||||
<label
|
||||
htmlFor="contract-upload"
|
||||
className="inline-block px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors cursor-pointer font-semibold"
|
||||
>
|
||||
Datei auswählen
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowUpload(false)}
|
||||
className="w-full px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-green-500 rounded-lg p-4 bg-green-50">
|
||||
<p className="text-green-800 font-semibold mb-2">✓ Datei ausgewählt</p>
|
||||
<p className="text-gray-700">{selectedFile.name}</p>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Größe: {(selectedFile.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setSelectedFile(null)}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Andere Datei wählen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={uploadProgress}
|
||||
className="flex-1 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:bg-gray-400 font-semibold"
|
||||
>
|
||||
{uploadProgress ? 'Wird hochgeladen...' : 'Jetzt hochladen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="bg-gray-50 px-8 py-4 text-sm text-gray-600 border-t">
|
||||
<p>
|
||||
SaveTheMoment Atlas • {location.name} • {location.contactEmail} • {location.contactPhone}
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
Dieser Vertrag wird elektronisch gespeichert. Sie erhalten eine Kopie per E-Mail.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
161
components/DashboardContent.tsx
Normal file
161
components/DashboardContent.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import { FiCalendar, FiCamera, FiBell } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { formatDate } from '@/lib/date-utils';
|
||||
|
||||
interface DashboardContentProps {
|
||||
user: any;
|
||||
stats: {
|
||||
totalBookings: number;
|
||||
reservedBookings: number;
|
||||
confirmedBookings: number;
|
||||
completedBookings: number;
|
||||
totalLocations: number;
|
||||
totalPhotoboxes: number;
|
||||
totalDrivers: number;
|
||||
};
|
||||
recentBookings: any[];
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'RESERVED': return 'Reserviert';
|
||||
case 'CONFIRMED': return 'Bestätigt';
|
||||
case 'COMPLETED': return 'Abgeschlossen';
|
||||
case 'CANCELLED': return 'Storniert';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
export default function DashboardContent({ user, stats, recentBookings }: DashboardContentProps) {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h2 className="text-3xl font-bold text-white">Dashboard</h2>
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-gray-700 to-gray-800 border border-gray-600 text-white rounded-lg hover:from-gray-600 hover:to-gray-700 transition-all">
|
||||
<FiBell /> Benachrichtigungen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Gesamt Buchungen</p>
|
||||
<p className="text-3xl font-bold text-white mt-2">{stats.totalBookings}</p>
|
||||
</div>
|
||||
<div className="bg-blue-500/20 p-3 rounded-lg">
|
||||
<FiCalendar className="text-2xl text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Reserviert</p>
|
||||
<p className="text-3xl font-bold text-yellow-400 mt-2">{stats.reservedBookings}</p>
|
||||
</div>
|
||||
<div className="bg-yellow-500/20 p-3 rounded-lg">
|
||||
<FiCalendar className="text-2xl text-yellow-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Bestätigt</p>
|
||||
<p className="text-3xl font-bold text-green-400 mt-2">{stats.confirmedBookings}</p>
|
||||
</div>
|
||||
<div className="bg-green-500/20 p-3 rounded-lg">
|
||||
<FiCalendar className="text-2xl text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Aktive Fotoboxen</p>
|
||||
<p className="text-3xl font-bold text-white mt-2">{stats.totalPhotoboxes}</p>
|
||||
</div>
|
||||
<div className="bg-purple-500/20 p-3 rounded-lg">
|
||||
<FiCamera className="text-2xl text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Letzte Buchungen</h3>
|
||||
{recentBookings.length === 0 ? (
|
||||
<p className="text-gray-400 text-center py-8">Noch keine Buchungen vorhanden</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{recentBookings.map((booking) => (
|
||||
<Link
|
||||
key={booking.id}
|
||||
href={`/dashboard/bookings/${booking.id}`}
|
||||
className="flex items-center justify-between p-4 bg-gray-700/50 border border-gray-600 rounded-lg hover:bg-gray-700 hover:border-gray-500 transition-all cursor-pointer"
|
||||
>
|
||||
<div>
|
||||
<p className="font-semibold text-white">{booking.customerName}</p>
|
||||
<p className="text-sm text-gray-400">{booking.eventCity} - {booking.location.name}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-400">
|
||||
{formatDate(booking.eventDate)}
|
||||
</p>
|
||||
<span className={`inline-block px-3 py-1 text-xs font-semibold rounded-full border ${
|
||||
booking.status === 'CONFIRMED' ? 'bg-green-500/20 text-green-400 border-green-500/50' :
|
||||
booking.status === 'RESERVED' ? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50' :
|
||||
'bg-gray-500/20 text-gray-400 border-gray-500/50'
|
||||
}`}>
|
||||
{getStatusLabel(booking.status)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 p-6 rounded-xl shadow-sm">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Schnellzugriff</h3>
|
||||
<div className="space-y-3">
|
||||
<Link href="/dashboard/bookings/new" className="block w-full px-4 py-3 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg font-semibold text-center hover:from-red-700 hover:to-pink-700 transition-all shadow-lg">
|
||||
Neue Buchung
|
||||
</Link>
|
||||
<Link href="/dashboard/tours/plan" className="block w-full px-4 py-3 bg-gradient-to-r from-gray-700 to-gray-800 text-white rounded-lg font-semibold text-center hover:from-gray-600 hover:to-gray-700 transition-all">
|
||||
Touren planen
|
||||
</Link>
|
||||
<Link href="/dashboard/photoboxes" className="block w-full px-4 py-3 border border-gray-600 text-gray-300 rounded-lg font-semibold text-center hover:bg-gray-700/50 hover:border-gray-500 transition-all">
|
||||
Verfügbarkeit prüfen
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-700">
|
||||
<h4 className="font-semibold text-white mb-3">Systeminfo</h4>
|
||||
<div className="space-y-2 text-sm text-gray-400">
|
||||
<div className="flex justify-between">
|
||||
<span>Standorte:</span>
|
||||
<span className="font-semibold text-white">{stats.totalLocations}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Fahrer:</span>
|
||||
<span className="font-semibold text-white">{stats.totalDrivers}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Fotoboxen:</span>
|
||||
<span className="font-semibold text-white">{stats.totalPhotoboxes}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
117
components/DashboardSidebar.tsx
Normal file
117
components/DashboardSidebar.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { signOut } from 'next-auth/react';
|
||||
import { FiHome, FiCalendar, FiMapPin, FiCamera, FiUsers, FiSettings, FiLogOut, FiTruck, FiPackage, FiGrid, FiBook } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface DashboardSidebarProps {
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function DashboardSidebar({ user }: DashboardSidebarProps) {
|
||||
const pathname = usePathname();
|
||||
const [projectsOpen, setProjectsOpen] = useState(false);
|
||||
|
||||
const isActive = (path: string) => {
|
||||
if (path === '/dashboard') {
|
||||
return pathname === '/dashboard';
|
||||
}
|
||||
return pathname?.startsWith(path);
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ href: '/dashboard', icon: FiHome, label: 'Dashboard' },
|
||||
{ href: '/dashboard/kalender', icon: FiBook, label: 'Kalender' },
|
||||
{ href: '/dashboard/bookings', icon: FiCalendar, label: 'Buchungen' },
|
||||
{ href: '/dashboard/tours', icon: FiTruck, label: 'Touren' },
|
||||
{ href: '/dashboard/locations', icon: FiMapPin, label: 'Standorte' },
|
||||
{ href: '/dashboard/photoboxes', icon: FiCamera, label: 'Fotoboxen' },
|
||||
{ href: '/dashboard/inventory', icon: FiPackage, label: 'Inventar' },
|
||||
{ href: '/dashboard/drivers', icon: FiUsers, label: 'Fahrer' },
|
||||
{ href: '/dashboard/settings', icon: FiSettings, label: 'Einstellungen' },
|
||||
];
|
||||
|
||||
const projects = [
|
||||
{ href: '/dashboard/projects/fotoboxjungs', label: 'Die Fotoboxjungs', color: 'blue' },
|
||||
{ href: '/dashboard/projects/kloenbox', label: 'Die Klönbox', color: 'green' },
|
||||
{ href: '/dashboard/projects/hochzeitsbuchstaben', label: 'Hochzeitsbuchstaben', color: 'pink' },
|
||||
{ href: '/dashboard/projects/forte-dj', label: 'Forte & Friends', color: 'purple' },
|
||||
{ href: '/dashboard/projects/melobox', label: 'Melobox', color: 'yellow' },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-gradient-to-br from-gray-800 to-gray-900 border-r border-gray-700 shadow-lg min-h-screen">
|
||||
<div className="p-6 border-b border-gray-700">
|
||||
<h1 className="text-xl font-bold text-white">
|
||||
SaveTheMoment <span className="text-red-400">Atlas</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-400 mt-1">{user?.name || 'Benutzer'}</p>
|
||||
</div>
|
||||
|
||||
<nav className="p-4 space-y-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = isActive(item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg font-medium transition-colors ${
|
||||
active
|
||||
? 'text-white bg-gradient-to-r from-red-600/20 to-pink-600/20 border border-red-500/50'
|
||||
: 'text-gray-300 hover:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<Icon /> {item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Weitere Projekte */}
|
||||
<div className="pt-4 border-t border-gray-700">
|
||||
<button
|
||||
onClick={() => setProjectsOpen(!projectsOpen)}
|
||||
className="flex items-center justify-between w-full px-4 py-3 text-gray-300 hover:bg-gray-700/50 rounded-lg transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FiGrid />
|
||||
<span>Weitere Projekte</span>
|
||||
</div>
|
||||
<span className={`transition-transform ${projectsOpen ? 'rotate-90' : ''}`}>›</span>
|
||||
</button>
|
||||
|
||||
{projectsOpen && (
|
||||
<div className="mt-2 ml-4 space-y-1">
|
||||
{projects.map((project) => (
|
||||
<Link
|
||||
key={project.href}
|
||||
href={project.href}
|
||||
className={`block px-4 py-2 text-sm rounded-lg transition-colors ${
|
||||
isActive(project.href)
|
||||
? 'text-white bg-gray-700'
|
||||
: 'text-gray-400 hover:text-gray-300 hover:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<span className="inline-block w-2 h-2 rounded-full mr-2" style={{ backgroundColor: project.color }}></span>
|
||||
{project.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: '/' })}
|
||||
className="flex items-center gap-3 px-4 py-3 text-gray-300 hover:bg-red-500/20 hover:text-red-400 rounded-lg w-full transition-colors"
|
||||
>
|
||||
<FiLogOut /> Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
172
components/DriverDashboard.tsx
Normal file
172
components/DriverDashboard.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { signOut } from 'next-auth/react';
|
||||
import { FiHome, FiTruck, FiMapPin, FiSettings, FiLogOut, FiCalendar } from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface DriverDashboardProps {
|
||||
user: any;
|
||||
myTours: any[];
|
||||
availableTours: any[];
|
||||
}
|
||||
|
||||
export default function DriverDashboard({ user, myTours, availableTours }: DriverDashboardProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="flex flex-col md:flex-row">
|
||||
<aside className="w-full md:w-64 bg-gray-900 text-white shadow-lg min-h-screen">
|
||||
<div className="p-6 border-b border-gray-700">
|
||||
<h1 className="text-xl font-bold">
|
||||
SaveTheMoment <span className="text-red-500">Atlas</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-400 mt-1">{user?.name}</p>
|
||||
<span className="inline-block mt-2 px-3 py-1 bg-gray-800 text-xs rounded-full">Fahrer</span>
|
||||
</div>
|
||||
|
||||
<nav className="p-4 space-y-2">
|
||||
<Link href="/driver" className="flex items-center gap-3 px-4 py-3 bg-gray-800 rounded-lg font-medium">
|
||||
<FiHome /> Übersicht
|
||||
</Link>
|
||||
<Link href="/driver/tours" className="flex items-center gap-3 px-4 py-3 hover:bg-gray-800 rounded-lg">
|
||||
<FiTruck /> Meine Touren
|
||||
</Link>
|
||||
<Link href="/driver/available" className="flex items-center gap-3 px-4 py-3 hover:bg-gray-800 rounded-lg">
|
||||
<FiCalendar /> Verfügbare Touren
|
||||
</Link>
|
||||
<Link href="/driver/settings" className="flex items-center gap-3 px-4 py-3 hover:bg-gray-800 rounded-lg">
|
||||
<FiSettings /> Einstellungen
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="absolute bottom-4 left-4 right-4 md:right-auto md:w-56">
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: '/' })}
|
||||
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-800 rounded-lg w-full"
|
||||
>
|
||||
<FiLogOut /> Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 p-4 md:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-8">Fahrer Dashboard</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Meine Touren</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-2">{myTours.length}</p>
|
||||
</div>
|
||||
<div className="bg-green-100 p-3 rounded-lg">
|
||||
<FiTruck className="text-2xl text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Verfügbare Touren</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-2">{availableTours.length}</p>
|
||||
</div>
|
||||
<div className="bg-blue-100 p-3 rounded-lg">
|
||||
<FiCalendar className="text-2xl text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">Meine nächsten Touren</h3>
|
||||
{myTours.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">Keine Touren zugewiesen</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{myTours.map((tour) => (
|
||||
<div key={tour.id} className="p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<p className="font-semibold text-gray-900">
|
||||
{new Date(tour.tourDate).toLocaleDateString('de-DE', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
<span className="px-3 py-1 bg-green-600 text-white text-xs rounded-full">
|
||||
{tour.bookings.length} Stops
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
{tour.bookings.slice(0, 3).map((booking: any) => (
|
||||
<div key={booking.id} className="flex items-center gap-2">
|
||||
<FiMapPin className="text-green-600" />
|
||||
<span>{booking.eventCity} - {booking.customerName}</span>
|
||||
</div>
|
||||
))}
|
||||
{tour.bookings.length > 3 && (
|
||||
<p className="text-xs text-gray-500 ml-6">
|
||||
+ {tour.bookings.length - 3} weitere
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Link
|
||||
href={`/driver/tours/${tour.id}`}
|
||||
className="block w-full text-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Details & Navigation
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">Verfügbare Touren</h3>
|
||||
{availableTours.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">Keine verfügbaren Touren</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{availableTours.map((tour) => (
|
||||
<div key={tour.id} className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<p className="font-semibold text-gray-900">
|
||||
{new Date(tour.tourDate).toLocaleDateString('de-DE', {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
<span className="px-3 py-1 bg-blue-600 text-white text-xs rounded-full">
|
||||
{tour.bookings.length} Stops
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mb-3">
|
||||
{tour.bookings.slice(0, 2).map((booking: any) => (
|
||||
<div key={booking.id} className="flex items-center gap-2">
|
||||
<FiMapPin className="text-blue-600" />
|
||||
<span>{booking.eventCity}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
Tour übernehmen
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
366
components/LocationsManager.tsx
Normal file
366
components/LocationsManager.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { FiMapPin, FiMail, FiSettings, FiCheck, FiX, FiRefreshCw } from 'react-icons/fi';
|
||||
import { formatDateTime } from '@/lib/date-utils';
|
||||
|
||||
interface LocationsManagerProps {
|
||||
locations: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function LocationsManager({ locations, user }: LocationsManagerProps) {
|
||||
const [selectedLocation, setSelectedLocation] = useState<any>(null);
|
||||
const [showEmailSettings, setShowEmailSettings] = useState(false);
|
||||
const [syncingLocation, setSyncingLocation] = useState<string | null>(null);
|
||||
|
||||
const handleManualSync = async (locationId: string) => {
|
||||
setSyncingLocation(locationId);
|
||||
try {
|
||||
const res = await fetch('/api/email-sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ locationId }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
alert(`Sync erfolgreich! ${data.newEmails} neue E-Mails, ${data.newBookings} neue Buchungen`);
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert(`Fehler beim Sync: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Sync');
|
||||
} finally {
|
||||
setSyncingLocation(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-bold text-white">Standortverwaltung</h2>
|
||||
<p className="text-gray-400 mt-1">E-Mail-Konfiguration und Standort-Einstellungen</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{locations.map(location => (
|
||||
<div key={location.id} className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white">{location.name}</h3>
|
||||
<p className="text-sm text-gray-400">{location.city}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{location.emailSyncEnabled ? (
|
||||
<span className="flex items-center gap-1 px-3 py-1 bg-green-500/20 text-green-400 border border-green-500/50 text-xs font-semibold rounded-full">
|
||||
<FiCheck /> E-Mail aktiv
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 px-3 py-1 bg-gray-500/20 text-gray-400 border border-gray-500/50 text-xs font-semibold rounded-full">
|
||||
<FiX /> E-Mail inaktiv
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FiMapPin className="text-gray-400" />
|
||||
<span className="text-gray-400">Website:</span>
|
||||
<a href={location.websiteUrl} target="_blank" rel="noopener noreferrer" className="text-red-400 hover:text-red-300 hover:underline transition-colors">
|
||||
{location.websiteUrl}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FiMail className="text-gray-400" />
|
||||
<span className="text-gray-400">E-Mail:</span>
|
||||
<span className="text-white">{location.contactEmail}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4 pt-4 border-t border-gray-700">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Fotoboxen</p>
|
||||
<p className="text-2xl font-bold text-white">{location._count.photoboxes}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Buchungen</p>
|
||||
<p className="text-2xl font-bold text-white">{location._count.bookings}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{location.lastEmailSync && (
|
||||
<div className="text-sm text-gray-400 mb-4">
|
||||
Letzter Sync: {formatDateTime(location.lastEmailSync)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
{location.emailSyncEnabled && (
|
||||
<button
|
||||
onClick={() => handleManualSync(location.id)}
|
||||
disabled={syncingLocation === location.id}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 transition-all shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FiRefreshCw className={syncingLocation === location.id ? 'animate-spin' : ''} />
|
||||
{syncingLocation === location.id ? 'Sync läuft...' : 'E-Mails abrufen'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedLocation(location);
|
||||
setShowEmailSettings(true);
|
||||
}}
|
||||
className={`${location.emailSyncEnabled ? 'flex-1' : 'w-full'} flex items-center justify-center gap-2 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 transition-colors`}
|
||||
>
|
||||
<FiSettings /> E-Mail-Einstellungen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showEmailSettings && selectedLocation && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6 border-b border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-2xl font-bold text-white">
|
||||
E-Mail-Einstellungen: {selectedLocation.name}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEmailSettings(false);
|
||||
setSelectedLocation(null);
|
||||
}}
|
||||
className="p-2 hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<FiX className="text-xl text-gray-300" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/locations/${selectedLocation.id}/email-settings`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
imapHost: formData.get('imapHost'),
|
||||
imapPort: parseInt(formData.get('imapPort') as string),
|
||||
imapUser: formData.get('imapUser'),
|
||||
imapPassword: formData.get('imapPassword'),
|
||||
imapSecure: formData.get('imapSecure') === 'on',
|
||||
smtpHost: formData.get('smtpHost'),
|
||||
smtpPort: parseInt(formData.get('smtpPort') as string),
|
||||
smtpUser: formData.get('smtpUser'),
|
||||
smtpPassword: formData.get('smtpPassword'),
|
||||
smtpSecure: formData.get('smtpSecure') === 'on',
|
||||
emailSyncEnabled: formData.get('emailSyncEnabled') === 'on',
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert('Einstellungen gespeichert!');
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Speichern');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Fehler beim Speichern');
|
||||
}
|
||||
}}
|
||||
className="space-y-6"
|
||||
>
|
||||
<div className="bg-blue-500/20 border border-blue-500/50 rounded-lg p-4">
|
||||
<p className="text-sm text-blue-300">
|
||||
<strong>Wichtig:</strong> Die E-Mail-Zugangsdaten werden verschlüsselt gespeichert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="emailSyncEnabled"
|
||||
defaultChecked={selectedLocation.emailSyncEnabled}
|
||||
className="w-4 h-4 text-red-600 rounded focus:ring-red-500"
|
||||
/>
|
||||
<span className="font-semibold text-white">E-Mail-Sync aktivieren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 pt-6">
|
||||
<h4 className="font-semibold text-white mb-4">IMAP-Einstellungen (Empfang)</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
IMAP-Server
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="imapHost"
|
||||
defaultValue={selectedLocation.imapHost || ''}
|
||||
placeholder="imap.example.com"
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="imapPort"
|
||||
defaultValue={selectedLocation.imapPort || 993}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="imapSecure"
|
||||
defaultChecked={selectedLocation.imapSecure ?? true}
|
||||
className="w-4 h-4 text-red-600 rounded focus:ring-red-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-300">SSL/TLS</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Benutzername
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="imapUser"
|
||||
defaultValue={selectedLocation.imapUser || ''}
|
||||
placeholder={selectedLocation.contactEmail}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="imapPassword"
|
||||
defaultValue={selectedLocation.imapPassword || ''}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 pt-6">
|
||||
<h4 className="font-semibold text-white mb-4">SMTP-Einstellungen (Versand)</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
SMTP-Server
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="smtpHost"
|
||||
defaultValue={selectedLocation.smtpHost || ''}
|
||||
placeholder="smtp.example.com"
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="smtpPort"
|
||||
defaultValue={selectedLocation.smtpPort || 465}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="smtpSecure"
|
||||
defaultChecked={selectedLocation.smtpSecure ?? true}
|
||||
className="w-4 h-4 text-red-600 rounded focus:ring-red-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-300">SSL/TLS</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Benutzername
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="smtpUser"
|
||||
defaultValue={selectedLocation.smtpUser || ''}
|
||||
placeholder={selectedLocation.contactEmail}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="smtpPassword"
|
||||
defaultValue={selectedLocation.smtpPassword || ''}
|
||||
placeholder="••••••••"
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-6 border-t border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowEmailSettings(false);
|
||||
setSelectedLocation(null);
|
||||
}}
|
||||
className="flex-1 px-4 py-2 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg hover:from-red-700 hover:to-pink-700 transition-all shadow-lg"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
405
components/NewBookingForm.tsx
Normal file
405
components/NewBookingForm.tsx
Normal file
@@ -0,0 +1,405 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FiCalendar, FiMapPin } from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface NewBookingFormProps {
|
||||
locations: any[];
|
||||
user: any;
|
||||
}
|
||||
|
||||
export default function NewBookingForm({
|
||||
locations,
|
||||
user,
|
||||
}: NewBookingFormProps) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
locationId: "",
|
||||
model: "VINTAGE_SMILE",
|
||||
customerName: "",
|
||||
customerEmail: "",
|
||||
customerPhone: "",
|
||||
customerAddress: "",
|
||||
customerCity: "",
|
||||
customerZip: "",
|
||||
invoiceType: "PRIVATE",
|
||||
companyName: "",
|
||||
eventDate: "",
|
||||
eventAddress: "",
|
||||
eventCity: "",
|
||||
eventZip: "",
|
||||
eventLocation: "",
|
||||
setupTimeStart: "",
|
||||
setupTimeLatest: "",
|
||||
dismantleTimeEarliest: "",
|
||||
dismantleTimeLatest: "",
|
||||
calculatedPrice: 0,
|
||||
notes: "",
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/bookings/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || "Fehler beim Erstellen");
|
||||
}
|
||||
|
||||
alert("Buchung erfolgreich erstellt!");
|
||||
router.push(`/dashboard/bookings/${data.booking.id}`);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href="/dashboard/bookings"
|
||||
className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors"
|
||||
>
|
||||
← Zurück zu Buchungen
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">
|
||||
Neue Buchung erstellen
|
||||
</h2>
|
||||
<p className="text-gray-400 mt-1">Manuelle Buchung anlegen</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6 space-y-6"
|
||||
>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">
|
||||
Standort & Fotobox
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Standort
|
||||
</label>
|
||||
<select
|
||||
value={formData.locationId}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, locationId: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Bitte wählen...</option>
|
||||
{locations.map((loc) => (
|
||||
<option key={loc.id} value={loc.id}>
|
||||
{loc.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Modell
|
||||
</label>
|
||||
<select
|
||||
value={formData.model}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, model: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
>
|
||||
<option value="VINTAGE_SMILE">Vintage Smile</option>
|
||||
<option value="VINTAGE_PHOTOS">Vintage Photos</option>
|
||||
<option value="NOSTALGIE">Nostalgie</option>
|
||||
<option value="MAGIC_MIRROR">Magic Mirror</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Kundendaten</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Rechnungsart
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center text-white">
|
||||
<input
|
||||
type="radio"
|
||||
value="PRIVATE"
|
||||
checked={formData.invoiceType === "PRIVATE"}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, invoiceType: e.target.value })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
Privat
|
||||
</label>
|
||||
<label className="flex items-center text-white">
|
||||
<input
|
||||
type="radio"
|
||||
value="BUSINESS"
|
||||
checked={formData.invoiceType === "BUSINESS"}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, invoiceType: e.target.value })
|
||||
}
|
||||
className="mr-2"
|
||||
/>
|
||||
Geschäftlich
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.invoiceType === "BUSINESS" && (
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Firmenname
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.companyName}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, companyName: e.target.value })
|
||||
}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.customerName}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, customerName: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.customerEmail}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, customerEmail: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Telefon
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.customerPhone}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, customerPhone: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white mb-4">
|
||||
Event-Details
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Event-Datum
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.eventDate}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, eventDate: e.target.value })
|
||||
}
|
||||
required
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Preis (€)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.calculatedPrice}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
calculatedPrice: parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Event-Adresse
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.eventAddress}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, eventAddress: e.target.value })
|
||||
}
|
||||
required
|
||||
placeholder="Straße und Hausnummer"
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
PLZ
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.eventZip}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, eventZip: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Stadt
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.eventCity}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, eventCity: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Location-Name (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.eventLocation}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, eventLocation: e.target.value })
|
||||
}
|
||||
placeholder="z.B. Hotel Maritim"
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Aufbau ab
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={formData.setupTimeStart}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, setupTimeStart: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Aufbau spätestens
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={formData.setupTimeLatest}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, setupTimeLatest: e.target.value })
|
||||
}
|
||||
required
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Notizen (optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.notes}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, notes: e.target.value })
|
||||
}
|
||||
rows={4}
|
||||
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/20 border border-red-500/50 text-red-400 px-4 py-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4 pt-4 border-t border-gray-700">
|
||||
<Link
|
||||
href="/dashboard/bookings"
|
||||
className="flex-1 px-4 py-3 bg-gray-700 text-gray-300 rounded-lg font-semibold text-center hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="flex-1 px-4 py-3 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg font-semibold hover:from-red-700 hover:to-pink-700 transition-all shadow-lg disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Wird erstellt..." : "Buchung erstellen"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
components/SessionProvider.tsx
Normal file
17
components/SessionProvider.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react';
|
||||
|
||||
export default function SessionProvider({
|
||||
children,
|
||||
session,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
session: any;
|
||||
}) {
|
||||
return (
|
||||
<NextAuthSessionProvider session={session}>
|
||||
{children}
|
||||
</NextAuthSessionProvider>
|
||||
);
|
||||
}
|
||||
78
components/SignaturePad.tsx
Normal file
78
components/SignaturePad.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import SignatureCanvas from 'react-signature-canvas';
|
||||
import { FiRefreshCw, FiCheck } from 'react-icons/fi';
|
||||
|
||||
interface SignaturePadProps {
|
||||
onSave: (signatureData: string) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export default function SignaturePad({ onSave, onCancel }: SignaturePadProps) {
|
||||
const sigCanvas = useRef<SignatureCanvas>(null);
|
||||
const [isEmpty, setIsEmpty] = useState(true);
|
||||
|
||||
const clear = () => {
|
||||
sigCanvas.current?.clear();
|
||||
setIsEmpty(true);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
if (sigCanvas.current && !sigCanvas.current.isEmpty()) {
|
||||
const dataUrl = sigCanvas.current.toDataURL('image/png');
|
||||
onSave(dataUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnd = () => {
|
||||
setIsEmpty(sigCanvas.current?.isEmpty() || false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-gray-300 rounded-lg overflow-hidden bg-white">
|
||||
<SignatureCanvas
|
||||
ref={sigCanvas}
|
||||
canvasProps={{
|
||||
className: 'w-full h-64 cursor-crosshair',
|
||||
}}
|
||||
onEnd={handleEnd}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={clear}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
<FiRefreshCw /> Zurücksetzen
|
||||
</button>
|
||||
|
||||
{onCancel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={save}
|
||||
disabled={isEmpty}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FiCheck /> Unterschrift speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600">
|
||||
Bitte unterschreiben Sie mit Maus, Trackpad oder Touchscreen im Feld oben.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user