style: run prettier on entire codebase
This commit is contained in:
@@ -1,154 +1,158 @@
|
||||
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 { 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'
|
||||
import { formatDate, formatTime } from "../utils/attendanceHelpers";
|
||||
import apiFetch from "../utils/api";
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
declare const L: any
|
||||
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
|
||||
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<LocationRecord | null>(null)
|
||||
const mapRef = useRef<HTMLDivElement>(null)
|
||||
const mapInstanceRef = useRef<unknown>(null)
|
||||
const alert = useAlert();
|
||||
const { hasPermission } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [record, setRecord] = useState<LocationRecord | null>(null);
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
const mapInstanceRef = useRef<unknown>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/attendance?action=location&id=${id}`)
|
||||
const result = await response.json()
|
||||
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
|
||||
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 })
|
||||
: raw.user_name || "";
|
||||
setRecord({ ...raw, user_name: userName });
|
||||
} else {
|
||||
alert.error('Záznam nebyl nalezen')
|
||||
navigate('/attendance/admin')
|
||||
alert.error("Záznam nebyl nalezen");
|
||||
navigate("/attendance/admin");
|
||||
}
|
||||
} catch {
|
||||
alert.error('Nepodařilo se načíst data')
|
||||
navigate('/attendance/admin')
|
||||
alert.error("Nepodařilo se načíst data");
|
||||
navigate("/attendance/admin");
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchData()
|
||||
}, [id, alert, navigate])
|
||||
fetchData();
|
||||
}, [id, alert, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!record || loading) return
|
||||
if (!record || loading) return;
|
||||
|
||||
const hasArrivalLocation = record.arrival_lat && record.arrival_lng
|
||||
const hasDepartureLocation = record.departure_lat && record.departure_lng
|
||||
const hasAnyLocation = hasArrivalLocation || hasDepartureLocation
|
||||
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
|
||||
if (!hasAnyLocation || !mapRef.current) return;
|
||||
|
||||
const loadLeaflet = async () => {
|
||||
if ((window as unknown as Record<string, unknown>).L) {
|
||||
initMap()
|
||||
return
|
||||
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 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 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()
|
||||
(mapInstanceRef.current as { remove: () => void }).remove();
|
||||
}
|
||||
|
||||
const map = L.map(mapRef.current!)
|
||||
mapInstanceRef.current = map
|
||||
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)
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
const bounds: [number, number][] = []
|
||||
const bounds: [number, number][] = [];
|
||||
|
||||
interface LocationPoint {
|
||||
lat: number
|
||||
lng: number
|
||||
type: string
|
||||
label: string
|
||||
time: string
|
||||
accuracy: number
|
||||
lat: number;
|
||||
lng: number;
|
||||
type: string;
|
||||
label: string;
|
||||
time: string;
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
const locations: LocationPoint[] = []
|
||||
const locations: LocationPoint[] = [];
|
||||
|
||||
if (hasArrivalLocation) {
|
||||
locations.push({
|
||||
lat: parseFloat(String(record.arrival_lat)),
|
||||
lng: parseFloat(String(record.arrival_lng)),
|
||||
type: 'arrival',
|
||||
label: 'Příchod',
|
||||
type: "arrival",
|
||||
label: "Příchod",
|
||||
time: formatTime(record.arrival_time),
|
||||
accuracy: Number(record.arrival_accuracy) || 0
|
||||
})
|
||||
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',
|
||||
type: "departure",
|
||||
label: "Odchod",
|
||||
time: formatTime(record.departure_time),
|
||||
accuracy: Number(record.departure_accuracy) || 0
|
||||
})
|
||||
accuracy: Number(record.departure_accuracy) || 0,
|
||||
});
|
||||
}
|
||||
|
||||
locations.forEach(loc => {
|
||||
const color = loc.type === 'arrival' ? '#22c55e' : '#ef4444'
|
||||
locations.forEach((loc) => {
|
||||
const color = loc.type === "arrival" ? "#22c55e" : "#ef4444";
|
||||
|
||||
const marker = L.circleMarker([loc.lat, loc.lng], {
|
||||
radius: 10,
|
||||
fillColor: color,
|
||||
color: '#fff',
|
||||
color: "#fff",
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
}).addTo(map)
|
||||
fillOpacity: 0.8,
|
||||
}).addTo(map);
|
||||
|
||||
marker.bindPopup(`<strong>${loc.label}</strong><br>${loc.time}<br>Přesnost: ${Math.round(loc.accuracy)}m`)
|
||||
marker.bindPopup(
|
||||
`<strong>${loc.label}</strong><br>${loc.time}<br>Přesnost: ${Math.round(loc.accuracy)}m`,
|
||||
);
|
||||
|
||||
if (loc.accuracy > 0) {
|
||||
L.circle([loc.lat, loc.lng], {
|
||||
@@ -157,55 +161,78 @@ export default function AttendanceLocation() {
|
||||
color: color,
|
||||
weight: 1,
|
||||
opacity: 0.3,
|
||||
fillOpacity: 0.1
|
||||
}).addTo(map)
|
||||
fillOpacity: 0.1,
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
bounds.push([loc.lat, loc.lng])
|
||||
})
|
||||
bounds.push([loc.lat, loc.lng]);
|
||||
});
|
||||
|
||||
if (bounds.length === 1) {
|
||||
map.setView(bounds[0], 16)
|
||||
map.setView(bounds[0], 16);
|
||||
} else if (bounds.length > 1) {
|
||||
map.fitBounds(bounds, { padding: [50, 50] })
|
||||
map.fitBounds(bounds, { padding: [50, 50] });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadLeaflet()
|
||||
loadLeaflet();
|
||||
|
||||
return () => {
|
||||
if (mapInstanceRef.current) {
|
||||
(mapInstanceRef.current as { remove: () => void }).remove()
|
||||
mapInstanceRef.current = null
|
||||
(mapInstanceRef.current as { remove: () => void }).remove();
|
||||
mapInstanceRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [record, loading])
|
||||
};
|
||||
}, [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 (!datetime) return "—";
|
||||
const d = new Date(datetime);
|
||||
return `${d.getDate()}.${d.getMonth() + 1}.${d.getFullYear()} ${formatTime(datetime)}`;
|
||||
};
|
||||
|
||||
if (!hasPermission('attendance.admin')) return <Forbidden />
|
||||
if (!hasPermission("attendance.admin")) return <Forbidden />;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: '1.5rem' }}>
|
||||
<div className="admin-skeleton-row" style={{ justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<div className="admin-skeleton-line" style={{ width: '32px', height: '32px', borderRadius: '8px' }} />
|
||||
<div className="admin-skeleton-line h-8" style={{ width: '200px' }} />
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.5rem" }}>
|
||||
<div
|
||||
className="admin-skeleton-row"
|
||||
style={{ justifyContent: "space-between" }}
|
||||
>
|
||||
<div
|
||||
style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}
|
||||
>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ width: "32px", height: "32px", borderRadius: "8px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line h-8"
|
||||
style={{ width: "200px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-card">
|
||||
<div className="admin-skeleton-line" style={{ width: '100%', height: '300px', borderRadius: '8px' }} />
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ width: "100%", height: "300px", borderRadius: "8px" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.25rem' }}>
|
||||
{[0, 1].map(i => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "1.25rem",
|
||||
}}
|
||||
>
|
||||
{[0, 1].map((i) => (
|
||||
<div key={i} className="admin-card">
|
||||
<div className="admin-skeleton" style={{ gap: '1rem' }}>
|
||||
<div className="admin-skeleton-line h-8" style={{ width: '50%' }} />
|
||||
<div className="admin-skeleton" style={{ gap: "1rem" }}>
|
||||
<div
|
||||
className="admin-skeleton-line h-8"
|
||||
style={{ width: "50%" }}
|
||||
/>
|
||||
<div className="admin-skeleton-line w-full" />
|
||||
<div className="admin-skeleton-line w-3/4" />
|
||||
</div>
|
||||
@@ -213,18 +240,20 @@ export default function AttendanceLocation() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!record) {
|
||||
return null
|
||||
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)
|
||||
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 (
|
||||
<div>
|
||||
@@ -238,7 +267,10 @@ export default function AttendanceLocation() {
|
||||
<h1 className="admin-page-title">Poloha záznamu</h1>
|
||||
</div>
|
||||
<div className="admin-page-actions">
|
||||
<Link to={`/attendance/admin?month=${month}`} className="admin-btn admin-btn-secondary">
|
||||
<Link
|
||||
to={`/attendance/admin?month=${month}`}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
>
|
||||
← Zpět na správu
|
||||
</Link>
|
||||
</div>
|
||||
@@ -257,18 +289,19 @@ export default function AttendanceLocation() {
|
||||
</div>
|
||||
<div className="admin-card-body">
|
||||
{hasAnyLocation && (
|
||||
<div
|
||||
ref={mapRef}
|
||||
className="attendance-location-map"
|
||||
/>
|
||||
<div ref={mapRef} className="attendance-location-map" />
|
||||
)}
|
||||
|
||||
<div className="attendance-location-grid">
|
||||
{/* Arrival */}
|
||||
<div className={`attendance-location-card ${!hasArrivalLocation ? 'empty' : ''}`}>
|
||||
<div
|
||||
className={`attendance-location-card ${!hasArrivalLocation ? "empty" : ""}`}
|
||||
>
|
||||
<h3 className="attendance-location-title">Příchod</h3>
|
||||
<div className="attendance-location-time">
|
||||
{record.arrival_time ? formatDatetimeLocal(record.arrival_time) : '—'}
|
||||
{record.arrival_time
|
||||
? formatDatetimeLocal(record.arrival_time)
|
||||
: "—"}
|
||||
</div>
|
||||
{hasArrivalLocation ? (
|
||||
<>
|
||||
@@ -277,7 +310,8 @@ export default function AttendanceLocation() {
|
||||
</div>
|
||||
<div className="attendance-location-coords">
|
||||
GPS: {record.arrival_lat}, {record.arrival_lng}
|
||||
{record.arrival_accuracy && ` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
|
||||
{record.arrival_accuracy &&
|
||||
` (přesnost: ${Math.round(Number(record.arrival_accuracy))}m)`}
|
||||
</div>
|
||||
<a
|
||||
href={`https://www.google.com/maps?q=${record.arrival_lat},${record.arrival_lng}`}
|
||||
@@ -297,10 +331,14 @@ export default function AttendanceLocation() {
|
||||
|
||||
{/* Departure */}
|
||||
{(hasDepartureLocation || record.departure_time) && (
|
||||
<div className={`attendance-location-card ${!hasDepartureLocation ? 'empty' : ''}`}>
|
||||
<div
|
||||
className={`attendance-location-card ${!hasDepartureLocation ? "empty" : ""}`}
|
||||
>
|
||||
<h3 className="attendance-location-title">Odchod</h3>
|
||||
<div className="attendance-location-time">
|
||||
{record.departure_time ? formatDatetimeLocal(record.departure_time) : '—'}
|
||||
{record.departure_time
|
||||
? formatDatetimeLocal(record.departure_time)
|
||||
: "—"}
|
||||
</div>
|
||||
{hasDepartureLocation ? (
|
||||
<>
|
||||
@@ -309,7 +347,8 @@ export default function AttendanceLocation() {
|
||||
</div>
|
||||
<div className="attendance-location-coords">
|
||||
GPS: {record.departure_lat}, {record.departure_lng}
|
||||
{record.departure_accuracy && ` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
|
||||
{record.departure_accuracy &&
|
||||
` (přesnost: ${Math.round(Number(record.departure_accuracy))}m)`}
|
||||
</div>
|
||||
<a
|
||||
href={`https://www.google.com/maps?q=${record.departure_lat},${record.departure_lng}`}
|
||||
@@ -331,5 +370,5 @@ export default function AttendanceLocation() {
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user