feat: Equipment-System, Buchungsbearbeitung, Kundenadresse, LexOffice-Fix
- Vintage Modell hinzugefuegt - Equipment Multi-Select (Neue Buchung + Bearbeitung) - Kundenadresse in Formularen - Bearbeiten-Seite fuer Buchungen - Abbau-Zeiten in Formularen und Uebersicht - Vertrag PDF nur bei Privatkunden - LexOffice Kontakt-Erstellung Fix (BUSINESS) - Zurueck-Pfeil auf Touren-Seite
This commit is contained in:
377
app/dashboard/tours/new/page.tsx
Normal file
377
app/dashboard/tours/new/page.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user