import { useState, useEffect, useRef } from "react"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; import { useNavigate, useParams, Link } from "react-router-dom"; import { motion } from "framer-motion"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import { formatDate, formatTime } from "../utils/attendanceHelpers"; import apiFetch from "../utils/api"; const API_BASE = "/api/admin"; interface LocationRecord { user_name: string; shift_date: string; arrival_time?: string | null; departure_time?: string | null; arrival_lat?: string | number | null; arrival_lng?: string | number | null; arrival_accuracy?: number | null; arrival_address?: string | null; departure_lat?: string | number | null; departure_lng?: string | number | null; departure_accuracy?: number | null; departure_address?: string | null; } export default function AttendanceLocation() { const alert = useAlert(); const { hasPermission } = useAuth(); const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const [loading, setLoading] = useState(true); const [record, setRecord] = useState(null); const mapRef = useRef(null); const mapInstanceRef = useRef(null); useEffect(() => { const fetchData = async () => { try { const response = await apiFetch( `${API_BASE}/attendance?action=location&id=${id}`, ); const result = await response.json(); if (result.success) { const raw = result.data.record || result.data; // Enrich with user_name from nested users relation const userName = raw.users ? `${raw.users.first_name} ${raw.users.last_name}`.trim() : raw.user_name || ""; setRecord({ ...raw, user_name: userName }); } else { alert.error("Záznam nebyl nalezen"); navigate("/attendance/admin"); } } catch { alert.error("Nepodařilo se načíst data"); navigate("/attendance/admin"); } finally { setLoading(false); } }; fetchData(); }, [id, alert, navigate]); useEffect(() => { if (!record || loading) 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); marker.bindPopup( `${loc.label}
${loc.time}
Přesnost: ${Math.round(loc.accuracy)}m`, ); 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, loading]); 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.admin")) return ; if (loading) { return (
{[0, 1].map((i) => (
))}
); } 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); return (

Poloha záznamu

← Zpět na správu

{record.user_name} — {formatDate(record.shift_date)}

{hasAnyLocation && (
)}
{/* 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)`}
Otevřít v Google Maps ) : (
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)`}
Otevřít v Google Maps ) : (
Poloha nebyla zaznamenána
)}
)}
); }