Files
Atlas/app/dashboard/tours/new/page.tsx
Julia Wehden a2c95c70e7 feat: Equipment-System, Buchungsbearbeitung, Kundenadresse, LexOffice-Fix
- Vintage Modell hinzugefuegt
- Equipment Multi-Select (Neue Buchung + Bearbeitung)
- Kundenadresse in Formularen
- Bearbeiten-Seite fuer Buchungen
- Abbau-Zeiten in Formularen und Uebersicht
- Vertrag PDF nur bei Privatkunden
- LexOffice Kontakt-Erstellung Fix (BUSINESS)
- Zurueck-Pfeil auf Touren-Seite
2026-03-19 16:21:55 +01:00

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>
);
}