feat(mui): migrate Attendance Location chrome onto MUI kit (Leaflet map kept)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 08:29:46 +02:00
parent 5630beee7c
commit 9b43d4e827

View File

@@ -3,8 +3,10 @@ import { useQuery } from "@tanstack/react-query";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden";
import { useNavigate, useParams, Link } from "react-router-dom";
import { useNavigate, useParams, Link as RouterLink } from "react-router-dom";
import { motion } from "framer-motion";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
@@ -14,6 +16,20 @@ import {
attendanceLocationOptions,
type LocationRecord,
} from "../lib/queries/attendance";
import { Button, Card, LoadingState, PageHeader } from "../ui";
const BackIcon = (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
);
export default function AttendanceLocation() {
const alert = useAlert();
@@ -167,127 +183,178 @@ export default function AttendanceLocation() {
const month = shiftDateStr.substring(0, 7);
if (isPending) {
return (
<div className="admin-loading">
<div className="admin-spinner" />
</div>
);
return <LoadingState />;
}
return (
<div>
<Box>
{/* Header */}
<motion.div
className="admin-page-header"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
>
<div>
<h1 className="admin-page-title">Poloha záznamu</h1>
</div>
<div className="admin-page-actions">
<Link
to={`/attendance/admin?month=${month}`}
className="admin-btn admin-btn-secondary"
>
&larr; Zpět na správu
</Link>
</div>
<PageHeader
title="Poloha záznamu"
actions={
<Button
component={RouterLink}
to={`/attendance/admin?month=${month}`}
variant="outlined"
color="inherit"
startIcon={BackIcon}
>
Zpět na správu
</Button>
}
/>
</motion.div>
{/* Info + map card */}
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
>
<div className="admin-card-header">
<h2 className="admin-card-title">
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
{record.user_name} {formatDate(record.shift_date)}
</h2>
</div>
<div className="admin-card-body">
</Typography>
{/* Leaflet map — kept entirely as-is */}
{hasAnyLocation && (
<div ref={mapRef} className="attendance-location-map" />
)}
<div className="attendance-location-grid">
{/* Location detail grid */}
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" },
gap: 2,
mt: hasAnyLocation ? 2 : 0,
}}
>
{/* Arrival */}
<div
className={`attendance-location-card ${!hasArrivalLocation ? "empty" : ""}`}
<Box
sx={{
p: 2,
borderRadius: 1,
border: "1px solid",
borderColor: "divider",
opacity: hasArrivalLocation ? 1 : 0.6,
}}
>
<h3 className="attendance-location-title">Příchod</h3>
<div className="attendance-location-time">
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
Příchod
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace", mb: 1 }}
>
{record.arrival_time
? formatDatetimeLocal(record.arrival_time)
: "—"}
</div>
</Typography>
{hasArrivalLocation ? (
<>
<div className="attendance-location-address">
<Typography variant="body2" sx={{ mb: 0.5 }}>
{record.arrival_address || <em>Adresa nezjištěna</em>}
</div>
<div className="attendance-location-coords">
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontSize: "0.75rem",
color: "text.secondary",
mb: 1,
}}
>
GPS: {record.arrival_lat}, {record.arrival_lng}
{record.arrival_accuracy &&
` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
</div>
<a
</Typography>
<Button
component="a"
href={`https://www.google.com/maps?q=${record.arrival_lat},${record.arrival_lng}`}
target="_blank"
rel="noopener noreferrer"
className="admin-btn admin-btn-secondary admin-btn-sm mt-2"
variant="outlined"
color="inherit"
size="small"
>
Otevřít v Google Maps
</a>
</Button>
</>
) : (
<div className="attendance-location-address">
<Typography variant="body2" color="text.secondary">
<em>Poloha nebyla zaznamenána</em>
</div>
</Typography>
)}
</div>
</Box>
{/* Departure */}
{(hasDepartureLocation || record.departure_time) && (
<div
className={`attendance-location-card ${!hasDepartureLocation ? "empty" : ""}`}
<Box
sx={{
p: 2,
borderRadius: 1,
border: "1px solid",
borderColor: "divider",
opacity: hasDepartureLocation ? 1 : 0.6,
}}
>
<h3 className="attendance-location-title">Odchod</h3>
<div className="attendance-location-time">
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
Odchod
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace", mb: 1 }}
>
{record.departure_time
? formatDatetimeLocal(record.departure_time)
: "—"}
</div>
</Typography>
{hasDepartureLocation ? (
<>
<div className="attendance-location-address">
<Typography variant="body2" sx={{ mb: 0.5 }}>
{record.departure_address || <em>Adresa nezjištěna</em>}
</div>
<div className="attendance-location-coords">
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontSize: "0.75rem",
color: "text.secondary",
mb: 1,
}}
>
GPS: {record.departure_lat}, {record.departure_lng}
{record.departure_accuracy &&
` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
</div>
<a
</Typography>
<Button
component="a"
href={`https://www.google.com/maps?q=${record.departure_lat},${record.departure_lng}`}
target="_blank"
rel="noopener noreferrer"
className="admin-btn admin-btn-secondary admin-btn-sm mt-2"
variant="outlined"
color="inherit"
size="small"
>
Otevřít v Google Maps
</a>
</Button>
</>
) : (
<div className="attendance-location-address">
<Typography variant="body2" color="text.secondary">
<em>Poloha nebyla zaznamenána</em>
</div>
</Typography>
)}
</div>
</Box>
)}
</div>
</div>
</Box>
</Card>
</motion.div>
</div>
</Box>
);
}