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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user