add tracker

This commit is contained in:
natib21
2026-07-07 08:19:30 +00:00
parent 66167e8ac5
commit ba895e4ba4

View File

@@ -19,6 +19,7 @@ import {
} from "@mantine/core";
import {
APIProvider,
InfoWindow,
Map as GoogleMap,
Marker,
useMap,
@@ -76,6 +77,51 @@ function FitBounds({ points }: { points: LatLng[] }) {
return null;
}
/** Lazily reverse-geocode a coordinate to a human address. */
function useAddress(lat: number, lng: number): string | null {
const [addr, setAddr] = useState<string | null>(null);
useEffect(() => {
if (typeof google === "undefined") return;
setAddr(null);
let cancelled = false;
new google.maps.Geocoder().geocode({ location: { lat, lng } }, (res, status) => {
if (cancelled) return;
setAddr(status === "OK" && res?.[0] ? res[0].formatted_address : "Unknown location");
});
return () => { cancelled = true; };
}, [lat, lng]);
return addr;
}
/** Hover popup: label, coordinates, speed/course, and the reverse-geocoded place. */
function HoverInfo({
device,
lat,
lng,
onClose,
}: {
device: GpsDevice;
lat: number;
lng: number;
onClose: () => void;
}) {
const address = useAddress(lat, lng);
return (
<InfoWindow position={{ lat, lng }} pixelOffset={[0, -46]} onCloseClick={onClose}>
<div style={{ minWidth: 190, fontSize: 13 }}>
<div style={{ fontWeight: 600, marginBottom: 2 }}>{deviceLabel(device)}</div>
<div style={{ fontFamily: "monospace" }}>
{lat.toFixed(5)}, {lng.toFixed(5)}
</div>
<div style={{ color: "#555" }}>
{toNum(device.lastSpeed) ?? 0} km/h · {device.lastCourse ?? 0}°
</div>
<div style={{ color: "#777", marginTop: 4 }}>{address ?? "Locating…"}</div>
</div>
</InfoWindow>
);
}
/** Draw the selected vehicle's recent path as a polyline. */
function RouteTrail({ path }: { path: LatLng[] }) {
const map = useMap();
@@ -97,6 +143,7 @@ export function TrackingPage() {
const { toast } = useToast();
const qc = useQueryClient();
const [selectedId, setSelectedId] = useState<string | null>(null);
const [hoverId, setHoverId] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [editDevice, setEditDevice] = useState<GpsDevice | null>(null);
const [form, setForm] = useState({ imei: "", name: "", vehicleId: "" });
@@ -154,17 +201,25 @@ export function TrackingPage() {
[history],
);
const markerIcon = (d: GpsDevice, selectedFlag: boolean) => {
// Teardrop pin colored by state with a white truck glyph inside.
const markerIcon = (d: GpsDevice, selectedFlag: boolean): google.maps.Icon | undefined => {
if (typeof google === "undefined") return undefined;
const color = selectedFlag ? freightBrand.primary : d.online ? "#2f80ed" : "#95a5a6";
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="40" height="48" viewBox="0 0 40 48">
<path d="M20 2C10 2 2 10 2 20c0 12 18 26 18 26s18-14 18-26C38 10 30 2 20 2Z" fill="${color}" stroke="#ffffff" stroke-width="1.5"/>
<g transform="translate(8,7)" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 18V6a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h1"/>
<path d="M15 18H9"/>
<path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/>
<circle cx="7" cy="18" r="2"/>
<circle cx="17" cy="18" r="2"/>
</g>
</svg>`;
return {
path: "M 0,-9 L 6,9 L 0,4 L -6,9 Z", // arrow
rotation: d.lastCourse ?? 0,
fillColor: selectedFlag ? freightBrand.primary : d.online ? "#2f80ed" : "#95a5a6",
fillOpacity: 1,
strokeColor: "#ffffff",
strokeWeight: 1.5,
scale: 1.4,
} as google.maps.Symbol;
url: `data:image/svg+xml,${encodeURIComponent(svg)}`,
scaledSize: new google.maps.Size(40, 48),
anchor: new google.maps.Point(20, 48),
};
};
const saveMutation = useMutation({
@@ -253,11 +308,18 @@ export function TrackingPage() {
<Marker
key={d.id}
position={{ lat, lng }}
title={deviceLabel(d)}
title={`${deviceLabel(d)}\n${lat.toFixed(5)}, ${lng.toFixed(5)}`}
icon={markerIcon(d, d.id === selectedId)}
onClick={() => setSelectedId(d.id)}
onMouseOver={() => setHoverId(d.id)}
/>
))}
{(() => {
const h = positioned.find((p) => p.d.id === hoverId);
return h ? (
<HoverInfo device={h.d} lat={h.lat} lng={h.lng} onClose={() => setHoverId(null)} />
) : null;
})()}
<FitBounds points={positioned.map((p) => ({ lat: p.lat, lng: p.lng }))} />
{selected?.vehicleId && trail.length > 1 && <RouteTrail path={trail} />}
</GoogleMap>