- Vintage Modell hinzugefuegt - Equipment Multi-Select (Neue Buchung + Bearbeitung) - Kundenadresse in Formularen - Bearbeiten-Seite fuer Buchungen - Abbau-Zeiten in Formularen und Uebersicht - Vertrag PDF nur bei Privatkunden - LexOffice Kontakt-Erstellung Fix (BUSINESS) - Zurueck-Pfeil auf Touren-Seite
378 lines
14 KiB
TypeScript
378 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { FiCalendar, FiTruck, FiCheckSquare, FiSquare, FiMapPin, FiClock, FiSave, FiAlertCircle } from 'react-icons/fi';
|
|
|
|
interface Driver {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
vehiclePlate: string | null;
|
|
vehicleModel: string | null;
|
|
available: boolean;
|
|
}
|
|
|
|
interface Booking {
|
|
id: string;
|
|
bookingNumber: string;
|
|
customerName: string;
|
|
eventDate: string;
|
|
eventAddress: string;
|
|
eventCity: string;
|
|
eventZip: string;
|
|
setupTimeStart: string;
|
|
setupTimeLatest: string;
|
|
status: string;
|
|
}
|
|
|
|
export default function NewTourPage() {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(true);
|
|
const [creating, setCreating] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [drivers, setDrivers] = useState<Driver[]>([]);
|
|
const [bookings, setBookings] = useState<Booking[]>([]);
|
|
|
|
const [tourDate, setTourDate] = useState('');
|
|
const [selectedDriver, setSelectedDriver] = useState('');
|
|
const [selectedBookings, setSelectedBookings] = useState<Set<string>>(new Set());
|
|
const [optimizationType, setOptimizationType] = useState<'fastest' | 'schedule'>('fastest');
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const [driversRes, bookingsRes] = await Promise.all([
|
|
fetch('/api/drivers?available=true'),
|
|
fetch('/api/bookings?status=READY_FOR_ASSIGNMENT,OPEN_FOR_DRIVERS'),
|
|
]);
|
|
|
|
if (!driversRes.ok || !bookingsRes.ok) {
|
|
throw new Error('Fehler beim Laden der Daten');
|
|
}
|
|
|
|
const driversData = await driversRes.json();
|
|
const bookingsData = await bookingsRes.json();
|
|
|
|
setDrivers(driversData.drivers || []);
|
|
setBookings(bookingsData.bookings || []);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Fehler beim Laden der Daten');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const toggleBooking = (bookingId: string) => {
|
|
const newSelected = new Set(selectedBookings);
|
|
if (newSelected.has(bookingId)) {
|
|
newSelected.delete(bookingId);
|
|
} else {
|
|
newSelected.add(bookingId);
|
|
}
|
|
setSelectedBookings(newSelected);
|
|
};
|
|
|
|
const selectAllOnDate = () => {
|
|
if (!tourDate) return;
|
|
|
|
const targetDate = new Date(tourDate).toISOString().split('T')[0];
|
|
const newSelected = new Set(selectedBookings);
|
|
|
|
bookings.forEach((booking) => {
|
|
const bookingDate = new Date(booking.eventDate).toISOString().split('T')[0];
|
|
if (bookingDate === targetDate) {
|
|
newSelected.add(booking.id);
|
|
}
|
|
});
|
|
|
|
setSelectedBookings(newSelected);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!tourDate) {
|
|
setError('Bitte wähle ein Tourdatum');
|
|
return;
|
|
}
|
|
|
|
if (selectedBookings.size === 0) {
|
|
setError('Bitte wähle mindestens eine Buchung aus');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setCreating(true);
|
|
setError(null);
|
|
|
|
const response = await fetch('/api/tours', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
tourDate,
|
|
driverId: selectedDriver || null,
|
|
bookingIds: Array.from(selectedBookings),
|
|
optimizationType,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || 'Fehler beim Erstellen der Tour');
|
|
}
|
|
|
|
const { tour } = await response.json();
|
|
router.push(`/dashboard/tours/${tour.id}`);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Fehler beim Erstellen der Tour');
|
|
setCreating(false);
|
|
}
|
|
};
|
|
|
|
const groupedBookings = bookings.reduce((acc, booking) => {
|
|
const date = new Date(booking.eventDate).toLocaleDateString('de-DE', {
|
|
weekday: 'short',
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
});
|
|
if (!acc[date]) acc[date] = [];
|
|
acc[date].push(booking);
|
|
return acc;
|
|
}, {} as Record<string, Booking[]>);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-8 flex items-center justify-center">
|
|
<div className="text-gray-400">Lade Daten...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-8 max-w-6xl mx-auto">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold text-white mb-2">Neue Tour erstellen</h1>
|
|
<p className="text-gray-400">Wähle Buchungen aus und weise sie einem Fahrer zu</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-6 bg-red-500/10 border border-red-500/50 rounded-lg p-4 flex items-start gap-3">
|
|
<FiAlertCircle className="text-red-400 mt-0.5 flex-shrink-0" size={20} />
|
|
<div className="text-red-400">{error}</div>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
<FiCalendar className="inline mr-2" />
|
|
Tourdatum *
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={tourDate}
|
|
onChange={(e) => setTourDate(e.target.value)}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white focus:border-pink-500 focus:outline-none"
|
|
/>
|
|
{tourDate && (
|
|
<button
|
|
type="button"
|
|
onClick={selectAllOnDate}
|
|
className="mt-2 text-sm text-pink-400 hover:text-pink-300"
|
|
>
|
|
Alle Buchungen an diesem Tag auswählen
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
<FiTruck className="inline mr-2" />
|
|
Fahrer zuweisen (optional)
|
|
</label>
|
|
<select
|
|
value={selectedDriver}
|
|
onChange={(e) => setSelectedDriver(e.target.value)}
|
|
className="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white focus:border-pink-500 focus:outline-none"
|
|
>
|
|
<option value="">-- Später zuweisen --</option>
|
|
{drivers.map((driver) => (
|
|
<option key={driver.id} value={driver.id}>
|
|
{driver.name}
|
|
{driver.vehiclePlate && ` (${driver.vehiclePlate})`}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{drivers.length === 0 && (
|
|
<p className="mt-2 text-sm text-yellow-400">
|
|
Keine verfügbaren Fahrer
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6">
|
|
<label className="block text-sm font-medium text-gray-300 mb-4">
|
|
Route-Optimierung
|
|
</label>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
value="fastest"
|
|
checked={optimizationType === 'fastest'}
|
|
onChange={(e) => setOptimizationType(e.target.value as 'fastest')}
|
|
className="text-pink-500 focus:ring-pink-500"
|
|
/>
|
|
<span className="text-white">Kürzeste Route</span>
|
|
<span className="text-gray-400 text-sm">(Entfernung)</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
value="schedule"
|
|
checked={optimizationType === 'schedule'}
|
|
onChange={(e) => setOptimizationType(e.target.value as 'schedule')}
|
|
className="text-pink-500 focus:ring-pink-500"
|
|
/>
|
|
<span className="text-white">Nach Zeitfenster</span>
|
|
<span className="text-gray-400 text-sm">(Aufbauzeiten)</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<label className="text-sm font-medium text-gray-300">
|
|
Buchungen auswählen * ({selectedBookings.size} ausgewählt)
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (selectedBookings.size === bookings.length) {
|
|
setSelectedBookings(new Set());
|
|
} else {
|
|
setSelectedBookings(new Set(bookings.map(b => b.id)));
|
|
}
|
|
}}
|
|
className="text-sm text-pink-400 hover:text-pink-300"
|
|
>
|
|
{selectedBookings.size === bookings.length ? 'Alle abwählen' : 'Alle auswählen'}
|
|
</button>
|
|
</div>
|
|
|
|
{bookings.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-400">
|
|
<FiMapPin className="mx-auto mb-2" size={32} />
|
|
<p>Keine verfügbaren Buchungen</p>
|
|
<p className="text-sm mt-1">
|
|
Buchungen müssen den Status "Bereit zur Zuweisung" oder "Offen für Fahrer" haben
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4 max-h-[500px] overflow-y-auto">
|
|
{Object.entries(groupedBookings).map(([date, dateBookings]) => (
|
|
<div key={date}>
|
|
<div className="text-sm font-medium text-gray-400 mb-2 sticky top-0 bg-gray-800/90 py-2">
|
|
{date}
|
|
</div>
|
|
<div className="space-y-2">
|
|
{dateBookings.map((booking) => (
|
|
<div
|
|
key={booking.id}
|
|
onClick={() => toggleBooking(booking.id)}
|
|
className={`
|
|
p-4 rounded-lg border cursor-pointer transition-all
|
|
${selectedBookings.has(booking.id)
|
|
? 'bg-pink-500/10 border-pink-500/50'
|
|
: 'bg-gray-900/50 border-gray-700 hover:border-gray-600'
|
|
}
|
|
`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="mt-1">
|
|
{selectedBookings.has(booking.id) ? (
|
|
<FiCheckSquare className="text-pink-400" size={20} />
|
|
) : (
|
|
<FiSquare className="text-gray-500" size={20} />
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="font-medium text-white">
|
|
{booking.bookingNumber}
|
|
</span>
|
|
<span className="text-gray-400">•</span>
|
|
<span className="text-gray-300">
|
|
{booking.customerName}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4 text-sm text-gray-400">
|
|
<div className="flex items-center gap-1">
|
|
<FiMapPin size={14} />
|
|
{booking.eventCity}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<FiClock size={14} />
|
|
{new Date(booking.setupTimeStart).toLocaleTimeString('de-DE', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
{' - '}
|
|
{new Date(booking.setupTimeLatest).toLocaleTimeString('de-DE', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="text-sm text-gray-500 mt-1">
|
|
{booking.eventAddress}, {booking.eventZip} {booking.eventCity}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => router.back()}
|
|
disabled={creating}
|
|
className="px-6 py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={creating || selectedBookings.size === 0 || !tourDate}
|
|
className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-gradient-to-r from-pink-600 to-red-600 text-white rounded-lg hover:from-pink-700 hover:to-red-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg"
|
|
>
|
|
<FiSave />
|
|
{creating ? 'Erstelle Tour...' : 'Tour erstellen'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|