import { useEffect, useState, useMemo, useRef } from "react"; import { History, RefreshCw, Download, Search, X, ChevronLeft, ChevronRight, Droplets, MapPin, Radio, Calendar, TrendingUp, TrendingDown, Minus, } from "lucide-react"; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from "recharts"; import { fetchMeters, fetchMeterReadings, type Meter, type MeterReading, type PaginatedMeterReadings, } from "../../api/meters"; export default function HistoricoPage() { const [meters, setMeters] = useState([]); const [metersLoading, setMetersLoading] = useState(true); const [meterSearch, setMeterSearch] = useState(""); const [dropdownOpen, setDropdownOpen] = useState(false); const [selectedMeter, setSelectedMeter] = useState(null); const [readings, setReadings] = useState([]); const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0, totalPages: 0, }); const [loadingReadings, setLoadingReadings] = useState(false); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [consumoActual, setConsumoActual] = useState(null); const [consumoPasado, setConsumoPasado] = useState(null); const [loadingStats, setLoadingStats] = useState(false); const dropdownRef = useRef(null); // Load meters on mount useEffect(() => { const load = async () => { try { const data = await fetchMeters(); setMeters(data); } catch (err) { console.error("Error loading meters:", err); } finally { setMetersLoading(false); } }; load(); }, []); // Close dropdown on outside click useEffect(() => { const handler = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setDropdownOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); // Filter meters by search const filteredMeters = useMemo(() => { if (!meterSearch.trim()) return meters; const q = meterSearch.toLowerCase(); return meters.filter( (m) => m.name.toLowerCase().includes(q) || m.serialNumber.toLowerCase().includes(q) || (m.location ?? "").toLowerCase().includes(q) || (m.cesptAccount ?? "").toLowerCase().includes(q) || (m.cadastralKey ?? "").toLowerCase().includes(q) ); }, [meters, meterSearch]); // Load readings when meter or filters change const loadReadings = async (page = 1, pageSize?: number) => { if (!selectedMeter) return; setLoadingReadings(true); try { const result: PaginatedMeterReadings = await fetchMeterReadings( selectedMeter.id, { startDate: startDate || undefined, endDate: endDate || undefined, page, pageSize: pageSize ?? pagination.pageSize, } ); setReadings(result.data); setPagination(result.pagination); } catch (err) { console.error("Error loading readings:", err); } finally { setLoadingReadings(false); } }; useEffect(() => { if (selectedMeter) { loadReadings(1); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedMeter, startDate, endDate]); // Load consumption stats when meter changes useEffect(() => { if (!selectedMeter) { setConsumoActual(null); setConsumoPasado(null); return; } const loadStats = async () => { setLoadingStats(true); try { // Consumo Actual: latest reading (today or most recent) const today = new Date(); const todayStr = today.toISOString().split("T")[0]; const latestResult = await fetchMeterReadings(selectedMeter.id, { endDate: todayStr, page: 1, pageSize: 1, }); const actual = latestResult.data.length > 0 ? Number(latestResult.data[0].readingValue) : null; setConsumoActual(actual); // Consumo Pasado: reading closest to first day of last month const firstOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1); const secondOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 2); const pastResult = await fetchMeterReadings(selectedMeter.id, { startDate: firstOfLastMonth.toISOString().split("T")[0], endDate: secondOfLastMonth.toISOString().split("T")[0], page: 1, pageSize: 1, }); if (pastResult.data.length > 0) { setConsumoPasado(Number(pastResult.data[0].readingValue)); } else { // Fallback: get the oldest reading around that date range const endOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0); const fallbackResult = await fetchMeterReadings(selectedMeter.id, { startDate: firstOfLastMonth.toISOString().split("T")[0], endDate: endOfLastMonth.toISOString().split("T")[0], page: 1, pageSize: 1, }); setConsumoPasado( fallbackResult.data.length > 0 ? Number(fallbackResult.data[0].readingValue) : null ); } } catch (err) { console.error("Error loading consumption stats:", err); } finally { setLoadingStats(false); } }; loadStats(); }, [selectedMeter]); const diferencial = useMemo(() => { if (consumoActual === null || consumoPasado === null) return null; return consumoActual - consumoPasado; }, [consumoActual, consumoPasado]); const handleSelectMeter = (meter: Meter) => { setSelectedMeter(meter); setMeterSearch(meter.name || meter.serialNumber); setDropdownOpen(false); setReadings([]); setPagination({ page: 1, pageSize: pagination.pageSize, total: 0, totalPages: 0 }); }; const handlePageChange = (newPage: number) => { loadReadings(newPage); }; const handlePageSizeChange = (newSize: number) => { setPagination((prev) => ({ ...prev, pageSize: newSize, page: 1 })); loadReadings(1, newSize); }; // Chart data: readings sorted ascending by date const chartData = useMemo(() => { return [...readings] .sort((a, b) => new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime()) .map((r) => ({ date: new Date(r.receivedAt).toLocaleDateString("es-MX", { day: "2-digit", month: "short", }), fullDate: new Date(r.receivedAt).toLocaleString("es-MX", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }), value: Number(r.readingValue), })); }, [readings]); // Compute tight Y-axis domain for chart const chartDomain = useMemo(() => { if (chartData.length === 0) return [0, 100]; const values = chartData.map((d) => d.value); const min = Math.min(...values); const max = Math.max(...values); const range = max - min; const padding = range > 0 ? range * 0.15 : max * 0.05 || 1; return [ Math.max(0, Math.floor(min - padding)), Math.ceil(max + padding), ]; }, [chartData]); const formatDate = (dateStr: string | null): string => { if (!dateStr) return "—"; return new Date(dateStr).toLocaleString("es-MX", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }); }; const exportToCSV = () => { if (!selectedMeter || readings.length === 0) return; const headers = ["Fecha/Hora", "Lectura (m³)", "Tipo", "Batería", "Señal"]; const rows = readings.map((r) => [ formatDate(r.receivedAt), Number(r.readingValue).toFixed(2), r.readingType || "—", r.batteryLevel !== null ? `${r.batteryLevel}%` : "—", r.signalStrength !== null ? `${r.signalStrength} dBm` : "—", ]); const csv = [headers, ...rows].map((row) => row.join(",")).join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); const serial = selectedMeter.serialNumber || "meter"; const date = new Date().toISOString().split("T")[0]; link.download = `historico_${serial}_${date}.csv`; link.click(); }; const clearDateFilters = () => { setStartDate(""); setEndDate(""); }; return (
{/* Header */}

