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