From 16f1f684992eebe268736c5e0e28e67eb1c194e8 Mon Sep 17 00:00:00 2001 From: Marlene-Angel <139193696+Marlene-Angel@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:32:23 -0800 Subject: [PATCH] Refactor meters: dividido en hook, sidebar, tabla y modal --- src/pages/meters/MeterPage.tsx | 910 ++++++----------------------- src/pages/meters/MetersModal.tsx | 269 +++++++++ src/pages/meters/MetersSidebar.tsx | 293 ++++++++++ src/pages/meters/MetersTable.tsx | 67 +++ src/pages/meters/useMeters.ts | 94 +++ 5 files changed, 917 insertions(+), 716 deletions(-) create mode 100644 src/pages/meters/MetersModal.tsx create mode 100644 src/pages/meters/MetersSidebar.tsx create mode 100644 src/pages/meters/MetersTable.tsx create mode 100644 src/pages/meters/useMeters.ts diff --git a/src/pages/meters/MeterPage.tsx b/src/pages/meters/MeterPage.tsx index daef8f9..69d22c1 100644 --- a/src/pages/meters/MeterPage.tsx +++ b/src/pages/meters/MeterPage.tsx @@ -1,16 +1,17 @@ -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; -import MaterialTable from "@material-table/core"; -import { - fetchMeters, - createMeter, - updateMeter, - deleteMeter, - type Meter, -} from "../../api/meters"; -import ConfirmModal from "../../components/layout/common/ConfirmModal"; // ✅ NUEVO +import type { Meter } from "../../api/meters"; +import { createMeter, deleteMeter, updateMeter } from "../../api/meters"; +import ConfirmModal from "../../components/layout/common/ConfirmModal"; -interface DeviceData { +import { useMeters } from "./useMeters"; +import MetersSidebar from "./MetersSidebar"; +import MetersTable from "./MetersTable"; +import MetersModal from "./MetersModal"; + +/* ================= TYPES (exportables para otros componentes) ================= */ + +export interface DeviceData { "Device ID": number; "Device EUI": string; "Join EUI": string; @@ -18,12 +19,12 @@ interface DeviceData { meterId?: string; } -type ProjectStatus = "ACTIVO" | "INACTIVO"; +export type ProjectStatus = "ACTIVO" | "INACTIVO"; -type ProjectCard = { +export type ProjectCard = { name: string; region: string; - projects: number; // placeholder + projects: number; meters: number; activeAlerts: number; lastSync: string; @@ -31,141 +32,137 @@ type ProjectCard = { status: ProjectStatus; }; +export type TakeType = "GENERAL" | "LORA" | "LORAWAN" | "GRANDES"; + +/* ================= MOCKS (sin backend) ================= */ + +const MOCK_PROJECTS_BY_TYPE: Record< + Exclude, + Array<{ name: string; meters?: number }> +> = { + LORA: [ + { name: "LoRa - Demo 01", meters: 12 }, + { name: "LoRa - Demo 02", meters: 7 }, + ], + LORAWAN: [{ name: "LoRaWAN - Demo 01", meters: 4 }], + GRANDES: [{ name: "Grandes - Demo 01", meters: 2 }], +}; + /* ================= COMPONENT ================= */ -export default function MeterManagement({ + +export default function MetersPage({ selectedProject: initialProject, }: { selectedProject?: string } = {}) { - const [allProjects, setAllProjects] = useState([]); - const [loadingProjects, setLoadingProjects] = useState(true); + const m = useMeters({ initialProject }); - const [selectedProject, setSelectedProject] = useState(initialProject || ""); + // UI state + const [takeType, setTakeType] = useState("GENERAL"); + const isMockMode = takeType !== "GENERAL"; - const [meters, setMeters] = useState([]); - const [filteredMeters, setFilteredMeters] = useState([]); - const [loadingMeters, setLoadingMeters] = useState(true); const [activeMeter, setActiveMeter] = useState(null); const [search, setSearch] = useState(""); - const [projectQuery, setProjectQuery] = useState(""); - const [showModal, setShowModal] = useState(false); const [editingId, setEditingId] = useState(null); - // ✅ NUEVO: confirm modal delete const [confirmOpen, setConfirmOpen] = useState(false); const [deleting, setDeleting] = useState(false); - const emptyMeter: Omit = { - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - areaName: "", - accountNumber: null, - userName: null, - userAddress: null, - meterSerialNumber: "", - meterName: "", - meterStatus: "Installed", - protocolType: "", - priceNo: null, - priceName: null, - dmaPartition: null, - supplyTypes: "", - deviceId: "", - deviceName: "", - deviceType: "", - usageAnalysisType: "", - installedTime: new Date().toISOString(), - }; + const emptyMeter: Omit = useMemo( + () => ({ + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + areaName: "", + accountNumber: null, + userName: null, + userAddress: null, + meterSerialNumber: "", + meterName: "", + meterStatus: "Installed", + protocolType: "", + priceNo: null, + priceName: null, + dmaPartition: null, + supplyTypes: "", + deviceId: "", + deviceName: "", + deviceType: "", + usageAnalysisType: "", + installedTime: new Date().toISOString(), + }), + [] + ); - const emptyDeviceData: DeviceData = { - "Device ID": 0, - "Device EUI": "", - "Join EUI": "", - AppKey: "", - }; + const emptyDeviceData: DeviceData = useMemo( + () => ({ + "Device ID": 0, + "Device EUI": "", + "Join EUI": "", + AppKey: "", + }), + [] + ); const [form, setForm] = useState>(emptyMeter); const [deviceForm, setDeviceForm] = useState(emptyDeviceData); - const [errors, setErrors] = useState<{ [key: string]: boolean }>({}); - - /* ================= LOAD ================= */ - const loadMeters = async () => { - setLoadingMeters(true); - setLoadingProjects(true); - try { - const data = await fetchMeters(); - - const projectsArray = [...new Set(data.map((r) => r.areaName))] - .filter(Boolean) as string[]; - - setAllProjects(projectsArray); - setMeters(data); - - // ✅ FIX: si no hay proyecto seleccionado, autoselecciona el primero disponible - setSelectedProject((prev) => { - if (prev) return prev; - if (initialProject) return initialProject; - return projectsArray[0] ?? ""; - }); - } catch (error) { - console.error("Error loading meters:", error); - setAllProjects([]); - setMeters([]); - } finally { - setLoadingMeters(false); - setLoadingProjects(false); - } - }; - - useEffect(() => { - loadMeters(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (initialProject) setSelectedProject(initialProject); - }, [initialProject]); - - // Filtrado por proyecto - useEffect(() => { - if (!selectedProject) { - setFilteredMeters([]); - return; - } - setFilteredMeters(meters.filter((m) => m.areaName === selectedProject)); - }, [selectedProject, meters]); - - /* ================= SIDEBAR PROJECT CARDS (ALWAYS OPEN) ================= */ - const projectsData: ProjectCard[] = useMemo(() => { - const counts = meters.reduce>((acc, m) => { - const area = m.areaName ?? "SIN PROYECTO"; - acc[area] = (acc[area] ?? 0) + 1; - return acc; - }, {}); + const [errors, setErrors] = useState>({}); + // Projects cards (real) + const projectsDataReal: ProjectCard[] = useMemo(() => { const baseRegion = "Baja California"; const baseContact = "Operaciones"; const baseLastSync = "Hace 1 h"; - return allProjects.map((name) => ({ + return m.allProjects.map((name) => ({ name, region: baseRegion, projects: 1, - meters: counts[name] ?? 0, + meters: m.projectsCounts[name] ?? 0, activeAlerts: 0, lastSync: baseLastSync, contact: baseContact, status: "ACTIVO", })); - }, [meters, allProjects]); + }, [m.allProjects, m.projectsCounts]); - const filteredProjects = useMemo(() => { - const q = projectQuery.trim().toLowerCase(); - if (!q) return projectsData; - return projectsData.filter((p) => p.name.toLowerCase().includes(q)); - }, [projectQuery, projectsData]); + // Projects cards (mock) + const projectsDataMock: ProjectCard[] = useMemo(() => { + const baseRegion = "Baja California"; + const baseContact = "Operaciones"; + const baseLastSync = "Hace 1 h"; - /* ================= DEVICE CONFIG MOCK ================= */ + const mocks = MOCK_PROJECTS_BY_TYPE[takeType as Exclude] ?? []; + return mocks.map((x) => ({ + name: x.name, + region: baseRegion, + projects: 1, + meters: x.meters ?? 0, + activeAlerts: 0, + lastSync: baseLastSync, + contact: baseContact, + status: "ACTIVO", + })); + }, [takeType]); + + const sidebarProjects = isMockMode ? projectsDataMock : projectsDataReal; + + // Search filtered + const searchFiltered = useMemo(() => { + if (isMockMode) return []; + const q = search.trim().toLowerCase(); + if (!q) return m.filteredMeters; + + return m.filteredMeters.filter((x) => { + return ( + (x.meterName ?? "").toLowerCase().includes(q) || + (x.meterSerialNumber ?? "").toLowerCase().includes(q) || + (x.deviceId ?? "").toLowerCase().includes(q) || + (x.areaName ?? "").toLowerCase().includes(q) + ); + }); + }, [isMockMode, search, m.filteredMeters]); + + // Device config mock const createOrUpdateDevice = async (deviceData: DeviceData): Promise => { return new Promise((resolve) => { setTimeout(() => { @@ -175,45 +172,43 @@ export default function MeterManagement({ }); }; - /* ================= VALIDATION ================= */ + // Validation const validateForm = (): boolean => { - const newErrors: { [key: string]: boolean } = {}; + const next: Record = {}; - if (!form.meterName.trim()) newErrors["meterName"] = true; - if (!form.meterSerialNumber.trim()) newErrors["meterSerialNumber"] = true; - if (!form.areaName.trim()) newErrors["areaName"] = true; - if (!form.deviceName.trim()) newErrors["deviceName"] = true; - if (!form.protocolType.trim()) newErrors["protocolType"] = true; + if (!form.meterName.trim()) next["meterName"] = true; + if (!form.meterSerialNumber.trim()) next["meterSerialNumber"] = true; + if (!form.areaName.trim()) next["areaName"] = true; + if (!form.deviceName.trim()) next["deviceName"] = true; + if (!form.protocolType.trim()) next["protocolType"] = true; - if (!deviceForm["Device ID"] || deviceForm["Device ID"] === 0) - newErrors["Device ID"] = true; - if (!deviceForm["Device EUI"].trim()) newErrors["Device EUI"] = true; - if (!deviceForm["Join EUI"].trim()) newErrors["Join EUI"] = true; - if (!deviceForm["AppKey"].trim()) newErrors["AppKey"] = true; + if (!deviceForm["Device ID"] || deviceForm["Device ID"] === 0) next["Device ID"] = true; + if (!deviceForm["Device EUI"].trim()) next["Device EUI"] = true; + if (!deviceForm["Join EUI"].trim()) next["Join EUI"] = true; + if (!deviceForm["AppKey"].trim()) next["AppKey"] = true; - setErrors(newErrors); - return Object.keys(newErrors).length === 0; + setErrors(next); + return Object.keys(next).length === 0; }; - /* ================= CRUD ================= */ + // CRUD const handleSave = async () => { + if (isMockMode) return; if (!validateForm()) return; try { let savedMeter: Meter; if (editingId) { - const meterToUpdate = meters.find((m) => m.id === editingId); + const meterToUpdate = m.meters.find((x) => x.id === editingId); if (!meterToUpdate) throw new Error("Meter to update not found"); const updatedMeter = await updateMeter(editingId, form); - setMeters((prev) => - prev.map((m) => (m.id === editingId ? updatedMeter : m)) - ); + m.setMeters((prev) => prev.map((x) => (x.id === editingId ? updatedMeter : x))); savedMeter = updatedMeter; } else { const newMeter = await createMeter(form); - setMeters((prev) => [...prev, newMeter]); + m.setMeters((prev) => [...prev, newMeter]); savedMeter = newMeter; } @@ -234,227 +229,67 @@ export default function MeterManagement({ } catch (error) { console.error("Error saving meter:", error); alert( - `Error saving meter: ${ - error instanceof Error ? error.message : "Please try again." - }` - - + `Error saving meter: ${error instanceof Error ? error.message : "Please try again."}` ); - } - }; - // ✅ MISMA lógica de delete, solo sin window.confirm const handleDelete = async () => { + if (isMockMode) return; if (!activeMeter) return; try { await deleteMeter(activeMeter.id); - setMeters((prev) => prev.filter((m) => m.id !== activeMeter.id)); + m.setMeters((prev) => prev.filter((x) => x.id !== activeMeter.id)); setActiveMeter(null); } catch (error) { console.error("Error deleting meter:", error); alert( - `Error deleting meter: ${ - error instanceof Error ? error.message : "Please try again." - }` + `Error deleting meter: ${error instanceof Error ? error.message : "Please try again."}` ); } }; const handleRefresh = () => { - loadMeters(); + m.loadMeters(); setActiveMeter(null); }; - /* ================= SEARCH (CLIENT) ================= */ - const searchFiltered = filteredMeters.filter((m) => { - const q = search.trim().toLowerCase(); - if (!q) return true; + const resetSelection = () => { + setActiveMeter(null); + setSearch(""); + }; - return ( - (m.meterName ?? "").toLowerCase().includes(q) || - (m.meterSerialNumber ?? "").toLowerCase().includes(q) || - (m.deviceId ?? "").toLowerCase().includes(q) || - (m.areaName ?? "").toLowerCase().includes(q) - ); - }); - - /* ================= UI ================= */ return (
{/* SIDEBAR */} - + {/* MAIN */}
- {/* HEADER */}

