import { useState, useEffect, useCallback } from "react"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { Link } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; import AdminDatePicker from "../components/AdminDatePicker"; import ConfirmModal from "../components/ConfirmModal"; import FormField from "../components/FormField"; import useModalLock from "../hooks/useModalLock"; import Forbidden from "../components/Forbidden"; 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 Trip { id: number; vehicle_id: number | string; 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 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; } export default function Trips() { const alert = useAlert(); const { hasPermission } = useAuth(); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [trips, setTrips] = useState([]); const [vehicles, setVehicles] = useState([]); 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: new Date().toISOString().split("T")[0], start_km: "", end_km: "", route_from: "", route_to: "", is_business: 1, notes: "", }); const [errors, setErrors] = useState>({}); const [, setLastKm] = useState(0); const fetchData = useCallback( async (showLoading = true) => { if (showLoading) setLoading(true); try { const [tripsRes, vehiclesRes] = await Promise.all([ apiFetch(`${API_BASE}/trips`), apiFetch(`${API_BASE}/vehicles`), ]); const tripsResult = await tripsRes.json(); const vehiclesResult = await vehiclesRes.json(); if (tripsResult.success) { setTrips(Array.isArray(tripsResult.data) ? tripsResult.data : []); } if (vehiclesResult.success) { setVehicles( Array.isArray(vehiclesResult.data) ? vehiclesResult.data : [], ); } } catch { alert.error("Nepodařilo se načíst data"); } finally { if (showLoading) setLoading(false); } }, [alert], ); useEffect(() => { fetchData(); }, [fetchData]); useModalLock(showModal); if (!hasPermission("trips.record")) return ; 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); const today = new Date().toISOString().split("T")[0]; setForm({ vehicle_id: "", trip_date: today, start_km: "", end_km: "", route_from: "", route_to: "", is_business: 1, notes: "", }); setLastKm(0); setErrors({}); setShowModal(true); }; const openEditModal = (trip: Trip) => { 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; setSubmitting(true); try { const url = editingTrip ? `${API_BASE}/trips/${editingTrip.id}` : `${API_BASE}/trips`; const response = await apiFetch(url, { method: editingTrip ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form), }); const result = await response.json(); if (result.success) { setShowModal(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í"); } finally { setSubmitting(false); } }; const handleDelete = async (tripId: number) => { try { const response = await apiFetch(`${API_BASE}/trips/${tripId}`, { method: "DELETE", }); const result = await response.json(); if (result.success) { await fetchData(false); alert.success(result.message); } else { alert.error(result.error); } } catch { alert.error("Chyba připojení"); } finally { setDeleteConfirm({ show: false, tripId: null }); } }; 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 (loading) { return (
{[0, 1, 2, 3].map((i) => (
))}
{[0, 1, 2, 3, 4].map((i) => (
))}
); } 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 }, ); return (

Kniha jízd

{new Date().toLocaleDateString("cs-CZ", { month: "long", year: "numeric", })}

{/* Stats Cards */}
{totals.count} Počet jízd
{formatKm(totals.total)} km Celkem naježděno
{formatKm(totals.business)} km Služební
{formatKm(totals.private)} km Soukromé
{/* Recent Trips */}

Poslední jízdy

Zobrazit historii
{trips.length === 0 ? (

Zatím nemáte žádné záznamy jízd.

) : (
{trips.slice(0, 10).map((trip) => ( ))}
Datum Vozidlo Řidič Trasa Vzdálenost Typ Akce
{formatDate(trip.trip_date)} {trip.vehicles?.spz ?? ""} {trip.users ? `${trip.users.first_name} ${trip.users.last_name}` : ""} {trip.route_from} → {trip.route_to} {formatKm( trip.distance ?? trip.end_km - trip.start_km, )}{" "} km {trip.is_business ? "Služební" : "Soukromá"}
)}
{/* Add/Edit Modal */} {showModal && (
setShowModal(false)} />

{editingTrip ? "Upravit jízdu" : "Přidat jízdu"}

{ setForm({ ...form, trip_date: val }); setErrors((prev) => ({ ...prev, trip_date: "" })); }} />
{ setForm({ ...form, start_km: e.target.value }); setErrors((prev) => ({ ...prev, start_km: "" })); }} className="admin-form-input" min="0" /> { setForm({ ...form, end_km: e.target.value }); setErrors((prev) => ({ ...prev, end_km: "" })); }} className="admin-form-input" min="0" />
{ setForm({ ...form, route_from: e.target.value }); setErrors((prev) => ({ ...prev, route_from: "" })); }} className="admin-form-input" placeholder="Např. Praha" /> { setForm({ ...form, route_to: e.target.value }); setErrors((prev) => ({ ...prev, route_to: "" })); }} className="admin-form-input" placeholder="Např. Brno" />