import { useEffect, useRef } from "react"; 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 as RouterLink } from "react-router-dom"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { styled } from "@mui/material/styles"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; // Leaflet map container (was the .attendance-location-map rule). zIndex:0 // creates a stacking context so Leaflet's internal z-indexes stay contained. const LocationMap = styled("div")(({ theme }) => ({ height: "400px", borderRadius: "8px", marginBottom: "1.5rem", background: theme.vars!.palette.background.paper, position: "relative", zIndex: 0, "@media (max-width:640px)": { height: "clamp(220px, 45vh, 400px)", }, })); import { formatDate, formatTime } from "../utils/attendanceHelpers"; import { attendanceLocationOptions, type LocationRecord, } from "../lib/queries/attendance"; import { Button, Card, LoadingState, PageEnter, PageHeader } from "../ui"; const BackIcon = ( ); export default function AttendanceLocation() { const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const mapRef = useRef(null); const mapInstanceRef = useRef(null); const locationQuery = useQuery(attendanceLocationOptions(id)); const record = locationQuery.data ?? null; const isPending = locationQuery.isPending; // Navigate away on fetch error useEffect(() => { if (locationQuery.error) { alert.error("Nepodařilo se načíst data"); navigate("/attendance/admin"); } }, [locationQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!record || isPending) return; const hasArrivalLocation = record.arrival_lat && record.arrival_lng; const hasDepartureLocation = record.departure_lat && record.departure_lng; const hasAnyLocation = hasArrivalLocation || hasDepartureLocation; if (!hasAnyLocation || !mapRef.current) return; const initMap = () => { if (mapInstanceRef.current) { (mapInstanceRef.current as { remove: () => void }).remove(); } const map = L.map(mapRef.current!); mapInstanceRef.current = map; L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap contributors", }).addTo(map); const bounds: [number, number][] = []; interface LocationPoint { lat: number; lng: number; type: string; label: string; time: string; accuracy: number; } const locations: LocationPoint[] = []; if (hasArrivalLocation) { locations.push({ lat: parseFloat(String(record.arrival_lat)), lng: parseFloat(String(record.arrival_lng)), type: "arrival", label: "Příchod", time: formatTime(record.arrival_time), accuracy: Number(record.arrival_accuracy) || 0, }); } if (hasDepartureLocation) { locations.push({ lat: parseFloat(String(record.departure_lat)), lng: parseFloat(String(record.departure_lng)), type: "departure", label: "Odchod", time: formatTime(record.departure_time), accuracy: Number(record.departure_accuracy) || 0, }); } locations.forEach((loc) => { const color = loc.type === "arrival" ? "#22c55e" : "#ef4444"; const marker = L.circleMarker([loc.lat, loc.lng], { radius: 10, fillColor: color, color: "#fff", weight: 2, opacity: 1, fillOpacity: 0.8, }).addTo(map); const popupEl = document.createElement("div"); const strong = document.createElement("strong"); strong.textContent = loc.label; popupEl.appendChild(strong); popupEl.appendChild(document.createElement("br")); popupEl.appendChild(document.createTextNode(loc.time)); popupEl.appendChild(document.createElement("br")); popupEl.appendChild( document.createTextNode(`Přesnost: ${Math.round(loc.accuracy)}m`), ); marker.bindPopup(popupEl); if (loc.accuracy > 0) { L.circle([loc.lat, loc.lng], { radius: loc.accuracy, fillColor: color, color: color, weight: 1, opacity: 0.3, fillOpacity: 0.1, }).addTo(map); } bounds.push([loc.lat, loc.lng]); }); if (bounds.length === 1) { map.setView(bounds[0], 16); } else if (bounds.length > 1) { map.fitBounds(bounds, { padding: [50, 50] }); } }; initMap(); return () => { if (mapInstanceRef.current) { (mapInstanceRef.current as { remove: () => void }).remove(); mapInstanceRef.current = null; } }; }, [record, isPending]); const formatDatetimeLocal = (datetime: string | null | undefined): string => { if (!datetime) return "—"; const d = new Date(datetime); return `${d.getDate()}.${d.getMonth() + 1}.${d.getFullYear()} ${formatTime(datetime)}`; }; if (!hasPermission("attendance.manage")) return ; if (!record) { return null; } const hasArrivalLocation = record.arrival_lat && record.arrival_lng; const hasDepartureLocation = record.departure_lat && record.departure_lng; const hasAnyLocation = hasArrivalLocation || hasDepartureLocation; const shiftDateStr = record.shift_date.includes("T") ? record.shift_date.split("T")[0] : record.shift_date; const month = shiftDateStr.substring(0, 7); if (isPending) { return ; } return ( {/* Header */} Zpět na správu } /> {/* Info + map card */} {record.user_name} — {formatDate(record.shift_date)} {/* Leaflet map */} {hasAnyLocation && } {/* Location detail grid */} {/* Arrival */} Příchod {record.arrival_time ? formatDatetimeLocal(record.arrival_time) : "—"} {hasArrivalLocation ? ( <> {record.arrival_address || Adresa nezjištěna} GPS: {record.arrival_lat}, {record.arrival_lng} {record.arrival_accuracy && ` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`} ) : ( Poloha nebyla zaznamenána )} {/* Departure */} {(hasDepartureLocation || record.departure_time) && ( Odchod {record.departure_time ? formatDatetimeLocal(record.departure_time) : "—"} {hasDepartureLocation ? ( <> {record.departure_address || Adresa nezjištěna} GPS: {record.departure_lat}, {record.departure_lng} {record.departure_accuracy && ` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`} ) : ( Poloha nebyla zaznamenána )} )} ); }