Meter Management

- {selectedProject - ? `Proyecto: ${selectedProject}` + {isMockMode + ? `Modo demo (${takeType}) - backend pendiente` + : m.selectedProject + ? `Proyecto: ${m.selectedProject}` : "Selecciona un proyecto desde el panel izquierdo"}

@@ -462,8 +297,10 @@ export default function MeterManagement({
- {/* ✅ CAMBIO: antes llamaba handleDelete, ahora abre modal */}
- {/* SEARCH */} setSearch(e.target.value)} - disabled={!selectedProject} + disabled={isMockMode || !m.selectedProject} /> - {/* TABLE */} -
- rowData.areaName || "-", - }, - { - title: "Account Number", - field: "accountNumber", - render: (rowData) => rowData.accountNumber || "-", - }, - { - title: "User Name", - field: "userName", - render: (rowData) => rowData.userName || "-", - }, - { - title: "User Address", - field: "userAddress", - render: (rowData) => rowData.userAddress || "-", - }, - { - title: "Meter S/N", - field: "meterSerialNumber", - render: (rowData) => rowData.meterSerialNumber || "-", - }, - { - title: "Meter Name", - field: "meterName", - render: (rowData) => rowData.meterName || "-", - }, - { - title: "Protocol Type", - field: "protocolType", - render: (rowData) => rowData.protocolType || "-", - }, - { - title: "Device ID", - field: "deviceId", - render: (rowData) => rowData.deviceId || "-", - }, - { - title: "Device Name", - field: "deviceName", - render: (rowData) => rowData.deviceName || "-", - }, - ]} - data={searchFiltered} - onRowClick={(_, rowData) => setActiveMeter(rowData as Meter)} - options={{ - actionsColumnIndex: -1, - search: false, - paging: true, - sorting: true, - rowStyle: (rowData) => ({ - backgroundColor: - activeMeter?.id === (rowData as Meter).id - ? "#EEF2FF" - : "#FFFFFF", - }), - }} - localization={{ - body: { - emptyDataSourceMessage: !selectedProject - ? "Select a project to view meters." - : loadingMeters - ? "Loading meters..." - : "No meters found. Click 'Add' to create your first meter.", - }, - }} - /> -
+ setActiveMeter(row)} + /> - {/* ✅ NUEVO: ConfirmModal para borrar */}
- {/* MODAL */} {showModal && ( -
-
-

- {editingId ? "Edit Meter" : "Add Meter"} -

- - {/* ✅ FORMULARIO (REINTEGRADO) */} -
-

- Meter Information -

- -
-
- { - setForm({ ...form, areaName: e.target.value }); - if (errors["areaName"]) { - setErrors({ ...errors, areaName: false }); - } - }} - required - /> - {errors["areaName"] && ( -

- This field is required -

- )} -
- -
- - setForm({ - ...form, - accountNumber: e.target.value || null, - }) - } - /> -
-
- -
-
- - setForm({ ...form, userName: e.target.value || null }) - } - /> -
- -
- - setForm({ ...form, userAddress: e.target.value || null }) - } - /> -
-
- -
-
- { - setForm({ ...form, meterSerialNumber: e.target.value }); - if (errors["meterSerialNumber"]) { - setErrors({ ...errors, meterSerialNumber: false }); - } - }} - required - /> - {errors["meterSerialNumber"] && ( -

- This field is required -

- )} -
- -
- { - setForm({ ...form, meterName: e.target.value }); - if (errors["meterName"]) { - setErrors({ ...errors, meterName: false }); - } - }} - required - /> - {errors["meterName"] && ( -

- This field is required -

- )} -
-
- -
-
- { - setForm({ ...form, protocolType: e.target.value }); - if (errors["protocolType"]) { - setErrors({ ...errors, protocolType: false }); - } - }} - required - /> - {errors["protocolType"] && ( -

- This field is required -

- )} -
- -
- - setForm({ ...form, deviceId: e.target.value || "" }) - } - /> -
-
- -
- { - setForm({ ...form, deviceName: e.target.value }); - if (errors["deviceName"]) { - setErrors({ ...errors, deviceName: false }); - } - }} - required - /> - {errors["deviceName"] && ( -

- This field is required -

- )} -
-
- -
-

