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 { formatDate, formatTime } from '../utils/attendanceHelpers' import apiFetch from '../utils/api' const API_BASE = '/api/admin' declare const L: any 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 loadLeaflet = async () => { if ((window as unknown as Record).L) { initMap() return } const link = document.createElement('link') link.rel = 'stylesheet' link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' document.head.appendChild(link) const script = document.createElement('script') script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' script.onload = initMap document.body.appendChild(script) } 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] }) } } loadLeaflet() 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
)}
)}
) }