import { useEffect, useState } from "react"; import { MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import { useApp } from "../state/store"; import { Pulse } from "../components/icons"; import { attendanceApi, type AttendanceSite } from "../lib/attendanceApi"; L.Marker.prototype.options.icon = L.icon({ iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png", iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png", shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41], }); function statusTag(s: AttendanceSite["status"]) { if (s === "checked_in") return "ON-SITE"; if (s === "late") return "LATE"; return "OFF"; } function FitToSites({ sites }: { sites: AttendanceSite[] }) { const map = useMap(); useEffect(() => { if (!sites.length) return; const bounds = L.latLngBounds(sites.map((s) => [s.lat, s.lng] as [number, number])); map.fitBounds(bounds, { padding: [40, 40] }); }, [map, sites]); return null; } export default function GeoAttendance() { const setScene = useApp((s) => s.setScene); const pushToast = useApp((s) => s.pushToast); const [filter, setFilter] = useState<"all" | AttendanceSite["kind"]>("all"); const [sites, setSites] = useState(null); useEffect(() => { let cancelled = false; attendanceApi .listSites() .then((rows) => !cancelled && setSites(rows)) .catch((err) => !cancelled && pushToast("err", `Sites failed: ${err.message}`)); return () => { cancelled = true; }; }, []); const allSites = sites || []; const visible = filter === "all" ? allSites : allSites.filter((s) => s.kind === filter); const stats = allSites.reduce( (acc, s) => ({ onSite: acc.onSite + (s.status === "checked_in" ? 1 : 0), late: acc.late + (s.status === "late" ? 1 : 0), off: acc.off + (s.status === "checked_out" ? 1 : 0), }), { onSite: 0, late: 0, off: 0 } ); return (
Attendance map

Where your people are

Live sites and check-in statuses sourced from EA2. Add a site by inserting an attendance flow doc (kind=value, source_context=CANVAS_ATTENDANCE_SITE) and linking it to flow/attendance_root via a defines edge with role=presentation. Tiles are served by OpenStreetMap.

{sites !== null && allSites.length === 0 && (

No sites configured yet. Seed `qa/seeds/003_attendance_sites.mjs` to add the default canvas attendance roster.

)}
{stats.onSite} on-site {stats.late} late {stats.off} off-shift
{(["all", "store", "office", "warehouse"] as const).map((k) => ( ))}
{visible.map((s) => ( {s.label}
{statusTag(s.status)}
{s.who}
))}
); }