Files
Atlas/app/dashboard/tours/page.tsx
2025-11-12 20:21:32 +01:00

431 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { formatDate } from '@/lib/date-utils';
import DashboardSidebar from '@/components/DashboardSidebar';
import { useSession } from 'next-auth/react';
export default function ToursPage() {
const router = useRouter();
const { data: session } = useSession();
const [tours, setTours] = useState<any[]>([]);
const [drivers, setDrivers] = useState<any[]>([]);
const [bookings, setBookings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({
tourDate: '',
driverId: '',
bookingIds: [] as string[],
optimizationType: 'fastest' as 'fastest' | 'schedule',
});
useEffect(() => {
fetchTours();
fetchDrivers();
fetchUnassignedBookings();
}, []);
const fetchTours = async () => {
try {
const res = await fetch('/api/tours');
const data = await res.json();
setTours(data.tours || []);
} catch (error) {
console.error('Fetch error:', error);
} finally {
setLoading(false);
}
};
const fetchDrivers = async () => {
try {
const res = await fetch('/api/drivers?available=true');
const data = await res.json();
setDrivers(data.drivers || []);
} catch (error) {
console.error('Drivers fetch error:', error);
}
};
const fetchUnassignedBookings = async () => {
try {
const res = await fetch('/api/bookings');
const data = await res.json();
const unassigned = (data.bookings || []).filter((b: any) => {
// Must be confirmed and not assigned to a tour
if (!b.tourId && b.status === 'CONFIRMED') {
// If booking has setup windows, check if any are already selected
if (b.setupWindows && b.setupWindows.length > 0) {
const hasSelectedWindow = b.setupWindows.some((w: any) => w.selected);
return !hasSelectedWindow; // Exclude if any window is already selected
}
return true; // No setup windows, just check tourId
}
return false;
});
setBookings(unassigned);
} catch (error) {
console.error('Bookings fetch error:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const res = await fetch('/api/tours', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (res.ok) {
setShowForm(false);
setFormData({
tourDate: '',
driverId: '',
bookingIds: [],
optimizationType: 'fastest',
});
fetchTours();
fetchUnassignedBookings();
} else {
alert('Fehler beim Erstellen');
}
} catch (error) {
console.error('Create error:', error);
alert('Fehler beim Erstellen');
}
};
const toggleBooking = (bookingId: string) => {
setFormData(prev => ({
...prev,
bookingIds: prev.bookingIds.includes(bookingId)
? prev.bookingIds.filter(id => id !== bookingId)
: [...prev.bookingIds, bookingId],
}));
};
// Filter bookings by selected tour date
const availableBookings = formData.tourDate
? bookings.filter(booking => {
const bookingDate = new Date(booking.eventDate).toISOString().split('T')[0];
const tourDate = formData.tourDate;
// Check if event date matches
if (bookingDate === tourDate) return true;
// Check if any setup window date matches
if (booking.setupWindows && booking.setupWindows.length > 0) {
return booking.setupWindows.some((window: any) => {
const windowDate = new Date(window.setupDate).toISOString().split('T')[0];
return windowDate === tourDate && !window.selected;
});
}
return false;
})
: bookings;
// Group bookings by date for display
const bookingsByDate = bookings.reduce((acc: any, booking: any) => {
const date = new Date(booking.eventDate).toISOString().split('T')[0];
if (!acc[date]) acc[date] = [];
acc[date].push(booking);
return acc;
}, {});
const getStatusBadge = (status: string) => {
const styles: Record<string, string> = {
PLANNED: 'bg-blue-500/20 text-blue-400 border-blue-500/50',
IN_PROGRESS: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50',
COMPLETED: 'bg-green-500/20 text-green-400 border-green-500/50',
CANCELLED: 'bg-red-500/20 text-red-400 border-red-500/50',
};
return styles[status] || 'bg-gray-500/20 text-gray-400 border-gray-500/50';
};
const getStatusLabel = (status: string) => {
const labels: Record<string, string> = {
PLANNED: 'Geplant',
IN_PROGRESS: 'In Arbeit',
COMPLETED: 'Abgeschlossen',
CANCELLED: 'Abgebrochen',
};
return labels[status] || status;
};
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
<div className="flex">
<DashboardSidebar user={session?.user} />
<div className="flex-1 p-8">
<p className="text-gray-300">Lädt...</p>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
<div className="flex">
<DashboardSidebar user={session?.user} />
<main className="flex-1 p-8">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-400 to-cyan-500 bg-clip-text text-transparent">
Touren
</h1>
<p className="text-gray-400 mt-1">Verwalten Sie Fahrer-Touren</p>
</div>
<button
onClick={() => setShowForm(true)}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg hover:from-blue-700 hover:to-cyan-700 font-semibold shadow-lg"
>
+ Neue Tour
</button>
</div>
{showForm && (
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50 overflow-y-auto">
<div className="bg-gradient-to-br from-gray-800 to-gray-900 rounded-xl shadow-2xl max-w-3xl w-full p-8 border border-gray-700 my-8">
<h2 className="text-2xl font-bold mb-6 text-white">Neue Tour erstellen</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Tour-Datum *
</label>
<input
type="date"
value={formData.tourDate}
onChange={(e) => setFormData({ ...formData, tourDate: 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">
Fahrer
</label>
<select
value={formData.driverId}
onChange={(e) => setFormData({ ...formData, driverId: 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"
>
<option value="">Noch keinen Fahrer zuweisen</option>
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>
{driver.name} {driver.vehiclePlate ? `(${driver.vehiclePlate})` : ''}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Routen-Optimierung
</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setFormData({ ...formData, optimizationType: 'fastest' })}
className={`px-4 py-3 rounded-lg border-2 transition-all ${
formData.optimizationType === 'fastest'
? 'bg-blue-600 border-blue-500 text-white'
: 'bg-gray-700 border-gray-600 text-gray-300 hover:border-gray-500'
}`}
>
<div className="font-semibold">🚗 Schnellste Route</div>
<div className="text-xs mt-1 opacity-80">Nach Distanz/Zeit</div>
</button>
<button
type="button"
onClick={() => setFormData({ ...formData, optimizationType: 'schedule' })}
className={`px-4 py-3 rounded-lg border-2 transition-all ${
formData.optimizationType === 'schedule'
? 'bg-purple-600 border-purple-500 text-white'
: 'bg-gray-700 border-gray-600 text-gray-300 hover:border-gray-500'
}`}
>
<div className="font-semibold"> Nach Aufbauzeiten</div>
<div className="text-xs mt-1 opacity-80">Zeitfenster beachten</div>
</button>
</div>
<p className="text-xs text-gray-400 mt-2">
{formData.optimizationType === 'fastest'
? 'Optimiert nach kürzester Strecke/Zeit'
: 'Berücksichtigt Aufbau-Zeitfenster der Buchungen'}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Buchungen zuordnen ({formData.bookingIds.length} ausgewählt)
</label>
{!formData.tourDate && (
<div className="bg-yellow-500/20 border border-yellow-500/50 rounded-lg p-4 mb-4">
<p className="text-yellow-300 text-sm">
Bitte wähle zuerst ein Tour-Datum aus, um passende Buchungen zu sehen
</p>
</div>
)}
{formData.tourDate && availableBookings.length === 0 && (
<div className="bg-blue-500/20 border border-blue-500/50 rounded-lg p-4 mb-4">
<p className="text-blue-300 text-sm">
Keine bestätigten Buchungen für {new Date(formData.tourDate).toLocaleDateString('de-DE')} gefunden
</p>
</div>
)}
<div className="bg-gray-700/50 border border-gray-600 rounded-lg p-4 max-h-64 overflow-y-auto">
{availableBookings.length > 0 ? (
<div className="space-y-2">
{availableBookings.map((booking) => {
const bookingDate = new Date(booking.eventDate).toISOString().split('T')[0];
const isEventDate = bookingDate === formData.tourDate;
const matchingWindows = booking.setupWindows?.filter((w: any) => {
const windowDate = new Date(w.setupDate).toISOString().split('T')[0];
return windowDate === formData.tourDate && !w.selected;
}) || [];
return (
<label
key={booking.id}
className="flex items-center gap-3 p-3 bg-gray-700 rounded-lg cursor-pointer hover:bg-gray-600"
>
<input
type="checkbox"
checked={formData.bookingIds.includes(booking.id)}
onChange={() => toggleBooking(booking.id)}
className="w-4 h-4"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<p className="text-white font-medium">{booking.bookingNumber}</p>
{!isEventDate && matchingWindows.length > 0 && (
<span className="px-2 py-0.5 bg-purple-600 text-white text-xs rounded-full">
📦 Flexibler Aufbau
</span>
)}
</div>
<p className="text-sm text-gray-400">
{booking.customerName} - Event: {formatDate(booking.eventDate)}
</p>
<p className="text-xs text-gray-500">{booking.eventAddress}, {booking.eventCity}</p>
{!isEventDate && matchingWindows.length > 0 && (
<div className="mt-2 space-y-1">
{matchingWindows.map((window: any) => (
<p key={window.id} className="text-xs text-purple-400">
🕐 Aufbau-Option: {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' })}
{window.preferred && ' ⭐'}
</p>
))}
</div>
)}
{isEventDate && booking.setupTimeStart && (
<p className="text-xs text-blue-400 mt-1">
Aufbau: {new Date(booking.setupTimeStart).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
{booking.setupTimeLatest && ` - ${new Date(booking.setupTimeLatest).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`}
</p>
)}
</div>
</label>
);
})}
</div>
) : (
<p className="text-gray-400 text-center py-4">
{formData.tourDate ? 'Keine Buchungen für dieses Datum' : 'Bitte Datum auswählen'}
</p>
)}
</div>
</div>
<div className="flex gap-4">
<button
type="button"
onClick={() => setShowForm(false)}
className="flex-1 px-6 py-3 bg-gray-700 text-gray-300 rounded-lg hover:bg-gray-600 font-semibold"
>
Abbrechen
</button>
<button
type="submit"
className="flex-1 px-6 py-3 bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg hover:from-blue-700 hover:to-cyan-700 font-semibold"
>
Erstellen
</button>
</div>
</form>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tours.map((tour) => (
<div
key={tour.id}
onClick={() => router.push(`/dashboard/tours/${tour.id}`)}
className="bg-gradient-to-br from-gray-800 to-gray-900 rounded-lg shadow-xl p-6 hover:shadow-2xl transition-all cursor-pointer border border-gray-700 hover:border-blue-500"
>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-lg font-bold text-white">{tour.tourNumber}</h3>
<p className="text-sm text-gray-400">{formatDate(tour.tourDate)}</p>
</div>
<span className={`px-3 py-1 text-xs font-semibold rounded-full border ${getStatusBadge(tour.status)}`}>
{getStatusLabel(tour.status)}
</span>
</div>
<div className="text-sm text-gray-400 space-y-2">
<p>
<span className="font-medium text-gray-300">Fahrer:</span>{' '}
{tour.driver ? tour.driver.name : 'Nicht zugewiesen'}
</p>
<p>
<span className="font-medium text-gray-300">Buchungen:</span> {tour.bookings.length}
</p>
{tour.totalDistance && (
<p>
<span className="font-medium text-gray-300">Strecke:</span> {tour.totalDistance} km
</p>
)}
{tour.estimatedDuration && (
<p>
<span className="font-medium text-gray-300">Dauer:</span> ~{tour.estimatedDuration} Min
</p>
)}
</div>
</div>
))}
</div>
{tours.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-400">Noch keine Touren vorhanden</p>
</div>
)}
</div>
</main>
</div>
</div>
);
}