import { useState, useRef } 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 { tripListOptions, tripVehiclesOptions, tripUsersOptions, type BackendTrip, } from "../lib/queries/trips"; import { companySettingsOptions } from "../lib/queries/settings"; import { formatDate } from "../utils/attendanceHelpers"; import { formatKm } from "../utils/formatters"; import { useApiMutation } from "../lib/queries/mutations"; import { Button, Card, DataTable, Modal, ConfirmDialog, Field, TextField, Select, DateField, MonthField, StatusChip, FilterBar, LoadingState, EmptyState, StatCard, PageEnter, type DataColumn, } from "../ui"; const API_BASE = "/api/admin"; interface Trip { id: number; vehicle_id: number | string; trip_date: string; start_km: number; end_km: number; distance: number; route_from: string; route_to: string; is_business: number | boolean; notes?: string; spz: string; driver_name: string; } interface EditForm { 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; } function mapTrip(bt: BackendTrip): Trip { const distance = bt.distance ?? bt.end_km - bt.start_km; return { id: bt.id, vehicle_id: bt.vehicle_id, trip_date: bt.trip_date, start_km: bt.start_km, end_km: bt.end_km, distance, route_from: bt.route_from, route_to: bt.route_to, is_business: bt.is_business ? 1 : 0, notes: bt.notes || undefined, spz: bt.vehicles?.spz ?? "", driver_name: bt.users ? `${bt.users.first_name} ${bt.users.last_name}` : "", }; } const PrintIcon = ( ); const EditIcon = ( ); const DeleteIcon = ( ); const TripsIcon = ( ); const TotalIcon = ( ); const BusinessIcon = ( ); export default function TripsAdmin() { const alert = useAlert(); const { hasPermission } = useAuth(); const [filterPeriod, setFilterPeriod] = useState(() => { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; }); const [filterVehicleId, setFilterVehicleId] = useState(""); const [filterUserId, setFilterUserId] = useState(""); const printRef = useRef(null); const [showEditModal, setShowEditModal] = useState(false); const [editingTrip, setEditingTrip] = useState(null); const [editForm, setEditForm] = useState({ vehicle_id: "", trip_date: "", start_km: "", end_km: "", route_from: "", route_to: "", is_business: 1, notes: "", }); const [deleteConfirm, setDeleteConfirm] = useState<{ show: boolean; trip: Trip | null; }>({ show: false, trip: null }); const { data: vehiclesData = [] } = useQuery(tripVehiclesOptions()); const vehicles = vehiclesData; const { data: tripUsersData = [] } = useQuery(tripUsersOptions()); const tripUsers = tripUsersData; const { data: companySettings } = useQuery(companySettingsOptions()); const companyName = companySettings?.company_name ?? ""; const { data: tripsData, isPending } = useQuery( tripListOptions({ month: Number(filterPeriod.slice(5, 7)) || undefined, year: Number(filterPeriod.slice(0, 4)) || undefined, vehicleId: filterVehicleId ? Number(filterVehicleId) : undefined, userId: filterUserId ? Number(filterUserId) : undefined, perPage: 100, }), ); const trips = (tripsData ?? []).map(mapTrip); // useApiMutation JSON.stringifies the whole TIn as the request body, so // TIn must match the backend schema (UpdateTripSchema) shape directly — // NOT a wrapper that nests the body. id is captured in the URL closure. const editMutation = useApiMutation< { id: number; 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; }, { message?: string; error?: string } >({ url: ({ id }) => `${API_BASE}/trips/${id}`, method: () => "PUT", invalidate: ["trips", "vehicles"], }); const deleteMutation = useApiMutation< number, { message?: string; error?: string } >({ url: (id) => `${API_BASE}/trips/${id}`, method: () => "DELETE", invalidate: ["trips", "vehicles"], onSuccess: (data) => { setDeleteConfirm({ show: false, trip: null }); alert.success(data?.message || "Smazáno"); }, }); if (!hasPermission("trips.manage")) return ; const openEditModal = (trip: Trip) => { setEditingTrip(trip); setEditForm({ 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 || "", }); setShowEditModal(true); }; const handleEditSubmit = async () => { if (!editingTrip) return; if ( parseInt(String(editForm.end_km)) <= parseInt(String(editForm.start_km)) ) { alert.error("Konečný stav km musí být větší než počáteční"); return; } try { const result = await editMutation.mutateAsync({ id: editingTrip.id, vehicle_id: editForm.vehicle_id, trip_date: editForm.trip_date, start_km: editForm.start_km, end_km: editForm.end_km, route_from: editForm.route_from, route_to: editForm.route_to, is_business: editForm.is_business, notes: editForm.notes, }); setShowEditModal(false); alert.success(result?.message || "Upraveno"); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const handleDelete = async () => { if (!deleteConfirm.trip) return; try { await deleteMutation.mutateAsync(deleteConfirm.trip.id); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } }; const getPeriodName = () => new Date( Number(filterPeriod.slice(0, 4)), Number(filterPeriod.slice(5, 7)) - 1, ).toLocaleString("cs-CZ", { month: "long", year: "numeric" }); const getSelectedVehicleName = () => { if (!filterVehicleId) return null; const v = vehicles.find((v) => String(v.id) === filterVehicleId); return v ? `${v.spz} - ${v.name}` : null; }; const getSelectedUserName = () => { if (!filterUserId) return null; const u = tripUsers.find((u) => String(u.id) === filterUserId); return u?.name || null; }; const handlePrint = () => { const periodName = getPeriodName(); setTimeout(() => { if (printRef.current) { const content = printRef.current.innerHTML; const printWindow = window.open("", "_blank"); if (!printWindow) return; printWindow.document.write(` Kniha jízd - ${periodName} ${content} `); printWindow.document.close(); printWindow.onload = () => { printWindow.print(); }; } }, 100); }; const calculateDistance = (): number => { const start = parseInt(String(editForm.start_km)) || 0; const end = parseInt(String(editForm.end_km)) || 0; return end > start ? end - start : 0; }; const totals = { count: trips.length, total: trips.reduce((sum, t) => sum + t.distance, 0), business: trips .filter((t) => Number(t.is_business)) .reduce((sum, t) => sum + t.distance, 0), }; const columns: DataColumn[] = [ { key: "trip_date", header: "Datum", width: "11%", mono: true, render: (trip) => formatDate(trip.trip_date), }, { key: "driver", header: "Řidič", width: "16%", render: (trip) => trip.driver_name, }, { key: "vehicle", header: "Vozidlo", width: "11%", mono: true, render: (trip) => , }, { key: "route", header: "Trasa", width: "20%", render: (trip) => ( {trip.route_from} → {trip.route_to} ), }, { key: "km", header: "Stav km", width: "14%", mono: true, render: (trip) => ( {formatKm(trip.start_km)} - {formatKm(trip.end_km)} ), }, { key: "distance", header: "Vzdálenost", width: "12%", mono: true, bold: true, render: (trip) => `${formatKm(trip.distance)} km`, }, { key: "type", header: "Typ", width: "10%", render: (trip) => ( ), }, { key: "actions", header: "Akce", width: "10%", align: "right", render: (trip) => ( openEditModal(trip)} aria-label="Upravit" title="Upravit" > {EditIcon} setDeleteConfirm({ show: true, trip })} aria-label="Smazat" title="Smazat" > {DeleteIcon} ), }, ]; return ( Správa knihy jízd {trips.length > 0 && ( )} {/* Filters */} ({ value: String(u.id), label: u.name, })), ]} /> {/* Stats Cards */} {/* Trips Table */} {isPending ? ( ) : ( columns={columns} rows={trips} rowKey={(trip) => trip.id} empty={ } /> )} {/* Edit Modal */} setShowEditModal(false)} onSubmit={handleEditSubmit} title="Upravit jízdu" subtitle={editingTrip?.driver_name} loading={editMutation.isPending} maxWidth="lg" > {editingTrip && ( <> setEditForm({ ...editForm, is_business: parseInt(value) }) } options={[ { value: "1", label: "Služební" }, { value: "0", label: "Soukromá" }, ]} /> setEditForm({ ...editForm, notes: e.target.value }) } /> )} {/* Delete Confirmation */} setDeleteConfirm({ show: false, trip: null })} onConfirm={handleDelete} title="Smazat záznam" message={ deleteConfirm.trip ? `Opravdu chcete smazat záznam jízdy z ${formatDate(deleteConfirm.trip.trip_date)}?` : "" } confirmText="Smazat" confirmVariant="danger" loading={deleteMutation.isPending} /> {/* Hidden Print Content */} {trips.length > 0 && (

KNIHA JÍZD

{companyName}
{getPeriodName()}
{getSelectedVehicleName() && (
Vozidlo: {getSelectedVehicleName()}
)} {getSelectedUserName() && (
Řidič: {getSelectedUserName()}
)}
Vygenerováno: {new Date().toLocaleString("cs-CZ")}
{totals.count}
Počet jízd
{formatKm(totals.total)} km
Celkem
{formatKm(totals.business)} km
Služební
{formatKm(totals.total - totals.business)} km
Soukromé
{trips.map((trip) => ( ))}
Datum Řidič Vozidlo Trasa Stav km Vzdálenost Typ Poznámka
{formatDate(trip.trip_date)} {trip.driver_name} {trip.spz} {trip.route_from} → {trip.route_to} {formatKm(trip.start_km)} - {formatKm(trip.end_km)} {formatKm(trip.distance)} km {trip.is_business ? "Služební" : "Soukromá"} {trip.notes || ""}
Celkem: {formatKm(totals.total)} km
)}
); }