import { useState, useEffect, useCallback, useRef } from "react"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { Link } from "react-router-dom"; import Forbidden from "../components/Forbidden"; import { motion, AnimatePresence } from "framer-motion"; import ConfirmModal from "../components/ConfirmModal"; import AdminDatePicker from "../components/AdminDatePicker"; import FormField from "../components/FormField"; import useModalLock from "../hooks/useModalLock"; import { formatDate } from "../utils/attendanceHelpers"; import { formatKm } from "../utils/formatters"; import apiFetch from "../utils/api"; const API_BASE = "/api/admin"; interface Vehicle { id: number | string; spz: string; name: string; } interface UserShort { id: number | string; name: string; } 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 BackendTrip { id: number; vehicle_id: number; user_id: number; trip_date: string; start_km: number; end_km: number; distance: number | null; route_from: string; route_to: string; is_business: boolean; notes: string | null; users: { id: number; first_name: string; last_name: string }; vehicles: { id: number; name: string; spz: 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}` : "", }; } export default function TripsAdmin() { const alert = useAlert(); const { hasPermission } = useAuth(); const [loading, setLoading] = useState(true); const [companyName, setCompanyName] = useState(""); const [filterMonth, setFilterMonth] = useState(() => String(new Date().getMonth() + 1), ); const [filterYear, setFilterYear] = useState(() => String(new Date().getFullYear()), ); const [filterVehicleId, setFilterVehicleId] = useState(""); const [filterUserId, setFilterUserId] = useState(""); const [trips, setTrips] = useState([]); const [vehicles, setVehicles] = useState([]); const [users, setUsers] = 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 }); // Fetch vehicles and users once on mount useEffect(() => { const fetchLookups = async () => { try { const [vRes, uRes, csRes] = await Promise.all([ apiFetch(`${API_BASE}/vehicles`), apiFetch(`${API_BASE}/trips/users`), apiFetch(`${API_BASE}/company-settings`), ]); const vJson = await vRes.json(); const uJson = await uRes.json(); const csJson = await csRes.json(); if (vJson.success) setVehicles(vJson.data); if (csJson.success) setCompanyName(csJson.data.company_name || ""); if (uJson.success) { setUsers(uJson.data); } } catch { // silently fail, filters will just be empty } }; fetchLookups(); }, []); const fetchData = useCallback( async (showLoading = true) => { if (showLoading) setLoading(true); try { let url = `${API_BASE}/trips?limit=1000&month=${filterMonth}&year=${filterYear}`; if (filterVehicleId) url += `&vehicle_id=${filterVehicleId}`; if (filterUserId) url += `&user_id=${filterUserId}`; const response = await apiFetch(url); const result = await response.json(); if (result.success) { const mapped = (result.data as BackendTrip[]).map(mapTrip); setTrips(mapped); } } catch { alert.error("Nepodařilo se načíst data"); } finally { if (showLoading) setLoading(false); } }, [filterMonth, filterYear, filterVehicleId, filterUserId, alert], ); useEffect(() => { fetchData(); }, [fetchData]); useModalLock(showEditModal); if (!hasPermission("trips.admin")) 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 response = await apiFetch(`${API_BASE}/trips/${editingTrip.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(editForm), }); const result = await response.json(); if (result.success) { setShowEditModal(false); await fetchData(false); await new Promise((resolve) => setTimeout(resolve, 300)); alert.success(result.message); } else { alert.error(result.error); } } catch { alert.error("Chyba připojení"); } }; const handleDelete = async () => { if (!deleteConfirm.trip) return; try { const response = await apiFetch( `${API_BASE}/trips/${deleteConfirm.trip.id}`, { method: "DELETE", }, ); const result = await response.json(); if (result.success) { setDeleteConfirm({ show: false, trip: null }); await fetchData(false); alert.success(result.message); } else { alert.error(result.error); } } catch { alert.error("Chyba připojení"); } }; const getPeriodName = () => new Date(Number(filterYear), Number(filterMonth) - 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 = users.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), }; return (

Správa knihy jízd

{trips.length > 0 && ( )} Vozidla
{/* Filters */}
{totals.count} Počet jízd
{formatKm(totals.total)} km Celkem naježděno
{formatKm(totals.business)} km Služební km
{/* Trips Table */}
{loading && (
{[0, 1, 2, 3, 4].map((i) => (
))}
)} {!loading && trips.length === 0 && (

Žádné záznamy jízd pro vybrané období.

)} {!loading && trips.length > 0 && (
{trips.map((trip) => ( ))}
Datum Řidič Vozidlo Trasa Stav km Vzdálenost Typ Akce
{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á"}
)}
{/* Edit Modal */} {showEditModal && editingTrip && (
setShowEditModal(false)} />

Upravit jízdu

{editingTrip.driver_name}

setEditForm({ ...editForm, trip_date: val }) } />
setEditForm({ ...editForm, start_km: e.target.value }) } className="admin-form-input" min="0" /> setEditForm({ ...editForm, end_km: e.target.value }) } className="admin-form-input" min="0" />
setEditForm({ ...editForm, route_from: e.target.value, }) } className="admin-form-input" /> setEditForm({ ...editForm, route_to: e.target.value }) } className="admin-form-input" />