feat(mui): migrate Dashboard (Přehled) onto MUI kit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 01:19:49 +02:00
parent 5ca03bd662
commit 5459d8c325
7 changed files with 1205 additions and 968 deletions

View File

@@ -1,12 +1,12 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import { Link as RouterLink } from "react-router-dom";
import { motion } from "framer-motion";
import Box from "@mui/material/Box";
import { useAuth } from "../../context/AuthContext";
import { useAlert } from "../../context/AlertContext";
import { formatKm, todayLocalStr } from "../../utils/formatters";
import AdminDatePicker from "../AdminDatePicker";
import { Button, Modal, Field, Select, DateField, TextField } from "../../ui";
import apiFetch from "../../utils/api";
import FormModal from "../FormModal";
const API_BASE = "/api/admin";
@@ -46,6 +46,14 @@ interface DashQuickActionsProps {
onPunch: () => void;
}
// Maps the legacy quick-action color tokens to the MUI Button palette color.
const ACTION_COLOR: Record<string, "success" | "error" | "info" | "warning"> = {
success: "success",
danger: "error",
info: "info",
warning: "warning",
};
export default function DashQuickActions({
dashData,
punching,
@@ -284,252 +292,192 @@ export default function DashQuickActions({
return (
<>
<motion.div
className="dash-quick-actions"
<Box
component={motion.div}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.08 }}
sx={{
display: "grid",
gridTemplateColumns: {
xs: "1fr",
sm: "repeat(2, 1fr)",
md: "repeat(4, 1fr)",
},
gap: 2,
mb: 3,
}}
>
{quickActions.map((action) =>
action.onClick ? (
<button
{quickActions.map((action) => {
const color = ACTION_COLOR[action.color] ?? "primary";
const common = {
variant: "contained" as const,
color,
startIcon: action.icon,
sx: { py: 1.5, justifyContent: "flex-start" },
};
return action.onClick ? (
<Button
key={action.label}
onClick={action.onClick}
disabled={action.disabled}
className={`dash-quick-btn dash-quick-btn-${action.color}`}
{...common}
>
{action.icon}
<span>{action.label}</span>
</button>
{action.label}
</Button>
) : (
<Link
<Button
key={action.label}
component={RouterLink}
to={action.path!}
className={`dash-quick-btn dash-quick-btn-${action.color}`}
{...common}
>
{action.icon}
<span>{action.label}</span>
</Link>
),
)}
</motion.div>
{action.label}
</Button>
);
})}
</Box>
<FormModal
<Modal
isOpen={showTripModal}
onClose={() => setShowTripModal(false)}
title="Přidat jízdu"
size="lg"
maxWidth="md"
onSubmit={handleTripSubmit}
submitLabel="Uložit"
submitText="Uložit"
loading={tripSubmitting}
>
<div className="admin-form">
<div className="admin-form-row">
<div
className={`admin-form-group${tripErrors.vehicle_id ? " has-error" : ""}`}
>
<label className="admin-form-label required">Vozidlo</label>
<select
value={tripForm.vehicle_id}
onChange={(e) => {
handleTripVehicleChange(e.target.value);
setTripErrors((prev) => ({
...prev,
vehicle_id: undefined,
}));
}}
className="admin-form-select"
>
<option value="">Vyberte vozidlo</option>
{tripVehicles.map((v) => (
<option key={v.id} value={v.id}>
{v.spz} - {v.name}
</option>
))}
</select>
{tripErrors.vehicle_id && (
<span className="admin-form-error">
{tripErrors.vehicle_id}
</span>
)}
</div>
<div
className={`admin-form-group${tripErrors.trip_date ? " has-error" : ""}`}
>
<label className="admin-form-label required">Datum jízdy</label>
<AdminDatePicker
mode="date"
value={tripForm.trip_date}
onChange={(val: string) => {
setTripForm((prev) => ({ ...prev, trip_date: val }));
setTripErrors((prev) => ({
...prev,
trip_date: undefined,
}));
}}
/>
{tripErrors.trip_date && (
<span className="admin-form-error">{tripErrors.trip_date}</span>
)}
</div>
</div>
<div className="admin-form-row admin-form-row-3">
<div
className={`admin-form-group${tripErrors.start_km ? " has-error" : ""}`}
>
<label className="admin-form-label required">
Počáteční stav km
</label>
<input
type="number"
inputMode="numeric"
value={tripForm.start_km}
onChange={(e) => {
setTripForm((prev) => ({
...prev,
start_km: e.target.value,
}));
setTripErrors((prev) => ({
...prev,
start_km: undefined,
}));
}}
className="admin-form-input"
min="0"
/>
{tripErrors.start_km && (
<span className="admin-form-error">{tripErrors.start_km}</span>
)}
</div>
<div
className={`admin-form-group${tripErrors.end_km ? " has-error" : ""}`}
>
<label className="admin-form-label required">
Konečný stav km
</label>
<input
type="number"
inputMode="numeric"
value={tripForm.end_km}
onChange={(e) => {
setTripForm((prev) => ({
...prev,
end_km: e.target.value,
}));
setTripErrors((prev) => ({
...prev,
end_km: undefined,
}));
}}
className="admin-form-input"
min="0"
/>
{tripErrors.end_km && (
<span className="admin-form-error">{tripErrors.end_km}</span>
)}
</div>
<div className="admin-form-group">
<label className="admin-form-label">Vzdálenost</label>
<input
type="text"
value={`${formatKm(tripDistance())} km`}
className="admin-form-input"
readOnly
disabled
/>
</div>
</div>
<div className="admin-form-row">
<div
className={`admin-form-group${tripErrors.route_from ? " has-error" : ""}`}
>
<label className="admin-form-label required">Místo odjezdu</label>
<input
type="text"
value={tripForm.route_from}
onChange={(e) => {
setTripForm((prev) => ({
...prev,
route_from: e.target.value,
}));
setTripErrors((prev) => ({
...prev,
route_from: undefined,
}));
}}
className="admin-form-input"
placeholder="Např. Praha"
/>
{tripErrors.route_from && (
<span className="admin-form-error">
{tripErrors.route_from}
</span>
)}
</div>
<div
className={`admin-form-group${tripErrors.route_to ? " has-error" : ""}`}
>
<label className="admin-form-label required">
Místo příjezdu
</label>
<input
type="text"
value={tripForm.route_to}
onChange={(e) => {
setTripForm((prev) => ({
...prev,
route_to: e.target.value,
}));
setTripErrors((prev) => ({
...prev,
route_to: undefined,
}));
}}
className="admin-form-input"
placeholder="Např. Brno"
/>
{tripErrors.route_to && (
<span className="admin-form-error">{tripErrors.route_to}</span>
)}
</div>
</div>
<div className="admin-form-group">
<label className="admin-form-label">Typ jízdy</label>
<select
value={tripForm.is_business}
onChange={(e) =>
setTripForm((prev) => ({
...prev,
is_business: parseInt(e.target.value),
}))
}
className="admin-form-select"
>
<option value={1}>Služební</option>
<option value={0}>Soukromá</option>
</select>
</div>
<div className="admin-form-group">
<label className="admin-form-label">Poznámky</label>
<textarea
value={tripForm.notes}
onChange={(e) =>
setTripForm((prev) => ({
...prev,
notes: e.target.value,
}))
}
className="admin-form-textarea"
rows={2}
placeholder="Volitelné poznámky..."
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Vozidlo" required error={tripErrors.vehicle_id}>
<Select
value={tripForm.vehicle_id}
onChange={(val) => {
handleTripVehicleChange(val);
setTripErrors((prev) => ({ ...prev, vehicle_id: undefined }));
}}
options={[
{ value: "", label: "Vyberte vozidlo" },
...tripVehicles.map((v) => ({
value: String(v.id),
label: `${v.spz} - ${v.name}`,
})),
]}
/>
</div>
</div>
</FormModal>
</Field>
<Field label="Datum jízdy" required error={tripErrors.trip_date}>
<DateField
value={tripForm.trip_date}
onChange={(val) => {
setTripForm((prev) => ({ ...prev, trip_date: val }));
setTripErrors((prev) => ({ ...prev, trip_date: undefined }));
}}
/>
</Field>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "repeat(3, 1fr)" },
gap: 2,
}}
>
<Field label="Počáteční stav km" required error={tripErrors.start_km}>
<TextField
type="number"
inputMode="numeric"
value={tripForm.start_km}
onChange={(e) => {
setTripForm((prev) => ({ ...prev, start_km: e.target.value }));
setTripErrors((prev) => ({ ...prev, start_km: undefined }));
}}
slotProps={{ htmlInput: { min: 0 } }}
/>
</Field>
<Field label="Konečný stav km" required error={tripErrors.end_km}>
<TextField
type="number"
inputMode="numeric"
value={tripForm.end_km}
onChange={(e) => {
setTripForm((prev) => ({ ...prev, end_km: e.target.value }));
setTripErrors((prev) => ({ ...prev, end_km: undefined }));
}}
slotProps={{ htmlInput: { min: 0 } }}
/>
</Field>
<Field label="Vzdálenost">
<TextField
value={`${formatKm(tripDistance())} km`}
InputProps={{ readOnly: true }}
disabled
/>
</Field>
</Box>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
}}
>
<Field label="Místo odjezdu" required error={tripErrors.route_from}>
<TextField
value={tripForm.route_from}
onChange={(e) => {
setTripForm((prev) => ({
...prev,
route_from: e.target.value,
}));
setTripErrors((prev) => ({ ...prev, route_from: undefined }));
}}
placeholder="Např. Praha"
/>
</Field>
<Field label="Místo příjezdu" required error={tripErrors.route_to}>
<TextField
value={tripForm.route_to}
onChange={(e) => {
setTripForm((prev) => ({ ...prev, route_to: e.target.value }));
setTripErrors((prev) => ({ ...prev, route_to: undefined }));
}}
placeholder="Např. Brno"
/>
</Field>
</Box>
<Field label="Typ jízdy">
<Select
value={String(tripForm.is_business)}
onChange={(val) =>
setTripForm((prev) => ({ ...prev, is_business: parseInt(val) }))
}
options={[
{ value: "1", label: "Služební" },
{ value: "0", label: "Soukromá" },
]}
/>
</Field>
<Field label="Poznámky">
<TextField
multiline
minRows={2}
value={tripForm.notes}
onChange={(e) =>
setTripForm((prev) => ({ ...prev, notes: e.target.value }))
}
placeholder="Volitelné poznámky..."
/>
</Field>
</Modal>
</>
);
}