- Device Configuration -

- -
-
- { - setDeviceForm({ - ...deviceForm, - "Device ID": parseInt(e.target.value) || 0, - }); - if (errors["Device ID"]) { - setErrors({ ...errors, "Device ID": false }); - } - }} - required - min={1} - /> - {errors["Device ID"] && ( -

- This field is required -

- )} -
- -
- { - setDeviceForm({ - ...deviceForm, - "Device EUI": e.target.value, - }); - if (errors["Device EUI"]) { - setErrors({ ...errors, "Device EUI": false }); - } - }} - required - /> - {errors["Device EUI"] && ( -

- This field is required -

- )} -
-
- -
- { - setDeviceForm({ - ...deviceForm, - "Join EUI": e.target.value, - }); - if (errors["Join EUI"]) { - setErrors({ ...errors, "Join EUI": false }); - } - }} - required - /> - {errors["Join EUI"] && ( -

- This field is required -

- )} -
- -
- { - setDeviceForm({ ...deviceForm, AppKey: e.target.value }); - if (errors["AppKey"]) { - setErrors({ ...errors, AppKey: false }); - } - }} - required - /> - {errors["AppKey"] && ( -

- This field is required -

- )} -
-
- -
- - -
-
-
+ { + setShowModal(false); + setDeviceForm(emptyDeviceData); + setErrors({}); + }} + onSave={handleSave} + /> )}
); } - diff --git a/src/pages/meters/MetersModal.tsx b/src/pages/meters/MetersModal.tsx new file mode 100644 index 0000000..55f0f55 --- /dev/null +++ b/src/pages/meters/MetersModal.tsx @@ -0,0 +1,269 @@ +import type React from "react"; +import type { Meter } from "../../api/meters"; +import type { DeviceData } from "./MeterPage"; + +type Props = { + editingId: string | null; + + form: Omit; + setForm: React.Dispatch>>; + + deviceForm: DeviceData; + setDeviceForm: React.Dispatch>; + + errors: Record; + setErrors: React.Dispatch>>; + + onClose: () => void; + onSave: () => void | Promise; +}; + +export default function MetersModal({ + editingId, + form, + setForm, + deviceForm, + setDeviceForm, + errors, + setErrors, + onClose, + onSave, +}: Props) { + const title = editingId ? "Edit Meter" : "Add Meter"; + + return ( +
+
+

{title}

+ + {/* FORM */} +
+

+ Meter Information +

+ +
+
+ { + setForm({ ...form, areaName: e.target.value }); + if (errors["areaName"]) setErrors({ ...errors, areaName: false }); + }} + required + /> + {errors["areaName"] &&

This field is required

} +
+ +
+ + setForm({ ...form, accountNumber: e.target.value || null }) + } + /> +
+
+ +
+
+ + setForm({ ...form, userName: e.target.value || null }) + } + /> +
+ +
+ + setForm({ ...form, userAddress: e.target.value || null }) + } + /> +
+
+ +
+
+ { + setForm({ ...form, meterSerialNumber: e.target.value }); + if (errors["meterSerialNumber"]) + setErrors({ ...errors, meterSerialNumber: false }); + }} + required + /> + {errors["meterSerialNumber"] && ( +

This field is required

+ )} +
+ +
+ { + setForm({ ...form, meterName: e.target.value }); + if (errors["meterName"]) setErrors({ ...errors, meterName: false }); + }} + required + /> + {errors["meterName"] &&

This field is required

} +
+
+ +
+
+ { + setForm({ ...form, protocolType: e.target.value }); + if (errors["protocolType"]) setErrors({ ...errors, protocolType: false }); + }} + required + /> + {errors["protocolType"] &&

This field is required

} +
+ +
+ setForm({ ...form, deviceId: e.target.value || "" })} + /> +
+
+ +
+ { + setForm({ ...form, deviceName: e.target.value }); + if (errors["deviceName"]) setErrors({ ...errors, deviceName: false }); + }} + required + /> + {errors["deviceName"] &&

This field is required

} +
+
+ + {/* DEVICE CONFIG */} +
+

