diff --git a/src/api/meters.ts b/src/api/meters.ts new file mode 100644 index 0000000..a7f9ca3 --- /dev/null +++ b/src/api/meters.ts @@ -0,0 +1,262 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +export const METERS_API_URL = `${API_BASE_URL}/api/v3/data/ppfu31vhv5gf6i0/mp1izvcpok5rk6s/records`; +const API_TOKEN = import.meta.env.VITE_API_TOKEN; + +const getAuthHeaders = () => ({ + Authorization: `Bearer ${API_TOKEN}`, + "Content-Type": "application/json", +}); + +export interface MeterRecord { + id: string; + fields: { + "Area Name": string; + "Account Number": string; + "User Name": string; + "User Address": string; + "Meter S/N": string; + "Meter Name": string; + "Meter Status": string; + "Protocol Type": string; + "Price No.": string; + "Price Name": string; + "DMA Partition": string; + "Supply Types": string; + "Device ID": string; + "Device Name": string; + "Device Type": string; + "Usage Analysis Type": string; + "Installed Time": string; + }; +} + +export interface MetersResponse { + records: MeterRecord[]; + next?: string; + prev?: string; + nestedNext?: string; + nestedPrev?: string; +} + +export interface Meter { + id: string; + areaName: string; + accountNumber: string; + userName: string; + userAddress: string; + meterSN: string; + meterName: string; + meterStatus: string; + protocolType: string; + priceNo: string; + priceName: string; + dmaPartition: string; + supplyTypes: string; + deviceID: string; + deviceName: string; + deviceType: string; + usageAnalysisType: string; + installedTime: string; +} + +export const fetchMeters = async (): Promise => { + try { + const response = await fetch(METERS_API_URL, { + method: "GET", + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to fetch meters"); + } + + const data: MetersResponse = await response.json(); + + return data.records.map((r: MeterRecord) => ({ + id: r.id, + areaName: r.fields["Area Name"] || "", + accountNumber: r.fields["Account Number"] || "", + userName: r.fields["User Name"] || "", + userAddress: r.fields["User Address"] || "", + meterSN: r.fields["Meter S/N"] || "", + meterName: r.fields["Meter Name"] || "", + meterStatus: r.fields["Meter Status"] || "", + protocolType: r.fields["Protocol Type"] || "", + priceNo: r.fields["Price No."] || "", + priceName: r.fields["Price Name"] || "", + dmaPartition: r.fields["DMA Partition"] || "", + supplyTypes: r.fields["Supply Types"] || "", + deviceID: r.fields["Device ID"] || "", + deviceName: r.fields["Device Name"] || "", + deviceType: r.fields["Device Type"] || "", + usageAnalysisType: r.fields["Usage Analysis Type"] || "", + installedTime: r.fields["Installed Time"] || "", + })); + } catch (error) { + console.error("Error fetching meters:", error); + throw error; + } +}; + +export const createMeter = async ( + meterData: Omit +): Promise => { + try { + const response = await fetch(METERS_API_URL, { + method: "POST", + headers: getAuthHeaders(), + body: JSON.stringify({ + fields: { + "Area Name": meterData.areaName, + "Account Number": meterData.accountNumber, + "User Name": meterData.userName, + "User Address": meterData.userAddress, + "Meter S/N": meterData.meterSN, + "Meter Name": meterData.meterName, + "Meter Status": meterData.meterStatus, + "Protocol Type": meterData.protocolType, + "Price No.": meterData.priceNo, + "Price Name": meterData.priceName, + "DMA Partition": meterData.dmaPartition, + "Supply Types": meterData.supplyTypes, + "Device ID": meterData.deviceID, + "Device Name": meterData.deviceName, + "Device Type": meterData.deviceType, + "Usage Analysis Type": meterData.usageAnalysisType, + "Installed Time": meterData.installedTime, + }, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to create meter: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const createdRecord = data.records?.[0]; + + if (!createdRecord) { + throw new Error("Invalid response format: no record returned"); + } + + return { + id: createdRecord.id, + areaName: createdRecord.fields["Area Name"] || meterData.areaName, + accountNumber: createdRecord.fields["Account Number"] || meterData.accountNumber, + userName: createdRecord.fields["User Name"] || meterData.userName, + userAddress: createdRecord.fields["User Address"] || meterData.userAddress, + meterSN: createdRecord.fields["Meter S/N"] || meterData.meterSN, + meterName: createdRecord.fields["Meter Name"] || meterData.meterName, + meterStatus: createdRecord.fields["Meter Status"] || meterData.meterStatus, + protocolType: createdRecord.fields["Protocol Type"] || meterData.protocolType, + priceNo: createdRecord.fields["Price No."] || meterData.priceNo, + priceName: createdRecord.fields["Price Name"] || meterData.priceName, + dmaPartition: createdRecord.fields["DMA Partition"] || meterData.dmaPartition, + supplyTypes: createdRecord.fields["Supply Types"] || meterData.supplyTypes, + deviceID: createdRecord.fields["Device ID"] || meterData.deviceID, + deviceName: createdRecord.fields["Device Name"] || meterData.deviceName, + deviceType: createdRecord.fields["Device Type"] || meterData.deviceType, + usageAnalysisType: createdRecord.fields["Usage Analysis Type"] || meterData.usageAnalysisType, + installedTime: createdRecord.fields["Installed Time"] || meterData.installedTime, + }; + } catch (error) { + console.error("Error creating meter:", error); + throw error; + } +}; + +export const updateMeter = async ( + id: string, + meterData: Omit +): Promise => { + try { + const response = await fetch(METERS_API_URL, { + method: "PATCH", + headers: getAuthHeaders(), + body: JSON.stringify({ + id: id, + fields: { + "Area Name": meterData.areaName, + "Account Number": meterData.accountNumber, + "User Name": meterData.userName, + "User Address": meterData.userAddress, + "Meter S/N": meterData.meterSN, + "Meter Name": meterData.meterName, + "Meter Status": meterData.meterStatus, + "Protocol Type": meterData.protocolType, + "Price No.": meterData.priceNo, + "Price Name": meterData.priceName, + "DMA Partition": meterData.dmaPartition, + "Supply Types": meterData.supplyTypes, + "Device ID": meterData.deviceID, + "Device Name": meterData.deviceName, + "Device Type": meterData.deviceType, + "Usage Analysis Type": meterData.usageAnalysisType, + "Installed Time": meterData.installedTime, + }, + }), + }); + + if (!response.ok) { + if (response.status === 400) { + const errorData = await response.json(); + throw new Error(`Bad Request: ${errorData.msg || "Invalid data provided"}`); + } + throw new Error(`Failed to update meter: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + const updatedRecord = data.records?.[0]; + + if (!updatedRecord) { + throw new Error("Invalid response format: no record returned"); + } + + return { + id: updatedRecord.id, + areaName: updatedRecord.fields["Area Name"] || meterData.areaName, + accountNumber: updatedRecord.fields["Account Number"] || meterData.accountNumber, + userName: updatedRecord.fields["User Name"] || meterData.userName, + userAddress: updatedRecord.fields["User Address"] || meterData.userAddress, + meterSN: updatedRecord.fields["Meter S/N"] || meterData.meterSN, + meterName: updatedRecord.fields["Meter Name"] || meterData.meterName, + meterStatus: updatedRecord.fields["Meter Status"] || meterData.meterStatus, + protocolType: updatedRecord.fields["Protocol Type"] || meterData.protocolType, + priceNo: updatedRecord.fields["Price No."] || meterData.priceNo, + priceName: updatedRecord.fields["Price Name"] || meterData.priceName, + dmaPartition: updatedRecord.fields["DMA Partition"] || meterData.dmaPartition, + supplyTypes: updatedRecord.fields["Supply Types"] || meterData.supplyTypes, + deviceID: updatedRecord.fields["Device ID"] || meterData.deviceID, + deviceName: updatedRecord.fields["Device Name"] || meterData.deviceName, + deviceType: updatedRecord.fields["Device Type"] || meterData.deviceType, + usageAnalysisType: updatedRecord.fields["Usage Analysis Type"] || meterData.usageAnalysisType, + installedTime: updatedRecord.fields["Installed Time"] || meterData.installedTime, + }; + } catch (error) { + console.error("Error updating meter:", error); + throw error; + } +}; + +export const deleteMeter = async (id: string): Promise => { + try { + const response = await fetch(METERS_API_URL, { + method: "DELETE", + headers: getAuthHeaders(), + body: JSON.stringify({ + id: id, + }), + }); + + if (!response.ok) { + if (response.status === 400) { + const errorData = await response.json(); + throw new Error(`Bad Request: ${errorData.msg || "Invalid data provided"}`); + } + throw new Error(`Failed to delete meter: ${response.status} ${response.statusText}`); + } + } catch (error) { + console.error("Error deleting meter:", error); + throw error; + } +}; diff --git a/src/pages/meters/MeterPage.tsx b/src/pages/meters/MeterPage.tsx index 5e4c1b9..52201db 100644 --- a/src/pages/meters/MeterPage.tsx +++ b/src/pages/meters/MeterPage.tsx @@ -1,15 +1,16 @@ -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; import MaterialTable from "@material-table/core"; +import { fetchProjectNames } from "../../api/projects"; +import { + fetchMeters, + createMeter, + updateMeter, + deleteMeter, + type Meter, +} from "../../api/meters"; /* ================= TYPES ================= */ -export interface Meter { - id: string; // recordId - serialNumber: string; - status: "ACTIVE" | "INACTIVE"; - project: string; - createdAt: string; -} interface User { name: string; @@ -26,47 +27,45 @@ export default function MeterManagement() { project: "CESPT", }; - // Lista de proyectos disponibles - const allProjects = ["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"]; + const [allProjects, setAllProjects] = useState([]); + const [loadingProjects, setLoadingProjects] = useState(true); // Proyectos visibles segĂșn el usuario - const visibleProjects = + const visibleProjects = useMemo(() => currentUser.role === "SUPER_ADMIN" ? allProjects : currentUser.project ? [currentUser.project] - : []; - - const [selectedProject, setSelectedProject] = useState( - visibleProjects[0] || "" + : [], + [allProjects, currentUser.role, currentUser.project] ); - // Datos locales iniciales (simulan la API) - const initialMeters: Meter[] = [ - { - id: "1", - serialNumber: "SN001", - status: "ACTIVE", - project: "GRH (PADRE)", - createdAt: "2025-12-17", - }, - { - id: "2", - serialNumber: "SN002", - status: "INACTIVE", - project: "CESPT", - createdAt: "2025-12-16", - }, - { - id: "3", - serialNumber: "SN003", - status: "ACTIVE", - project: "Proyecto A", - createdAt: "2025-12-15", - }, - ]; + const [selectedProject, setSelectedProject] = useState(""); - const [meters, setMeters] = useState(initialMeters); + useEffect(() => { + const loadProjects = async () => { + try { + const projects = await fetchProjectNames(); + setAllProjects(projects); + } catch (error) { + console.error('Error loading projects:', error); + setAllProjects([]); + } finally { + setLoadingProjects(false); + } + }; + + loadProjects(); + }, []); + + useEffect(() => { + if (visibleProjects.length > 0 && !selectedProject) { + setSelectedProject(visibleProjects[0]); + } + }, [visibleProjects, selectedProject]); + + const [meters, setMeters] = useState([]); + const [loadingMeters, setLoadingMeters] = useState(true); const [activeMeter, setActiveMeter] = useState(null); const [search, setSearch] = useState(""); @@ -74,54 +73,111 @@ export default function MeterManagement() { const [editingId, setEditingId] = useState(null); const emptyMeter: Omit = { - serialNumber: "", - status: "ACTIVE", - project: selectedProject, - createdAt: new Date().toISOString().slice(0, 10), + areaName: selectedProject, + accountNumber: "", + userName: "", + userAddress: "", + meterSN: "", + meterName: "", + meterStatus: "ACTIVE", + protocolType: "", + priceNo: "", + priceName: "", + dmaPartition: "", + supplyTypes: "", + deviceID: "", + deviceName: "", + deviceType: "", + usageAnalysisType: "", + installedTime: new Date().toISOString().slice(0, 10), }; const [form, setForm] = useState>(emptyMeter); - /* ================= CRUD LOCAL ================= */ - const handleSave = () => { - if (editingId) { - setMeters((prev) => - prev.map((m) => - m.id === editingId ? { ...m, ...form } : m - ) - ); - } else { - const newMeter: Meter = { - id: (Math.random() * 1000000).toFixed(0), - ...form, - }; - setMeters((prev) => [...prev, newMeter]); + const loadMeters = async () => { + setLoadingMeters(true); + try { + const data = await fetchMeters(); + setMeters(data); + } catch (error) { + console.error("Error loading meters:", error); + setMeters([]); + } finally { + setLoadingMeters(false); } - - setShowModal(false); - setEditingId(null); - setForm({ ...emptyMeter, project: selectedProject }); - setActiveMeter(null); }; - const handleDelete = () => { + useEffect(() => { + loadMeters(); + }, []); + + const handleSave = async () => { + try { + if (editingId) { + const meterToUpdate = meters.find(m => m.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 + ) + ); + } else { + const newMeter = await createMeter(form); + setMeters((prev) => [...prev, newMeter]); + } + setShowModal(false); + setEditingId(null); + setForm({ ...emptyMeter, areaName: selectedProject }); + setActiveMeter(null); + } catch (error) { + console.error('Error saving meter:', error); + alert( + `Error saving meter: ${ + error instanceof Error ? error.message : "Please try again." + }` + ); + } + }; + + const handleDelete = async () => { if (!activeMeter) return; - setMeters((prev) => prev.filter((m) => m.id !== activeMeter.id)); - setActiveMeter(null); + + const confirmDelete = window.confirm( + `Are you sure you want to delete the meter "${activeMeter.meterName}"?` + ); + + if (!confirmDelete) return; + + try { + await deleteMeter(activeMeter.id); + setMeters((prev) => prev.filter((m) => m.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." + }` + ); + } }; const handleRefresh = () => { - // Simula recargar los datos originales - setMeters(initialMeters); + loadMeters(); setActiveMeter(null); }; /* ================= FILTER ================= */ const filtered = meters.filter( (m) => - (m.serialNumber.toLowerCase().includes(search.toLowerCase()) || - m.project.toLowerCase().includes(search.toLowerCase())) && - m.project === selectedProject + (m.meterName.toLowerCase().includes(search.toLowerCase()) || + m.meterSN.toLowerCase().includes(search.toLowerCase()) || + m.areaName.toLowerCase().includes(search.toLowerCase())) && + m.areaName === selectedProject ); /* ================= UI ================= */ @@ -137,13 +193,26 @@ export default function MeterManagement() { value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)} className="w-full border px-3 py-2 rounded" + disabled={loadingProjects || visibleProjects.length === 0} > - {visibleProjects.map((proj) => ( - - ))} + {loadingProjects ? ( + + ) : visibleProjects.length === 0 ? ( + + ) : ( + visibleProjects.map((proj) => ( + + )) + )} + + {visibleProjects.length === 0 && !loadingProjects && ( +

+ No projects available. Please contact your administrator. +

+ )} {/* MAIN */} @@ -164,11 +233,12 @@ export default function MeterManagement() {
@@ -177,7 +247,25 @@ export default function MeterManagement() { onClick={() => { if (!activeMeter) return; setEditingId(activeMeter.id); - setForm({ ...activeMeter }); + setForm({ + areaName: activeMeter.areaName, + accountNumber: activeMeter.accountNumber, + userName: activeMeter.userName, + userAddress: activeMeter.userAddress, + meterSN: activeMeter.meterSN, + meterName: activeMeter.meterName, + meterStatus: activeMeter.meterStatus, + protocolType: activeMeter.protocolType, + priceNo: activeMeter.priceNo, + priceName: activeMeter.priceName, + dmaPartition: activeMeter.dmaPartition, + supplyTypes: activeMeter.supplyTypes, + deviceID: activeMeter.deviceID, + deviceName: activeMeter.deviceName, + deviceType: activeMeter.deviceType, + usageAnalysisType: activeMeter.usageAnalysisType, + installedTime: activeMeter.installedTime, + }); setShowModal(true); }} disabled={!activeMeter} @@ -214,25 +302,31 @@ export default function MeterManagement() { {/* TABLE */} ( - {rowData.status} + {rowData.meterStatus} ), }, - { title: "Project", field: "project" }, - { title: "Created", field: "createdAt", type: "date" }, + { title: "Protocol Type", field: "protocolType" }, + { title: "Device Name", field: "deviceName" }, + { title: "Area Name", field: "areaName" }, + { title: "Installed Time", field: "installedTime", type: "date" }, ]} data={filtered} onRowClick={(_, rowData) => setActiveMeter(rowData as Meter)} @@ -248,55 +342,198 @@ export default function MeterManagement() { : "#FFFFFF", }), }} + localization={{ + body: { + emptyDataSourceMessage: loadingMeters + ? "Loading meters..." + : "No meters found. Click 'Add' to create your first meter.", + }, + }} />
{/* MODAL */} {showModal && (
-
+

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

- - setForm({ ...form, serialNumber: e.target.value }) - } - /> +
+ + setForm({ ...form, areaName: e.target.value })} + /> +
- +
+ + setForm({ ...form, accountNumber: e.target.value })} + /> +
- - setForm({ ...form, project: e.target.value }) - } - /> +
+ + setForm({ ...form, userName: e.target.value })} + /> +
- - setForm({ ...form, createdAt: e.target.value }) - } - /> +
+ + setForm({ ...form, userAddress: e.target.value })} + /> +
+ +
+ + setForm({ ...form, meterSN: e.target.value })} + /> +
+ +
+ + setForm({ ...form, meterName: e.target.value })} + /> +
+ +
+ + +
+ +
+ + setForm({ ...form, protocolType: e.target.value })} + /> +
+ +
+ + setForm({ ...form, priceNo: e.target.value })} + /> +
+ +
+ + setForm({ ...form, priceName: e.target.value })} + /> +
+ +
+ + setForm({ ...form, dmaPartition: e.target.value })} + /> +
+ +
+ + setForm({ ...form, supplyTypes: e.target.value })} + /> +
+ +
+ + setForm({ ...form, deviceID: e.target.value })} + /> +
+ +
+ + setForm({ ...form, deviceName: e.target.value })} + /> +
+ +
+ + setForm({ ...form, deviceType: e.target.value })} + /> +
+ +
+ + setForm({ ...form, usageAnalysisType: e.target.value })} + /> +
+ +
+ + setForm({ ...form, installedTime: e.target.value })} + /> +
diff --git a/src/pages/meters/meters.tapi.ts b/src/pages/meters/meters.tapi.ts deleted file mode 100644 index e69de29..0000000