import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Link as RouterLink } from "react-router-dom"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { formatDate } from "../utils/attendanceHelpers"; import { formatKm, todayLocalStr } from "../utils/formatters"; import apiFetch from "../utils/api"; import { tripListOptions, tripVehiclesOptions, type BackendTrip, } from "../lib/queries/trips"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, Modal, ConfirmDialog, Field, TextField, Select, DateField, StatusChip, LoadingState, StatCard, PageEnter, type DataColumn, } from "../ui"; const API_BASE = "/api/admin"; interface TripForm { vehicle_id: string; trip_date: string; start_km: string | number; end_km: string | number; route_from: string; route_to: string; is_business: number; notes: string; } const PlusIcon = ( ); const EditIcon = ( ); const DeleteIcon = ( ); const TripsIcon = ( ); const TotalIcon = ( ); const BusinessIcon = ( ); const PrivateIcon = ( ); export default function Trips() { const alert = useAlert(); const { hasPermission } = useAuth(); const { data: tripsData, isPending: tripsLoading } = useQuery( tripListOptions({}), ); const { data: vehiclesData } = useQuery(tripVehiclesOptions()); const trips = tripsData ?? []; const vehicles = vehiclesData ?? []; const [showModal, setShowModal] = useState(false); const [editingTrip, setEditingTrip] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; tripId: number | null; }>({ show: false, tripId: null }); const [form, setForm] = useState({ vehicle_id: "", trip_date: todayLocalStr(), start_km: "", end_km: "", route_from: "", route_to: "", is_business: 1, notes: "", }); const [errors, setErrors] = useState>({}); const [, setLastKm] = useState(0); if (!hasPermission("trips.record")) return ; const submitMutation = useApiMutation< TripForm, { message?: string; error?: string } >({ url: (formData) => editingTrip ? `${API_BASE}/trips/${editingTrip.id}` : `${API_BASE}/trips`, method: () => (editingTrip ? "PUT" : "POST"), invalidate: ["trips", "vehicles"], onSuccess: (data) => { setShowModal(false); alert.success(data?.message || "Hotovo"); }, }); const deleteMutation = useApiMutation< number, { message?: string; error?: string } >({ url: (id) => `${API_BASE}/trips/${id}`, method: () => "DELETE", invalidate: ["trips", "vehicles"], onSuccess: (data) => { alert.success(data?.message || "Smazáno"); setDeleteConfirm({ show: false, tripId: null }); }, }); const fetchLastKm = async (vehicleId: string) => { if (!vehicleId) { setLastKm(0); return; } try { const response = await apiFetch(`${API_BASE}/trips/last-km/${vehicleId}`); const result = await response.json(); if (result.success) { const km = result.data?.last_km || 0; setLastKm(km); if (!editingTrip) { setForm((prev) => ({ ...prev, start_km: km })); } return; } } catch { /* fallback below */ } setLastKm(0); }; const openCreateModal = () => { setEditingTrip(null); setForm({ vehicle_id: "", trip_date: todayLocalStr(), start_km: "", end_km: "", route_from: "", route_to: "", is_business: 1, notes: "", }); setLastKm(0); setErrors({}); setShowModal(true); }; const openEditModal = (trip: BackendTrip) => { setEditingTrip(trip); setForm({ vehicle_id: String(trip.vehicle_id), trip_date: trip.trip_date, start_km: trip.start_km, end_km: trip.end_km, route_from: trip.route_from, route_to: trip.route_to, is_business: Number(trip.is_business), notes: trip.notes || "", }); setLastKm(trip.start_km); setErrors({}); setShowModal(true); }; const handleVehicleChange = (vehicleId: string) => { setForm((prev) => ({ ...prev, vehicle_id: vehicleId })); fetchLastKm(vehicleId); }; const validateForm = (): boolean => { const newErrors: Record = {}; if (!form.vehicle_id) newErrors.vehicle_id = "Vyberte vozidlo"; if (!form.trip_date) newErrors.trip_date = "Zadejte datum"; if (!form.start_km) newErrors.start_km = "Zadejte počáteční km"; if (!form.end_km) newErrors.end_km = "Zadejte konečný km"; if ( form.start_km && form.end_km && parseInt(String(form.end_km)) <= parseInt(String(form.start_km)) ) { newErrors.end_km = "Musí být větší než počáteční"; } if (!form.route_from) newErrors.route_from = "Zadejte místo odjezdu"; if (!form.route_to) newErrors.route_to = "Zadejte místo příjezdu"; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async () => { if (!validateForm()) return; try { await submitMutation.mutateAsync(form); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleDelete = async (tripId: number) => { try { await deleteMutation.mutateAsync(tripId); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const calculateDistance = (): number => { const start = parseInt(String(form.start_km)) || 0; const end = parseInt(String(form.end_km)) || 0; return end > start ? end - start : 0; }; if (tripsLoading) { return ; } const totals = trips.reduce( (acc, t) => { const dist = t.distance ?? t.end_km - t.start_km; acc.count++; acc.total += dist; if (t.is_business) acc.business += dist; else acc.private += dist; return acc; }, { total: 0, business: 0, private: 0, count: 0 }, ); const recentTrips = trips.slice(0, 10); const columns: DataColumn[] = [ { key: "trip_date", header: "Datum", width: "12%", mono: true, render: (trip) => formatDate(trip.trip_date), }, { key: "vehicle", header: "Vozidlo", width: "12%", render: (trip) => ( ), }, { key: "driver", header: "Řidič", width: "16%", render: (trip) => trip.users ? `${trip.users.first_name} ${trip.users.last_name}` : "", }, { key: "route", header: "Trasa", width: "22%", render: (trip) => ( {trip.route_from} → {trip.route_to} ), }, { key: "distance", header: "Vzdálenost", width: "12%", mono: true, bold: true, render: (trip) => `${formatKm(trip.distance ?? trip.end_km - trip.start_km)} km`, }, { key: "type", header: "Typ", width: "12%", render: (trip) => ( ), }, { key: "actions", header: "Akce", width: "10%", align: "right", render: (trip) => ( openEditModal(trip)} aria-label="Upravit" title="Upravit" > {EditIcon} setDeleteConfirm({ show: true, tripId: trip.id })} aria-label="Smazat" title="Smazat" > {DeleteIcon} ), }, ]; return ( Kniha jízd {new Date().toLocaleDateString("cs-CZ", { month: "long", year: "numeric", })} {/* Stats Cards */} {/* Recent Trips */} Poslední jízdy columns={columns} rows={recentTrips} rowKey={(trip) => trip.id} empty={ Zatím nemáte žádné záznamy jízd. } /> {/* Add/Edit Modal */} setShowModal(false)} onSubmit={handleSubmit} title={editingTrip ? "Upravit jízdu" : "Přidat jízdu"} loading={submitMutation.isPending} maxWidth="md" > setForm({ ...form, is_business: parseInt(value) }) } options={[ { value: "1", label: "Služební" }, { value: "0", label: "Soukromá" }, ]} /> setForm({ ...form, notes: e.target.value })} placeholder="Volitelné poznámky..." /> setDeleteConfirm({ show: false, tripId: null })} onConfirm={() => handleDelete(deleteConfirm.tripId!)} title="Smazat jízdu" message="Opravdu chcete smazat tento záznam?" confirmText="Smazat" cancelText="Zrušit" confirmVariant="danger" loading={deleteMutation.isPending} /> ); }