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:
@@ -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"
|
||||||
← 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user