Add Analytics section and improve Connectors pages
- 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>
This commit is contained in:
80
src/pages/analytics/MapComponents.tsx
Normal file
80
src/pages/analytics/MapComponents.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user