- Add Analytics pages: Map (Leaflet), Reports, and Server metrics - Add Analytics section to sidebar (Admin only) - Improve SHMetersPage and XMetersPage with real API data - Add analytics API service for connector stats and server metrics - Register system routes in backend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
import { useEffect } from "react";
|
|
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
|
|
import L from "leaflet";
|
|
import type { MeterWithCoords } from "../../api/analytics";
|
|
import "leaflet/dist/leaflet.css";
|
|
|
|
// Fix Leaflet default icon issue
|
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
|
L.Icon.Default.mergeOptions({
|
|
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
|
|
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
|
|
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
|
});
|
|
|
|
function FitBounds({ meters }: { meters: MeterWithCoords[] }) {
|
|
const map = useMap();
|
|
|
|
useEffect(() => {
|
|
if (meters.length > 0) {
|
|
try {
|
|
const bounds = L.latLngBounds(
|
|
meters.map((m) => [Number(m.lat), Number(m.lng)] as L.LatLngTuple)
|
|
);
|
|
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 });
|
|
} catch (e) {
|
|
console.error("Error fitting bounds:", e);
|
|
}
|
|
}
|
|
}, [meters, map]);
|
|
|
|
return null;
|
|
}
|
|
|
|
interface MapComponentsProps {
|
|
meters: MeterWithCoords[];
|
|
}
|
|
|
|
export default function MapComponents({ meters }: MapComponentsProps) {
|
|
const defaultCenter: [number, number] = meters.length > 0
|
|
? [Number(meters[0].lat), Number(meters[0].lng)]
|
|
: [32.4724, -116.9498];
|
|
|
|
return (
|
|
<MapContainer
|
|
center={defaultCenter}
|
|
zoom={12}
|
|
style={{ height: "100%", width: "100%" }}
|
|
scrollWheelZoom={true}
|
|
>
|
|
<TileLayer
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
/>
|
|
{meters.length > 0 && <FitBounds meters={meters} />}
|
|
{meters.map((meter) => (
|
|
<Marker
|
|
key={meter.id}
|
|
position={[Number(meter.lat), Number(meter.lng)]}
|
|
>
|
|
<Popup>
|
|
<div className="min-w-[160px]">
|
|
<p className="font-bold">{meter.name || meter.serial_number}</p>
|
|
<p className="text-sm">Serial: {meter.serial_number}</p>
|
|
<p className="text-sm">Proyecto: {meter.project_name || "N/A"}</p>
|
|
<p className="text-sm">
|
|
Estado:{" "}
|
|
<span className={meter.status === "active" ? "text-green-600" : "text-red-600"}>
|
|
{meter.status === "active" ? "Activo" : "Inactivo"}
|
|
</span>
|
|
</p>
|
|
{meter.last_reading != null && (
|
|
<p className="text-sm">Lectura: {Number(meter.last_reading).toFixed(2)} m³</p>
|
|
)}
|
|
</div>
|
|
</Popup>
|
|
</Marker>
|
|
))}
|
|
</MapContainer>
|
|
);
|
|
}
|