- 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
210 lines
8.0 KiB
TypeScript
210 lines
8.0 KiB
TypeScript
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from '@/lib/auth';
|
|
import { redirect } from 'next/navigation';
|
|
import { prisma } from '@/lib/prisma';
|
|
import Link from 'next/link';
|
|
import { FiPlus, FiCalendar, FiTruck, FiMapPin, FiClock } from 'react-icons/fi';
|
|
|
|
export default async function ToursPage() {
|
|
const session = await getServerSession(authOptions);
|
|
|
|
if (!session || session.user.role !== 'ADMIN') {
|
|
redirect('/login');
|
|
}
|
|
|
|
// Hole alle Touren, sortiert nach Datum
|
|
const tours = await prisma.tour.findMany({
|
|
include: {
|
|
driver: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
vehiclePlate: true,
|
|
},
|
|
},
|
|
bookings: {
|
|
select: {
|
|
id: true,
|
|
bookingNumber: true,
|
|
customerName: true,
|
|
eventCity: true,
|
|
},
|
|
},
|
|
tourStops: {
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
tourDate: 'desc',
|
|
},
|
|
take: 50,
|
|
});
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'PLANNED': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
|
|
case 'IN_PROGRESS': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
|
case 'COMPLETED': return 'bg-green-500/20 text-green-400 border-green-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) => {
|
|
switch (status) {
|
|
case 'PLANNED': return 'Geplant';
|
|
case 'IN_PROGRESS': return 'Unterwegs';
|
|
case 'COMPLETED': return 'Abgeschlossen';
|
|
case 'CANCELLED': return 'Storniert';
|
|
default: return status;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-8">
|
|
<div className="mb-8">
|
|
<Link
|
|
href="/dashboard"
|
|
className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors"
|
|
>
|
|
← Zurück zum Dashboard
|
|
</Link>
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-white">Touren</h1>
|
|
<p className="text-gray-400 mt-1">Verwalte Fahrer-Touren und Route-Optimierung</p>
|
|
</div>
|
|
<Link
|
|
href="/dashboard/tours/new"
|
|
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-pink-600 to-red-600 text-white rounded-lg hover:from-pink-700 hover:to-red-700 transition-all shadow-lg"
|
|
>
|
|
<FiPlus />
|
|
Neue Tour erstellen
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{tours.length === 0 ? (
|
|
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-12 text-center">
|
|
<FiTruck className="mx-auto text-gray-500 text-5xl mb-4" />
|
|
<h3 className="text-xl font-bold text-gray-300 mb-2">Noch keine Touren</h3>
|
|
<p className="text-gray-400 mb-6">Erstelle deine erste Tour, um Buchungen zu Fahrern zuzuweisen.</p>
|
|
<Link
|
|
href="/dashboard/tours/new"
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-pink-600 to-red-600 text-white rounded-lg hover:from-pink-700 hover:to-red-700 transition-all"
|
|
>
|
|
<FiPlus />
|
|
Tour erstellen
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-4">
|
|
{tours.map((tour) => {
|
|
const completedStops = tour.tourStops.filter(s =>
|
|
s.status === 'SETUP_COMPLETE' || s.status === 'PICKUP_COMPLETE'
|
|
).length;
|
|
const totalStops = tour.tourStops.length;
|
|
const progress = totalStops > 0 ? (completedStops / totalStops) * 100 : 0;
|
|
|
|
return (
|
|
<Link
|
|
key={tour.id}
|
|
href={`/dashboard/tours/${tour.id}`}
|
|
className="block bg-gray-800/50 border border-gray-700 rounded-lg p-6 hover:border-pink-500/50 hover:bg-gray-800/70 transition-all"
|
|
>
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="text-xl font-bold text-white">{tour.tourNumber}</h3>
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${getStatusColor(tour.status)}`}>
|
|
{getStatusLabel(tour.status)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-6 text-sm text-gray-400">
|
|
<div className="flex items-center gap-2">
|
|
<FiCalendar size={16} />
|
|
{new Date(tour.tourDate).toLocaleDateString('de-DE', {
|
|
weekday: 'short',
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
})}
|
|
</div>
|
|
|
|
{tour.driver && (
|
|
<div className="flex items-center gap-2">
|
|
<FiTruck size={16} />
|
|
{tour.driver.name}
|
|
{tour.driver.vehiclePlate && (
|
|
<span className="text-gray-500">({tour.driver.vehiclePlate})</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2">
|
|
<FiMapPin size={16} />
|
|
{totalStops} {totalStops === 1 ? 'Stopp' : 'Stopps'}
|
|
</div>
|
|
|
|
{tour.estimatedDuration && (
|
|
<div className="flex items-center gap-2">
|
|
<FiClock size={16} />
|
|
{Math.round(tour.estimatedDuration / 60)}h
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Fortschrittsbalken */}
|
|
{totalStops > 0 && tour.status !== 'CANCELLED' && (
|
|
<div className="mt-4">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-xs text-gray-400">
|
|
Fortschritt: {completedStops} von {totalStops} Stopps abgeschlossen
|
|
</span>
|
|
<span className="text-xs text-gray-400">{progress.toFixed(0)}%</span>
|
|
</div>
|
|
<div className="w-full bg-gray-700 rounded-full h-2">
|
|
<div
|
|
className="bg-gradient-to-r from-pink-500 to-red-500 h-2 rounded-full transition-all"
|
|
style={{ width: `${progress}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Buchungen-Preview */}
|
|
{tour.bookings.length > 0 && (
|
|
<div className="mt-4 pt-4 border-t border-gray-700">
|
|
<div className="text-xs text-gray-400 mb-2">Buchungen:</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{tour.bookings.slice(0, 5).map((booking) => (
|
|
<span
|
|
key={booking.id}
|
|
className="px-2 py-1 bg-gray-700/50 text-gray-300 text-xs rounded"
|
|
>
|
|
{booking.bookingNumber}
|
|
</span>
|
|
))}
|
|
{tour.bookings.length > 5 && (
|
|
<span className="px-2 py-1 text-gray-500 text-xs">
|
|
+{tour.bookings.length - 5} weitere
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|