+ Device Configuration +

+ +
+
+ { + setDeviceForm({ ...deviceForm, "Device ID": parseInt(e.target.value) || 0 }); + if (errors["Device ID"]) setErrors({ ...errors, "Device ID": false }); + }} + required + min={1} + /> + {errors["Device ID"] &&

This field is required

} +
+ +
+ { + setDeviceForm({ ...deviceForm, "Device EUI": e.target.value }); + if (errors["Device EUI"]) setErrors({ ...errors, "Device EUI": false }); + }} + required + /> + {errors["Device EUI"] &&

This field is required

} +
+
+ +
+ { + setDeviceForm({ ...deviceForm, "Join EUI": e.target.value }); + if (errors["Join EUI"]) setErrors({ ...errors, "Join EUI": false }); + }} + required + /> + {errors["Join EUI"] &&

This field is required

} +
+ +
+ { + setDeviceForm({ ...deviceForm, AppKey: e.target.value }); + if (errors["AppKey"]) setErrors({ ...errors, AppKey: false }); + }} + required + /> + {errors["AppKey"] &&

This field is required

} +
+
+ + {/* ACTIONS */} +
+ + +
+
+
+ ); +} diff --git a/src/pages/meters/MetersSidebar.tsx b/src/pages/meters/MetersSidebar.tsx new file mode 100644 index 0000000..ae5b352 --- /dev/null +++ b/src/pages/meters/MetersSidebar.tsx @@ -0,0 +1,293 @@ +// src/pages/meters/MetersSidebar.tsx +import { useEffect, useMemo, useRef, useState } from "react"; +import { ChevronDown, RefreshCcw, Check } from "lucide-react"; +import type React from "react"; +import type { ProjectCard, TakeType } from "./MeterPage"; + +type Props = { + loadingProjects: boolean; + + takeType: TakeType; + setTakeType: (t: TakeType) => void; + + selectedProject: string; + setSelectedProject: React.Dispatch>; + + isMockMode: boolean; + projects: ProjectCard[]; + + onRefresh: () => void; + refreshDisabled?: boolean; + + allProjects: string[]; + onResetSelection?: () => void; +}; + +type TakeTypeOption = { key: TakeType; label: string }; + +const TAKE_TYPE_OPTIONS: TakeTypeOption[] = [ + { key: "GENERAL", label: "General" }, + { key: "LORA", label: "LoRa" }, + { key: "LORAWAN", label: "LoRaWAN" }, + { key: "GRANDES", label: "Grandes consumidores" }, +]; + +export default function MetersSidebar({ + loadingProjects, + takeType, + setTakeType, + selectedProject, + setSelectedProject, + isMockMode, + projects, + onRefresh, + refreshDisabled, + allProjects, + onResetSelection, +}: Props) { + const [typesMenuOpen, setTypesMenuOpen] = useState(false); + + // para detectar click fuera (igual a tu implementación) + const menuRef = useRef(null); + + useEffect(() => { + const onClickOutside = (e: MouseEvent) => { + if (!menuRef.current) return; + if (!menuRef.current.contains(e.target as Node)) { + setTypesMenuOpen(false); + } + }; + document.addEventListener("mousedown", onClickOutside); + return () => document.removeEventListener("mousedown", onClickOutside); + }, []); + + const takeTypeLabel = useMemo( + () => TAKE_TYPE_OPTIONS.find((o) => o.key === takeType)?.label ?? "General", + [takeType] + ); + + return ( + + ); +} diff --git a/src/pages/meters/MetersTable.tsx b/src/pages/meters/MetersTable.tsx new file mode 100644 index 0000000..9c5ea82 --- /dev/null +++ b/src/pages/meters/MetersTable.tsx @@ -0,0 +1,67 @@ +import MaterialTable from "@material-table/core"; +import type { Meter } from "../../api/meters"; + +type Props = { + data: Meter[]; + isLoading: boolean; + + isMockMode: boolean; + selectedProject: string; + + activeMeter: Meter | null; + onRowClick: (row: Meter) => void; +}; + +export default function MetersTable({ + data, + isLoading, + isMockMode, + selectedProject, + activeMeter, + onRowClick, +}: Props) { + const disabled = isMockMode || !selectedProject; + + return ( +
+ r.areaName || "-" }, + { title: "Account Number", field: "accountNumber", render: (r: any) => r.accountNumber || "-" }, + { title: "User Name", field: "userName", render: (r: any) => r.userName || "-" }, + { title: "User Address", field: "userAddress", render: (r: any) => r.userAddress || "-" }, + { title: "Meter S/N", field: "meterSerialNumber", render: (r: any) => r.meterSerialNumber || "-" }, + { title: "Meter Name", field: "meterName", render: (r: any) => r.meterName || "-" }, + { title: "Protocol Type", field: "protocolType", render: (r: any) => r.protocolType || "-" }, + { title: "Device ID", field: "deviceId", render: (r: any) => r.deviceId || "-" }, + { title: "Device Name", field: "deviceName", render: (r: any) => r.deviceName || "-" }, + ]} + data={disabled ? [] : data} + onRowClick={(_, rowData) => onRowClick(rowData as Meter)} + options={{ + actionsColumnIndex: -1, + search: false, + paging: true, + sorting: true, + rowStyle: (rowData) => ({ + backgroundColor: + activeMeter?.id === (rowData as Meter).id ? "#EEF2FF" : "#FFFFFF", + }), + }} + localization={{ + body: { + emptyDataSourceMessage: isMockMode + ? "Modo demo: selecciona 'General' para ver datos reales." + : !selectedProject + ? "Select a project to view meters." + : isLoading + ? "Loading meters..." + : "No meters found. Click 'Add' to create your first meter.", + }, + }} + /> +
+ ); +} diff --git a/src/pages/meters/useMeters.ts b/src/pages/meters/useMeters.ts new file mode 100644 index 0000000..6ab8641 --- /dev/null +++ b/src/pages/meters/useMeters.ts @@ -0,0 +1,94 @@ +import { useEffect, useMemo, useState } from "react"; +import { fetchMeters, type Meter } from "../../api/meters"; + +type UseMetersArgs = { + initialProject?: string; +}; + +export function useMeters({ initialProject }: UseMetersArgs) { + const [allProjects, setAllProjects] = useState([]); + const [loadingProjects, setLoadingProjects] = useState(true); + + const [selectedProject, setSelectedProject] = useState(initialProject || ""); + + const [meters, setMeters] = useState([]); + const [filteredMeters, setFilteredMeters] = useState([]); + const [loadingMeters, setLoadingMeters] = useState(true); + + const loadMeters = async () => { + setLoadingMeters(true); + setLoadingProjects(true); + + try { + const data = await fetchMeters(); + + const projectsArray = [...new Set(data.map((r) => r.areaName))] + .filter(Boolean) as string[]; + + setAllProjects(projectsArray); + setMeters(data); + + setSelectedProject((prev) => { + if (prev) return prev; + if (initialProject) return initialProject; + return projectsArray[0] ?? ""; + }); + } catch (error) { + console.error("Error loading meters:", error); + setAllProjects([]); + setMeters([]); + setSelectedProject(""); + } finally { + setLoadingMeters(false); + setLoadingProjects(false); + } + }; + + // init + useEffect(() => { + loadMeters(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // keep selectedProject synced if parent changes initialProject + useEffect(() => { + if (initialProject) setSelectedProject(initialProject); + }, [initialProject]); + + // filter by project + useEffect(() => { + if (!selectedProject) { + setFilteredMeters([]); + return; + } + setFilteredMeters(meters.filter((m) => m.areaName === selectedProject)); + }, [selectedProject, meters]); + + const projectsCounts = useMemo(() => { + return meters.reduce>((acc, m) => { + const area = m.areaName ?? "SIN PROYECTO"; + acc[area] = (acc[area] ?? 0) + 1; + return acc; + }, {}); + }, [meters]); + + return { + // loading + loadingProjects, + loadingMeters, + + // projects + allProjects, + projectsCounts, + selectedProject, + setSelectedProject, + + // data + meters, + setMeters, + filteredMeters, + + // actions + loadMeters, + }; +}