feat(mui): migrate Trips History (Moje historie) onto MUI kit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 23:47:59 +02:00
parent 1d0793d2bb
commit a05f0f1665

View File

@@ -1,19 +1,26 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import AdminDatePicker from "../components/AdminDatePicker"; import Box from "@mui/material/Box";
import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden"; import Forbidden from "../components/Forbidden";
import { formatDate } from "../utils/attendanceHelpers"; import { formatDate } from "../utils/attendanceHelpers";
import { formatKm } from "../utils/formatters"; import { formatKm } from "../utils/formatters";
import FormField from "../components/FormField"; import { tripHistoryOptions, tripVehiclesOptions } from "../lib/queries/trips";
import { import {
tripHistoryOptions, Card,
tripVehiclesOptions, DataTable,
type BackendTrip, Field,
type TripVehicle, Select,
} from "../lib/queries/trips"; MonthField,
StatusChip,
PageHeader,
FilterBar,
EmptyState,
LoadingState,
StatCard,
type DataColumn,
} from "../ui";
interface Trip { interface Trip {
id: number; id: number;
@@ -29,8 +36,56 @@ interface Trip {
notes?: string; notes?: string;
} }
const TripsIcon = (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="12" y1="20" x2="12" y2="10" />
<line x1="18" y1="20" x2="18" y2="4" />
<line x1="6" y1="20" x2="6" y2="16" />
</svg>
);
const TotalIcon = (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
);
const BusinessIcon = (
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="1" y="3" width="15" height="13" rx="2" ry="2" />
<path d="M16 8h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-1" />
<circle cx="5.5" cy="18" r="2" />
<circle cx="18.5" cy="18" r="2" />
<path d="M8 18h8" />
</svg>
);
export default function TripsHistory() { export default function TripsHistory() {
const alert = useAlert();
const { user, hasPermission } = useAuth(); const { user, hasPermission } = useAuth();
const [month, setMonth] = useState(() => { const [month, setMonth] = useState(() => {
const now = new Date(); const now = new Date();
@@ -48,7 +103,7 @@ export default function TripsHistory() {
userId: user?.id, userId: user?.id,
}), }),
); );
const trips = (tripsData ?? []).map((t) => ({ const trips: Trip[] = (tripsData ?? []).map((t) => ({
id: t.id, id: t.id,
trip_date: t.trip_date, trip_date: t.trip_date,
spz: t.vehicles?.spz ?? "", spz: t.vehicles?.spz ?? "",
@@ -81,222 +136,184 @@ export default function TripsHistory() {
return date.toLocaleDateString("cs-CZ", { month: "long", year: "numeric" }); return date.toLocaleDateString("cs-CZ", { month: "long", year: "numeric" });
}; };
const columns: DataColumn<Trip>[] = [
{
key: "trip_date",
header: "Datum",
width: "11%",
mono: true,
render: (trip) => formatDate(trip.trip_date),
},
{
key: "vehicle",
header: "Vozidlo",
width: "10%",
render: (trip) => <StatusChip label={trip.spz} color="default" />,
},
{
key: "driver",
header: "Řidič",
width: "14%",
render: (trip) => (
<Box component="span" sx={{ color: "text.secondary" }}>
{trip.driver_name}
</Box>
),
},
{
key: "route",
header: "Trasa",
width: "18%",
render: (trip) => (
<Box component="span" sx={{ whiteSpace: "nowrap" }}>
{trip.route_from} &rarr; {trip.route_to}
</Box>
),
},
{
key: "km",
header: "Stav km",
width: "13%",
mono: true,
render: (trip) => (
<Box
component="span"
sx={{ whiteSpace: "nowrap", color: "text.secondary" }}
>
{formatKm(trip.start_km)} - {formatKm(trip.end_km)}
</Box>
),
},
{
key: "distance",
header: "Vzdálenost",
width: "10%",
mono: true,
bold: true,
render: (trip) => `${formatKm(trip.distance)} km`,
},
{
key: "type",
header: "Typ",
width: "10%",
render: (trip) => (
<StatusChip
label={trip.is_business ? "Služební" : "Soukromá"}
color={trip.is_business ? "success" : "warning"}
/>
),
},
{
key: "notes",
header: "Poznámka",
width: "14%",
render: (trip) => (
<Box component="span" sx={{ color: "text.secondary" }}>
{trip.notes || "—"}
</Box>
),
},
];
return ( return (
<div> <Box>
<motion.div <motion.div
className="admin-page-header"
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }} transition={{ duration: 0.25 }}
> >
<div> <PageHeader title="Historie jízd" subtitle={getMonthName(month)} />
<h1 className="admin-page-title">Historie jízd</h1>
<p className="admin-page-subtitle">{getMonthName(month)}</p>
</div>
</motion.div> </motion.div>
{/* Filters */} {/* Filters */}
<motion.div <motion.div
className="admin-card"
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }} transition={{ duration: 0.25, delay: 0.06 }}
> >
<div className="admin-card-body"> <FilterBar>
<div className="admin-form-row"> <Box sx={{ flex: "1 1 200px" }}>
<FormField label="Měsíc"> <Field label="Měsíc">
<AdminDatePicker <MonthField value={month} onChange={(val) => setMonth(val)} />
mode="month" </Field>
value={month} </Box>
onChange={(val: string) => setMonth(val)} <Box sx={{ flex: "1 1 200px" }}>
/> <Field label="Vozidlo">
</FormField> <Select
<FormField label="Vozidlo">
<select
value={vehicleId} value={vehicleId}
onChange={(e) => setVehicleId(e.target.value)} onChange={(value) => setVehicleId(value)}
className="admin-form-select" options={[
> { value: "", label: "Všechna vozidla" },
<option value="">Všechna vozidla</option> ...vehicles.map((v) => ({
{vehicles.map((v) => ( value: String(v.id),
<option key={v.id} value={v.id}> label: `${v.spz} - ${v.name}`,
{v.spz} - {v.name} })),
</option> ]}
))} />
</select> </Field>
</FormField> </Box>
</div> </FilterBar>
</div>
</motion.div> </motion.div>
<motion.div <motion.div
className="admin-grid admin-grid-3 mt-6"
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.08 }} transition={{ duration: 0.25, delay: 0.08 }}
> >
<div className="admin-stat-card info"> <Box
<div className="admin-stat-icon info"> sx={{
<svg display: "grid",
width="22" gridTemplateColumns: {
height="22" xs: "1fr",
viewBox="0 0 24 24" sm: "1fr 1fr",
fill="none" lg: "repeat(3, 1fr)",
stroke="currentColor" },
strokeWidth="2" gap: 2,
strokeLinecap="round" mt: 3,
strokeLinejoin="round" }}
> >
<line x1="12" y1="20" x2="12" y2="10" /> <StatCard
<line x1="18" y1="20" x2="18" y2="4" /> label="Počet jízd"
<line x1="6" y1="20" x2="6" y2="16" /> value={totals.count}
</svg> icon={TripsIcon}
</div> color="info"
<div className="admin-stat-content"> />
<span className="admin-stat-value">{totals.count}</span> <StatCard
<span className="admin-stat-label">Počet jízd</span> label="Celkem naježděno"
</div> value={`${formatKm(totals.total)} km`}
</div> icon={TotalIcon}
<div className="admin-stat-card"> color="default"
<div className="admin-stat-icon"> />
<svg <StatCard
width="22" label="Služební km"
height="22" value={`${formatKm(totals.business)} km`}
viewBox="0 0 24 24" icon={BusinessIcon}
fill="none" color="success"
stroke="currentColor" />
strokeWidth="2" </Box>
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</div>
<div className="admin-stat-content">
<span className="admin-stat-value">
{formatKm(totals.total)} km
</span>
<span className="admin-stat-label">Celkem naježděno</span>
</div>
</div>
<div className="admin-stat-card success">
<div className="admin-stat-icon success">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="1" y="3" width="15" height="13" rx="2" ry="2" />
<path d="M16 8h2a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-1" />
<circle cx="5.5" cy="18" r="2" />
<circle cx="18.5" cy="18" r="2" />
<path d="M8 18h8" />
</svg>
</div>
<div className="admin-stat-content">
<span className="admin-stat-value">
{formatKm(totals.business)} km
</span>
<span className="admin-stat-label">Služební km</span>
</div>
</div>
</motion.div> </motion.div>
{/* Trips Table */} {/* Trips Table */}
<motion.div <motion.div
className="admin-card mt-6"
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.12 }} transition={{ duration: 0.25, delay: 0.12 }}
> >
<div className="admin-card-body"> <Card sx={{ mt: 3 }}>
{isPending ? ( {isPending ? (
<div className="admin-loading"> <LoadingState />
<div className="admin-spinner" />
</div>
) : ( ) : (
<> <DataTable<Trip>
{trips.length === 0 && ( columns={columns}
<div className="admin-empty-state"> rows={trips}
<p>Žádné záznamy jízd pro vybrané období.</p> rowKey={(trip) => trip.id}
</div> empty={
)} <EmptyState title="Žádné záznamy jízd pro vybrané období." />
{trips.length > 0 && ( }
<div className="admin-table-responsive"> />
<table className="admin-table">
<thead>
<tr>
<th>Datum</th>
<th>Vozidlo</th>
<th>Řidič</th>
<th>Trasa</th>
<th>Stav km</th>
<th>Vzdálenost</th>
<th>Typ</th>
<th>Poznámka</th>
</tr>
</thead>
<tbody>
{trips.map((trip) => (
<tr key={trip.id}>
<td className="admin-mono">
{formatDate(trip.trip_date)}
</td>
<td>
<span className="admin-badge">{trip.spz}</span>
</td>
<td style={{ color: "var(--text-secondary)" }}>
{trip.driver_name}
</td>
<td>
<span style={{ whiteSpace: "nowrap" }}>
{trip.route_from} &rarr; {trip.route_to}
</span>
</td>
<td className="admin-mono">
<span
style={{
whiteSpace: "nowrap",
color: "var(--text-secondary)",
}}
>
{formatKm(trip.start_km)} -{" "}
{formatKm(trip.end_km)}
</span>
</td>
<td className="admin-mono">
<strong>{formatKm(trip.distance)} km</strong>
</td>
<td>
<span
className={`admin-badge ${trip.is_business ? "admin-badge-success" : "admin-badge-warning"}`}
>
{trip.is_business ? "Služební" : "Soukromá"}
</span>
</td>
<td
style={{
color: "var(--text-secondary)",
maxWidth: "200px",
}}
>
{trip.notes || "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
)} )}
</div> </Card>
</motion.div> </motion.div>
</div> </Box>
); );
} }