{"Histórico de Tomas"}

Consulta el historial de lecturas por medidor

{/* Meter Selector */}
{ setMeterSearch(e.target.value); setDropdownOpen(true); }} onFocus={() => setDropdownOpen(true)} placeholder={metersLoading ? "Cargando medidores..." : "Buscar por nombre, serial, ubicación, cuenta CESPT o clave catastral..."} disabled={metersLoading} className="w-full pl-10 pr-10 py-2.5 text-sm bg-slate-50 dark:bg-zinc-800 dark:text-zinc-100 border border-slate-200 dark:border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 transition-all" /> {meterSearch && ( )} {/* Dropdown */} {dropdownOpen && filteredMeters.length > 0 && (
{filteredMeters.slice(0, 50).map((meter) => ( ))}
)} {dropdownOpen && meterSearch && filteredMeters.length === 0 && !metersLoading && (
No se encontraron medidores
)}
{/* No meter selected state */} {!selectedMeter && (

Selecciona un medidor

Busca y selecciona un medidor para ver su historial de lecturas

)} {/* Content when meter is selected */} {selectedMeter && ( <> {/* Meter Info Card */}
} label="Serial" value={selectedMeter.serialNumber} /> } label="Nombre" value={selectedMeter.name} /> } label="Proyecto" value={selectedMeter.projectName || "—"} /> } label="Ubicación" value={selectedMeter.location || "—"} /> } label="Última Lectura" value={ selectedMeter.lastReadingValue !== null ? `${Number(selectedMeter.lastReadingValue).toFixed(2)} m³` : "Sin datos" } />
{/* Consumption Stats */}

Diferencial

{loadingStats ? (
) : diferencial !== null ? (

0 ? "text-emerald-600 dark:text-emerald-400" : diferencial < 0 ? "text-red-600 dark:text-red-400" : "text-slate-800 dark:text-white" }`}> {diferencial > 0 ? "+" : ""}{diferencial.toFixed(2)} {"m³"}

) : (

{"—"}

)}

Actual - Pasado

0 ? "bg-gradient-to-br from-emerald-500 to-emerald-600" : diferencial !== null && diferencial < 0 ? "bg-gradient-to-br from-red-500 to-red-600" : "bg-gradient-to-br from-slate-400 to-slate-500" }`}> {diferencial !== null && diferencial > 0 ? ( ) : diferencial !== null && diferencial < 0 ? ( ) : ( )}
{/* Date Filters */}
setStartDate(e.target.value)} className="px-3 py-1.5 text-sm bg-slate-50 dark:bg-zinc-800 dark:text-zinc-100 border border-slate-200 dark:border-zinc-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20" />
setEndDate(e.target.value)} className="px-3 py-1.5 text-sm bg-slate-50 dark:bg-zinc-800 dark:text-zinc-100 border border-slate-200 dark:border-zinc-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20" />
{(startDate || endDate) && ( )}
{/* Chart */} {chartData.length > 1 && (

{"Consumo en el Tiempo"}

{`${chartData.length} lecturas en el período`}

{"Lectura (m³)"}
[ `${(value ?? 0).toFixed(2)} m³`, "Lectura", ]} labelFormatter={(_label, payload) => // eslint-disable-next-line @typescript-eslint/no-explicit-any (payload as any)?.[0]?.payload?.fullDate || _label } />
)} {/* Table */}
{pagination.total} {" "} lecturas encontradas {pagination.totalPages > 1 && (
{pagination.page} / {pagination.totalPages}
)}
{loadingReadings ? ( Array.from({ length: 8 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, j) => ( ))} )) ) : readings.length === 0 ? ( ) : ( readings.map((reading, idx) => ( )) )}
Fecha / Hora {"Lectura (m³)"} Tipo {"Batería"} {"Señal"}

No hay lecturas disponibles

{startDate || endDate ? "Intenta ajustar el rango de fechas" : "Este medidor aún no tiene lecturas registradas"}

{formatDate(reading.receivedAt)} {Number(reading.readingValue).toFixed(2)} {reading.batteryLevel !== null ? ( ) : ( {"—"} )} {reading.signalStrength !== null ? ( ) : ( {"—"} )}
{/* Footer pagination */} {!loadingReadings && readings.length > 0 && (
Mostrando{" "} {(pagination.page - 1) * pagination.pageSize + 1} {" "} a{" "} {Math.min(pagination.page * pagination.pageSize, pagination.total)} {" "} de{" "} {pagination.total} {" "} resultados
{"Filas por página:"}
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1) .filter((pageNum) => { if (pageNum === 1 || pageNum === pagination.totalPages) return true; if (Math.abs(pageNum - pagination.page) <= 1) return true; return false; }) .map((pageNum, idx, arr) => { const prevNum = arr[idx - 1]; const showEllipsis = prevNum && pageNum - prevNum > 1; return (
{showEllipsis && ( ... )}
); })}
)}
)}
); } function InfoItem({ icon, label, value, }: { icon: React.ReactNode; label: string; value: string; }) { return (
{icon}

{label}

{value}

); } function ConsumptionCard({ label, sublabel, value, loading, gradient, }: { label: string; sublabel: string; value: number | null; loading: boolean; gradient: string; }) { return (

{label}

{loading ? (
) : value !== null ? (

{value.toFixed(2)} {"m³"}

) : (

{"—"}

)}

{sublabel}

); } function TypeBadge({ type }: { type: string | null }) { if (!type) return {"—"}; const styles: Record = { AUTOMATIC: { bg: "bg-emerald-50 dark:bg-emerald-900/30", text: "text-emerald-700 dark:text-emerald-400", dot: "bg-emerald-500", }, MANUAL: { bg: "bg-blue-50 dark:bg-blue-900/30", text: "text-blue-700 dark:text-blue-400", dot: "bg-blue-500", }, SCHEDULED: { bg: "bg-violet-50 dark:bg-violet-900/30", text: "text-violet-700 dark:text-violet-400", dot: "bg-violet-500", }, }; const style = styles[type] || { bg: "bg-slate-50 dark:bg-zinc-800", text: "text-slate-700 dark:text-zinc-300", dot: "bg-slate-500", }; return ( {type} ); } function BatteryIndicator({ level }: { level: number }) { const getColor = () => { if (level > 50) return "bg-emerald-500"; if (level > 20) return "bg-amber-500"; return "bg-red-500"; }; return (
{level}%
); } function SignalIndicator({ strength }: { strength: number }) { const getBars = () => { if (strength >= -70) return 4; if (strength >= -85) return 3; if (strength >= -100) return 2; return 1; }; const bars = getBars(); return (
{[1, 2, 3, 4].map((i) => (
))}
); }