From 854c499fe7bc75ab1b226cfdd269585c6342d61d Mon Sep 17 00:00:00 2001 From: Esteban Date: Wed, 17 Dec 2025 20:42:53 -0600 Subject: [PATCH 1/9] Projects api call to set different values in table component --- src/pages/concentrators/ConcentratorsPage.tsx | 64 +++++++---- src/pages/concentrators/concentrators.api.ts | 102 ++++++++++++++++++ 2 files changed, 148 insertions(+), 18 deletions(-) diff --git a/src/pages/concentrators/ConcentratorsPage.tsx b/src/pages/concentrators/ConcentratorsPage.tsx index ab9492d..5b9b768 100644 --- a/src/pages/concentrators/ConcentratorsPage.tsx +++ b/src/pages/concentrators/ConcentratorsPage.tsx @@ -1,6 +1,7 @@ -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 { fetchProjects } from "./concentrators.api"; /* ================= TYPES ================= */ interface Concentrator { @@ -27,21 +28,43 @@ export default function ConcentratorsPage() { project: "CESPT", }; - // Lista de proyectos disponibles - const allProjects = ["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"]; + const [allProjects, setAllProjects] = useState([]); + const [loadingProjects, setLoadingProjects] = useState(true); + + useEffect(() => { + const loadProjects = async () => { + try { + const projects = await fetchProjects(); + setAllProjects(projects); + } catch (error) { + console.error('Error loading projects:', error); + setAllProjects(["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"]); + } finally { + setLoadingProjects(false); + } + }; + + loadProjects(); + }, []); // 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] ); + const [selectedProject, setSelectedProject] = useState(""); + + useEffect(() => { + if (visibleProjects.length > 0 && !selectedProject) { + setSelectedProject(visibleProjects[0]); + } + }, [visibleProjects, selectedProject]); + const [concentrators, setConcentrators] = useState([ { id: 1, @@ -75,15 +98,15 @@ export default function ConcentratorsPage() { const [showModal, setShowModal] = useState(false); const [editingId, setEditingId] = useState(null); - const emptyConcentrator: Omit = { + const getEmptyConcentrator = (): Omit => ({ name: "", location: "", status: "ACTIVE", project: selectedProject, createdAt: new Date().toISOString().slice(0, 10), - }; + }); - const [form, setForm] = useState>(emptyConcentrator); + const [form, setForm] = useState>(getEmptyConcentrator()); /* ================= CRUD ================= */ const handleSave = () => { @@ -99,7 +122,7 @@ export default function ConcentratorsPage() { } setShowModal(false); setEditingId(null); - setForm({ ...emptyConcentrator, project: selectedProject }); + setForm({ ...getEmptyConcentrator(), project: selectedProject }); setActiveConcentrator(null); }; @@ -132,12 +155,17 @@ export default function ConcentratorsPage() { value={selectedProject} onChange={(e) => setSelectedProject(e.target.value)} className="w-full border px-3 py-2 rounded" + disabled={loadingProjects} > - {visibleProjects.map((proj) => ( - - ))} + {loadingProjects ? ( + + ) : ( + visibleProjects.map((proj) => ( + + )) + )} @@ -156,7 +184,7 @@ export default function ConcentratorsPage() {
+
+ + setForm({ ...form, "Operator": e.target.value })} + /> +
- setForm({ ...form, createdAt: e.target.value })} - /> +
+ + setForm({ ...form, "Instruction Manual": e.target.value })} + /> +
+ +
+ + +
+ +
+ + setForm({ ...form, "Installed Time": e.target.value })} + /> +
+ +
+ + setForm({ ...form, "Device Time": new Date(e.target.value).toISOString() })} + /> +
+ +
+ + setForm({ ...form, "Communication Time": new Date(e.target.value).toISOString() })} + /> +
diff --git a/src/pages/concentrators/concentrators.api.ts b/src/pages/concentrators/concentrators.api.ts index 1652397..268ecba 100644 --- a/src/pages/concentrators/concentrators.api.ts +++ b/src/pages/concentrators/concentrators.api.ts @@ -22,6 +22,36 @@ interface ProjectsResponse { nestedPrev?: string; } +export const createConcentrator = async (concentratorData: { + "Area Name": string; + "Device S/N": string; + "Device Name": string; + "Device Time": string; + "Device Status": string; + "Operator": string; + "Installed Time": string; + "Communication Time": string; + "Instruction Manual": string; +}): Promise => { + try { + // const response = await fetch('/api/v3/data/ppfu31vhv5gf6i0/mqqvi3woqdw5ziq/records', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ fields: concentratorData }), + // }); + // if (!response.ok) { + // throw new Error('Failed to create concentrator'); + // } + + console.log('Creating concentrator with data:', concentratorData); + } catch (error) { + console.error('Error creating concentrator:', error); + throw error; + } +}; + export const fetchProjects = async (): Promise => { try { // const response = await fetch('/api/v3/data/ppfu31vhv5gf6i0/m05u6wpquvdbv3c/records'); From 0f0328f41bce10389865509d6d5f5ab0e7e549e3 Mon Sep 17 00:00:00 2001 From: Marlene-Angel <139193696+Marlene-Angel@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:27:56 -0800 Subject: [PATCH 3/9] Se agrego la interfaz ProjectsPage --- src/App.tsx | 25 +- src/components/layout/Sidebar.tsx | 113 ++++----- src/pages/projects/ProjectsPage.tsx | 366 ++++++++++++++++++++++++++++ vite.config.ts | 7 + 4 files changed, 434 insertions(+), 77 deletions(-) create mode 100644 src/pages/projects/ProjectsPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 697b492..183cf0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,22 +5,33 @@ import TopMenu from "./components/layout/TopMenu"; import Home from "./pages/Home"; import MetersPage from "./pages/meters/MeterPage"; import ConcentratorsPage from "./pages/concentrators/ConcentratorsPage"; -import UsersPage from "./pages/UsersPage"; // nueva página -import RolesPage from "./pages/RolesPage"; // nueva página +import ProjectsPage from "./pages/projects/ProjectsPage"; +import UsersPage from "./pages/UsersPage"; +import RolesPage from "./pages/RolesPage"; + +export type Page = + | "home" + | "projects" + | "meters" + | "concentrators" + | "users" + | "roles"; export default function App() { - const [page, setPage] = useState("home"); + const [page, setPage] = useState("home"); const renderPage = () => { switch (page) { + case "projects": + return ; case "meters": return ; case "concentrators": return ; case "users": - return ; // nueva + return ; case "roles": - return ; // nueva + return ; case "home": default: return ; @@ -32,7 +43,9 @@ export default function App() {
-
{renderPage()}
+
+ {renderPage()} +
); diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 5a3ac72..5e61852 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -7,17 +7,16 @@ import { ExpandLess, Menu, People, - Key } from "@mui/icons-material"; +import { Page } from "../../App"; interface SidebarProps { - setPage: (page: string) => void; + setPage: (page: Page) => void; } export default function Sidebar({ setPage }: SidebarProps) { const [systemOpen, setSystemOpen] = useState(true); - const [waterOpen, setWaterOpen] = useState(true); - const [usersOpen, setUsersOpen] = useState(true); // Nuevo + const [usersOpen, setUsersOpen] = useState(true); const [pinned, setPinned] = useState(false); const [hovered, setHovered] = useState(false); @@ -51,7 +50,6 @@ export default function Sidebar({ setPage }: SidebarProps) { {/* MENU */}
    - {/* DASHBOARD */}
  • +
  • +
  • +
)} - {/* WATER METER SYSTEM + {/* USERS MANAGEMENT */}
  • - {isExpanded && waterOpen && ( + {isExpanded && usersOpen && (
      - {[ - ["water-install", "Water Meter Installation"], - ["device-install", "Device Installation"], - ["meter-management", "Meter Management"], - ["device-management", "Device Management"], - ["data-monitoring", "Data Monitoring"], - ["data-query", "Data Query"], - ].map(([key, label]) => ( -
    • - -
    • - ))} +
    • + +
    • +
    • + +
    )}
  • - *} - - {/* SYSTEM USERS */} -
  • - - - {isExpanded && usersOpen && ( -
      -
    • - -
    • -
    • - -
    • -
    - )} -
  • - -
    diff --git a/src/pages/projects/ProjectsPage.tsx b/src/pages/projects/ProjectsPage.tsx new file mode 100644 index 0000000..e362b72 --- /dev/null +++ b/src/pages/projects/ProjectsPage.tsx @@ -0,0 +1,366 @@ +import { useEffect, useState } from "react"; +import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; +import MaterialTable from "@material-table/core"; + +/* ================= TYPES ================= */ +interface Project { + id: string; + areaName: string; + deviceSN: string; + deviceName: string; + deviceType: string; + deviceStatus: "ACTIVE" | "INACTIVE"; + operator: string; + installedTime: string; + communicationTime: string; + instructionManual: string; +} + +/* ================= MOCK DATA ================= */ +const mockProjects: Project[] = [ + { + id: "1", + areaName: "Zona Norte", + deviceSN: "SN-001", + deviceName: "Sensor Alpha", + deviceType: "Flow Meter", + deviceStatus: "ACTIVE", + operator: "Juan Pérez", + installedTime: "2024-01-10", + communicationTime: "2024-01-11", + instructionManual: "Manual Alpha", + }, + { + id: "2", + areaName: "Zona Centro", + deviceSN: "SN-002", + deviceName: "Sensor Beta", + deviceType: "Pressure Meter", + deviceStatus: "INACTIVE", + operator: "María López", + installedTime: "2024-02-05", + communicationTime: "2024-02-06", + instructionManual: "Manual Beta", + }, + { + id: "3", + areaName: "Zona Sur", + deviceSN: "SN-003", + deviceName: "Sensor Gamma", + deviceType: "Flow Meter", + deviceStatus: "ACTIVE", + operator: "Carlos Ruiz", + installedTime: "2024-03-01", + communicationTime: "2024-03-02", + instructionManual: "Manual Gamma", + }, +]; + +/* ================= API ================= */ +const API_URL = "/api/v2/tables/m05u6wpquvdbv3c/records"; + +const fetchProjects = async (): Promise => { + const res = await fetch(API_URL); + const data = await res.json(); + + return data.records.map((r: any) => ({ + id: r.id, + areaName: r.fields["Area Name"] ?? "", + deviceSN: r.fields["Device S/N"] ?? "", + deviceName: r.fields["Device Name"] ?? "", + deviceType: r.fields["Device Type"] ?? "", + deviceStatus: + r.fields["Device Status"] === "INACTIVE" + ? "INACTIVE" + : "ACTIVE", + operator: r.fields["Operator"] ?? "", + installedTime: r.fields["Installed Time"] ?? "", + communicationTime: r.fields["Communication Time"] ?? "", + instructionManual: r.fields["Instruction Manual"] ?? "", + })); +}; + +/* ================= COMPONENT ================= */ +export default function ProjectsPage() { + const [projects, setProjects] = useState([]); + const [activeProject, setActiveProject] = + useState(null); + const [search, setSearch] = useState(""); + + const [showModal, setShowModal] = useState(false); + const [editingId, setEditingId] = + useState(null); + + const emptyProject: Omit = { + areaName: "", + deviceSN: "", + deviceName: "", + deviceType: "", + deviceStatus: "ACTIVE", + operator: "", + installedTime: "", + communicationTime: "", + instructionManual: "", + }; + + const [form, setForm] = + useState>(emptyProject); + + /* ================= LOAD ================= */ + const loadProjects = async () => { + try { + const data = await fetchProjects(); + if (data.length === 0) { + setProjects(mockProjects); + } else { + setProjects(data); + } + } catch { + setProjects(mockProjects); + } + }; + + useEffect(() => { + loadProjects(); + }, []); + + /* ================= CRUD ================= */ + const handleSave = () => { + if (editingId) { + setProjects((prev) => + prev.map((p) => + p.id === editingId + ? { ...p, ...form } + : p + ) + ); + } else { + setProjects((prev) => [ + ...prev, + { id: Date.now().toString(), ...form }, + ]); + } + + setShowModal(false); + setEditingId(null); + setForm(emptyProject); + setActiveProject(null); + }; + + const handleDelete = () => { + if (!activeProject) return; + setProjects((prev) => + prev.filter( + (p) => p.id !== activeProject.id + ) + ); + setActiveProject(null); + }; + + /* ================= FILTER ================= */ + const filtered = projects.filter((p) => + `${p.areaName} ${p.deviceName} ${p.deviceSN}` + .toLowerCase() + .includes(search.toLowerCase()) + ); + + /* ================= UI ================= */ + return ( +
    +
    + {/* HEADER */} +
    +
    +

    + Project Management +

    +

    + Projects registered +

    +
    + +
    + + + + + + + +
    +
    + + {/* SEARCH */} + setSearch(e.target.value)} + /> + + {/* TABLE */} + ( + + {rowData.deviceStatus} + + ), + }, + { title: "Operator", field: "operator" }, + { title: "Installed Time", field: "installedTime" }, + { title: "Communication Time", field: "communicationTime" }, + { title: "Instruction Manual", field: "instructionManual" }, + ]} + data={filtered} + onRowClick={(_, rowData) => + setActiveProject(rowData as Project) + } + options={{ + search: false, + paging: true, + sorting: true, + rowStyle: (rowData) => ({ + backgroundColor: + activeProject?.id === + (rowData as Project).id + ? "#EEF2FF" + : "#FFFFFF", + }), + }} + /> +
    + + {/* MODAL */} + {showModal && ( +
    +
    +

    + {editingId ? "Edit Project" : "Add Project"} +

    + + setForm({ ...form, areaName: e.target.value })} /> + + setForm({ ...form, deviceSN: e.target.value })} /> + + setForm({ ...form, deviceName: e.target.value })} /> + + setForm({ ...form, deviceType: e.target.value })} /> + + setForm({ ...form, operator: e.target.value })} /> + + setForm({ ...form, installedTime: e.target.value })} /> + + setForm({ ...form, communicationTime: e.target.value })} /> + + setForm({ ...form, instructionManual: e.target.value })} /> + + + +
    + + +
    +
    +
    + )} +
    + ); +} diff --git a/vite.config.ts b/vite.config.ts index a1c3892..437d9be 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,4 +5,11 @@ import tailwindcss from "@tailwindcss/vite" // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(),tailwindcss()], + + server: { + host: '0.0.0.0', // Esto permite que el servidor escuche en todas las IP disponibles + port: 5173, // Puerto por defecto de Vite (puedes cambiarlo si lo deseas) + }, + }) + From 82048045112bc7c62dcf6a78d0e9fd04b5e903c4 Mon Sep 17 00:00:00 2001 From: Esteban Date: Thu, 18 Dec 2025 23:14:16 -0600 Subject: [PATCH 4/9] Project list and crud logic to endpoints --- .env.example | 2 + .gitignore | 7 + src/pages/projects/ProjectsPage.tsx | 406 ++++++++++++++++++++-------- 3 files changed, 296 insertions(+), 119 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8ae4c16 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VITE_API_BASE_URL=domain_url +VITE_API_TOKEN=api_token diff --git a/.gitignore b/.gitignore index a547bf3..d305d04 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,13 @@ dist dist-ssr *.local +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/src/pages/projects/ProjectsPage.tsx b/src/pages/projects/ProjectsPage.tsx index e362b72..87283dc 100644 --- a/src/pages/projects/ProjectsPage.tsx +++ b/src/pages/projects/ProjectsPage.tsx @@ -16,63 +16,46 @@ interface Project { instructionManual: string; } -/* ================= MOCK DATA ================= */ -const mockProjects: Project[] = [ - { - id: "1", - areaName: "Zona Norte", - deviceSN: "SN-001", - deviceName: "Sensor Alpha", - deviceType: "Flow Meter", - deviceStatus: "ACTIVE", - operator: "Juan Pérez", - installedTime: "2024-01-10", - communicationTime: "2024-01-11", - instructionManual: "Manual Alpha", - }, - { - id: "2", - areaName: "Zona Centro", - deviceSN: "SN-002", - deviceName: "Sensor Beta", - deviceType: "Pressure Meter", - deviceStatus: "INACTIVE", - operator: "María López", - installedTime: "2024-02-05", - communicationTime: "2024-02-06", - instructionManual: "Manual Beta", - }, - { - id: "3", - areaName: "Zona Sur", - deviceSN: "SN-003", - deviceName: "Sensor Gamma", - deviceType: "Flow Meter", - deviceStatus: "ACTIVE", - operator: "Carlos Ruiz", - installedTime: "2024-03-01", - communicationTime: "2024-03-02", - instructionManual: "Manual Gamma", - }, -]; - /* ================= API ================= */ -const API_URL = "/api/v2/tables/m05u6wpquvdbv3c/records"; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +const API_URL = `${API_BASE_URL}/api/v3/data/ppfu31vhv5gf6i0/m05u6wpquvdbv3c/records`; +const API_TOKEN = import.meta.env.VITE_API_TOKEN; + +const getAuthHeaders = () => ({ + Authorization: `Bearer ${API_TOKEN}`, + "Content-Type": "application/json", +}); + +interface ProjectApiRecord { + id: number; + fields: { + "Area name"?: string; + "Device S/N"?: string; + "Device Name"?: string; + "Device Type"?: string; + "Device Status"?: string; + Operator?: string; + "Installed Time"?: string; + "Communication Time"?: string; + "Instruction Manual"?: string | null; + }; +} const fetchProjects = async (): Promise => { - const res = await fetch(API_URL); + const res = await fetch(API_URL, { + method: "GET", + headers: getAuthHeaders(), + }); const data = await res.json(); - return data.records.map((r: any) => ({ - id: r.id, - areaName: r.fields["Area Name"] ?? "", + return data.records.map((r: ProjectApiRecord) => ({ + id: r.id.toString(), + areaName: r.fields["Area name"] ?? "", deviceSN: r.fields["Device S/N"] ?? "", deviceName: r.fields["Device Name"] ?? "", deviceType: r.fields["Device Type"] ?? "", deviceStatus: - r.fields["Device Status"] === "INACTIVE" - ? "INACTIVE" - : "ACTIVE", + r.fields["Device Status"] === "Installed" ? "ACTIVE" : "INACTIVE", operator: r.fields["Operator"] ?? "", installedTime: r.fields["Installed Time"] ?? "", communicationTime: r.fields["Communication Time"] ?? "", @@ -83,13 +66,12 @@ const fetchProjects = async (): Promise => { /* ================= COMPONENT ================= */ export default function ProjectsPage() { const [projects, setProjects] = useState([]); - const [activeProject, setActiveProject] = - useState(null); + const [loading, setLoading] = useState(true); + const [activeProject, setActiveProject] = useState(null); const [search, setSearch] = useState(""); const [showModal, setShowModal] = useState(false); - const [editingId, setEditingId] = - useState(null); + const [editingId, setEditingId] = useState(null); const emptyProject: Omit = { areaName: "", @@ -103,20 +85,19 @@ export default function ProjectsPage() { instructionManual: "", }; - const [form, setForm] = - useState>(emptyProject); + const [form, setForm] = useState>(emptyProject); /* ================= LOAD ================= */ const loadProjects = async () => { + setLoading(true); try { const data = await fetchProjects(); - if (data.length === 0) { - setProjects(mockProjects); - } else { - setProjects(data); - } - } catch { - setProjects(mockProjects); + setProjects(data); + } catch (error) { + console.error("Error loading projects:", error); + setProjects([]); + } finally { + setLoading(false); } }; @@ -125,36 +106,197 @@ export default function ProjectsPage() { }, []); /* ================= CRUD ================= */ - const handleSave = () => { - if (editingId) { - setProjects((prev) => - prev.map((p) => - p.id === editingId - ? { ...p, ...form } - : p - ) + const createProject = async ( + projectData: Omit + ): Promise => { + const res = await fetch(API_URL, { + method: "POST", + headers: getAuthHeaders(), + body: JSON.stringify({ + fields: { + "Area name": projectData.areaName, + "Device S/N": projectData.deviceSN, + "Device Name": projectData.deviceName, + "Device Type": projectData.deviceType, + "Device Status": + projectData.deviceStatus === "ACTIVE" ? "Installed" : "Inactive", + Operator: projectData.operator, + "Installed Time": projectData.installedTime, + "Communication Time": projectData.communicationTime, + "Instruction Manual": projectData.instructionManual, + }, + }), + }); + + if (!res.ok) { + throw new Error( + `Failed to create project: ${res.status} ${res.statusText}` ); - } else { - setProjects((prev) => [ - ...prev, - { id: Date.now().toString(), ...form }, - ]); } - setShowModal(false); - setEditingId(null); - setForm(emptyProject); - setActiveProject(null); + const data = await res.json(); + + const createdRecord = data.records?.[0]; + if (!createdRecord) { + throw new Error("Invalid response format: no record returned"); + } + + return { + id: createdRecord.id.toString(), + areaName: createdRecord.fields["Area name"] ?? projectData.areaName, + deviceSN: createdRecord.fields["Device S/N"] ?? projectData.deviceSN, + deviceName: createdRecord.fields["Device Name"] ?? projectData.deviceName, + deviceType: createdRecord.fields["Device Type"] ?? projectData.deviceType, + deviceStatus: + createdRecord.fields["Device Status"] === "Installed" + ? "ACTIVE" + : "INACTIVE", + operator: createdRecord.fields["Operator"] ?? projectData.operator, + installedTime: + createdRecord.fields["Installed Time"] ?? projectData.installedTime, + communicationTime: + createdRecord.fields["Communication Time"] ?? + projectData.communicationTime, + instructionManual: + createdRecord.fields["Instruction Manual"] ?? + projectData.instructionManual, + }; }; - const handleDelete = () => { + const updateProject = async ( + id: string, + projectData: Omit + ): Promise => { + const res = await fetch(API_URL, { + method: "PATCH", + headers: getAuthHeaders(), + body: JSON.stringify({ + id: parseInt(id), + fields: { + "Area name": projectData.areaName, + "Device S/N": projectData.deviceSN, + "Device Name": projectData.deviceName, + "Device Type": projectData.deviceType, + "Device Status": + projectData.deviceStatus === "ACTIVE" ? "Installed" : "Inactive", + Operator: projectData.operator, + "Installed Time": projectData.installedTime, + "Communication Time": projectData.communicationTime, + "Instruction Manual": projectData.instructionManual, + }, + }), + }); + + if (!res.ok) { + if (res.status === 400) { + const errorData = await res.json(); + throw new Error( + `Bad Request: ${errorData.msg || "Invalid data provided"}` + ); + } + throw new Error( + `Failed to update project: ${res.status} ${res.statusText}` + ); + } + + const data = await res.json(); + + const updatedRecord = data.records?.[0]; + if (!updatedRecord) { + throw new Error("Invalid response format: no record returned"); + } + + return { + id: updatedRecord.id.toString(), + areaName: updatedRecord.fields["Area name"] ?? projectData.areaName, + deviceSN: updatedRecord.fields["Device S/N"] ?? projectData.deviceSN, + deviceName: updatedRecord.fields["Device Name"] ?? projectData.deviceName, + deviceType: updatedRecord.fields["Device Type"] ?? projectData.deviceType, + deviceStatus: + updatedRecord.fields["Device Status"] === "Installed" + ? "ACTIVE" + : "INACTIVE", + operator: updatedRecord.fields["Operator"] ?? projectData.operator, + installedTime: + updatedRecord.fields["Installed Time"] ?? projectData.installedTime, + communicationTime: + updatedRecord.fields["Communication Time"] ?? + projectData.communicationTime, + instructionManual: + updatedRecord.fields["Instruction Manual"] ?? + projectData.instructionManual, + }; + }; + + const deleteProject = async (id: string): Promise => { + const res = await fetch(API_URL, { + method: "DELETE", + headers: getAuthHeaders(), + body: JSON.stringify({ + id: id, + }), + }); + + if (!res.ok) { + if (res.status === 400) { + const errorData = await res.json(); + throw new Error( + `Bad Request: ${errorData.msg || "Invalid data provided"}` + ); + } + throw new Error( + `Failed to delete project: ${res.status} ${res.statusText}` + ); + } + }; + + const handleSave = async () => { + try { + if (editingId) { + const updatedProject = await updateProject(editingId, form); + setProjects((prev) => + prev.map((p) => (p.id === editingId ? updatedProject : p)) + ); + } else { + const newProject = await createProject(form); + setProjects((prev) => [...prev, newProject]); + } + + setShowModal(false); + setEditingId(null); + setForm(emptyProject); + setActiveProject(null); + } catch (error) { + console.error("Error saving project:", error); + alert( + `Error saving project: ${ + error instanceof Error ? error.message : "Please try again." + }` + ); + } + }; + + const handleDelete = async () => { if (!activeProject) return; - setProjects((prev) => - prev.filter( - (p) => p.id !== activeProject.id - ) + + const confirmDelete = window.confirm( + `Are you sure you want to delete the project "${activeProject.deviceName}"?` ); - setActiveProject(null); + + if (!confirmDelete) return; + + try { + await deleteProject(activeProject.id); + setProjects((prev) => prev.filter((p) => p.id !== activeProject.id)); + setActiveProject(null); + } catch (error) { + console.error("Error deleting project:", error); + alert( + `Error deleting project: ${ + error instanceof Error ? error.message : "Please try again." + }` + ); + } }; /* ================= FILTER ================= */ @@ -172,17 +314,12 @@ export default function ProjectsPage() {
    -

    - Project Management -

    -

    - Projects registered -

    +

    Project Management

    +

    Projects registered

    @@ -248,6 +385,7 @@ export default function ProjectsPage() { {/* TABLE */} - setActiveProject(rowData as Project) - } + onRowClick={(_, rowData) => setActiveProject(rowData as Project)} options={{ search: false, paging: true, sorting: true, rowStyle: (rowData) => ({ backgroundColor: - activeProject?.id === - (rowData as Project).id + activeProject?.id === (rowData as Project).id ? "#EEF2FF" : "#FFFFFF", }), }} + localization={{ + body: { + emptyDataSourceMessage: loading + ? "Loading projects..." + : "No projects found. Click 'Add' to create your first project.", + }, + }} />
    @@ -300,46 +442,74 @@ export default function ProjectsPage() { {editingId ? "Edit Project" : "Add Project"} - setForm({ ...form, areaName: e.target.value })} /> + onChange={(e) => setForm({ ...form, areaName: e.target.value })} + /> - setForm({ ...form, deviceSN: e.target.value })} /> + onChange={(e) => setForm({ ...form, deviceSN: e.target.value })} + /> - setForm({ ...form, deviceName: e.target.value })} /> + onChange={(e) => setForm({ ...form, deviceName: e.target.value })} + /> - setForm({ ...form, deviceType: e.target.value })} /> + onChange={(e) => setForm({ ...form, deviceType: e.target.value })} + /> - setForm({ ...form, operator: e.target.value })} /> + onChange={(e) => setForm({ ...form, operator: e.target.value })} + /> - setForm({ ...form, installedTime: e.target.value })} /> + onChange={(e) => + setForm({ ...form, installedTime: e.target.value }) + } + /> - setForm({ ...form, communicationTime: e.target.value })} /> + onChange={(e) => + setForm({ ...form, communicationTime: e.target.value }) + } + /> - setForm({ ...form, instructionManual: e.target.value })} /> + onChange={(e) => + setForm({ ...form, instructionManual: e.target.value }) + } + />
    - +
    {/* MAIN */} @@ -213,7 +222,8 @@ export default function ConcentratorsPage() { setEditingSerial(null); setShowModal(true); }} - className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg" + disabled={!selectedProject || visibleProjects.length === 0} + className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg disabled:opacity-50 disabled:cursor-not-allowed" > Add diff --git a/src/pages/concentrators/concentrators.api.ts b/src/pages/concentrators/concentrators.api.ts index 268ecba..abac5f7 100644 --- a/src/pages/concentrators/concentrators.api.ts +++ b/src/pages/concentrators/concentrators.api.ts @@ -1,27 +1,3 @@ -interface ProjectRecord { - id: string; - fields: { - "Project ID": string; - "Area name": string; - "Device S/N": string; - "Device Name": string; - "Device Type": string; - "Device Status": string; - "Operator": string; - "Installed Time": string; - "Communication Time": string; - "Instruction Manual": string; - }; -} - -interface ProjectsResponse { - records: ProjectRecord[]; - next?: string; - prev?: string; - nestedNext?: string; - nestedPrev?: string; -} - export const createConcentrator = async (concentratorData: { "Area Name": string; "Device S/N": string; @@ -51,82 +27,3 @@ export const createConcentrator = async (concentratorData: { throw error; } }; - -export const fetchProjects = async (): Promise => { - try { - // const response = await fetch('/api/v3/data/ppfu31vhv5gf6i0/m05u6wpquvdbv3c/records'); - // const data: ProjectsResponse = await response.json(); - - const dummyResponse: ProjectsResponse = { - records: [ - { - id: "rec1", - fields: { - "Project ID": "1", - "Area name": "GRH (PADRE)", - "Device S/N": "SN001", - "Device Name": "Device 1", - "Device Type": "Type A", - "Device Status": "Active", - "Operator": "Op1", - "Installed Time": "2023-01-01", - "Communication Time": "2023-01-02", - "Instruction Manual": "Manual 1" - } - }, - { - id: "rec2", - fields: { - "Project ID": "2", - "Area name": "CESPT", - "Device S/N": "SN002", - "Device Name": "Device 2", - "Device Type": "Type B", - "Device Status": "Active", - "Operator": "Op2", - "Installed Time": "2023-01-02", - "Communication Time": "2023-01-03", - "Instruction Manual": "Manual 2" - } - }, - { - id: "rec3", - fields: { - "Project ID": "3", - "Area name": "Proyecto A", - "Device S/N": "SN003", - "Device Name": "Device 3", - "Device Type": "Type C", - "Device Status": "Inactive", - "Operator": "Op3", - "Installed Time": "2023-01-03", - "Communication Time": "2023-01-04", - "Instruction Manual": "Manual 3" - } - }, - { - id: "rec4", - fields: { - "Project ID": "4", - "Area name": "Proyecto B", - "Device S/N": "SN004", - "Device Name": "Device 4", - "Device Type": "Type D", - "Device Status": "Active", - "Operator": "Op4", - "Installed Time": "2023-01-04", - "Communication Time": "2023-01-05", - "Instruction Manual": "Manual 4" - } - } - ] - }; - - const projectNames = [...new Set(dummyResponse.records.map(record => record.fields["Area name"]))]; - - return projectNames; - } catch (error) { - console.error('Error fetching projects:', error); - return ["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"]; - } -}; diff --git a/src/pages/projects/ProjectsPage.tsx b/src/pages/projects/ProjectsPage.tsx index 87283dc..c366004 100644 --- a/src/pages/projects/ProjectsPage.tsx +++ b/src/pages/projects/ProjectsPage.tsx @@ -1,67 +1,13 @@ import { useEffect, useState } from "react"; import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; import MaterialTable from "@material-table/core"; - -/* ================= TYPES ================= */ -interface Project { - id: string; - areaName: string; - deviceSN: string; - deviceName: string; - deviceType: string; - deviceStatus: "ACTIVE" | "INACTIVE"; - operator: string; - installedTime: string; - communicationTime: string; - instructionManual: string; -} - -/* ================= API ================= */ -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; -const API_URL = `${API_BASE_URL}/api/v3/data/ppfu31vhv5gf6i0/m05u6wpquvdbv3c/records`; -const API_TOKEN = import.meta.env.VITE_API_TOKEN; - -const getAuthHeaders = () => ({ - Authorization: `Bearer ${API_TOKEN}`, - "Content-Type": "application/json", -}); - -interface ProjectApiRecord { - id: number; - fields: { - "Area name"?: string; - "Device S/N"?: string; - "Device Name"?: string; - "Device Type"?: string; - "Device Status"?: string; - Operator?: string; - "Installed Time"?: string; - "Communication Time"?: string; - "Instruction Manual"?: string | null; - }; -} - -const fetchProjects = async (): Promise => { - const res = await fetch(API_URL, { - method: "GET", - headers: getAuthHeaders(), - }); - const data = await res.json(); - - return data.records.map((r: ProjectApiRecord) => ({ - id: r.id.toString(), - areaName: r.fields["Area name"] ?? "", - deviceSN: r.fields["Device S/N"] ?? "", - deviceName: r.fields["Device Name"] ?? "", - deviceType: r.fields["Device Type"] ?? "", - deviceStatus: - r.fields["Device Status"] === "Installed" ? "ACTIVE" : "INACTIVE", - operator: r.fields["Operator"] ?? "", - installedTime: r.fields["Installed Time"] ?? "", - communicationTime: r.fields["Communication Time"] ?? "", - instructionManual: r.fields["Instruction Manual"] ?? "", - })); -}; +import { + Project, + fetchProjects, + createProject as apiCreateProject, + updateProject as apiUpdateProject, + deleteProject as apiDeleteProject, +} from "../../api/projects"; /* ================= COMPONENT ================= */ export default function ProjectsPage() { @@ -105,160 +51,16 @@ export default function ProjectsPage() { loadProjects(); }, []); - /* ================= CRUD ================= */ - const createProject = async ( - projectData: Omit - ): Promise => { - const res = await fetch(API_URL, { - method: "POST", - headers: getAuthHeaders(), - body: JSON.stringify({ - fields: { - "Area name": projectData.areaName, - "Device S/N": projectData.deviceSN, - "Device Name": projectData.deviceName, - "Device Type": projectData.deviceType, - "Device Status": - projectData.deviceStatus === "ACTIVE" ? "Installed" : "Inactive", - Operator: projectData.operator, - "Installed Time": projectData.installedTime, - "Communication Time": projectData.communicationTime, - "Instruction Manual": projectData.instructionManual, - }, - }), - }); - - if (!res.ok) { - throw new Error( - `Failed to create project: ${res.status} ${res.statusText}` - ); - } - - const data = await res.json(); - - const createdRecord = data.records?.[0]; - if (!createdRecord) { - throw new Error("Invalid response format: no record returned"); - } - - return { - id: createdRecord.id.toString(), - areaName: createdRecord.fields["Area name"] ?? projectData.areaName, - deviceSN: createdRecord.fields["Device S/N"] ?? projectData.deviceSN, - deviceName: createdRecord.fields["Device Name"] ?? projectData.deviceName, - deviceType: createdRecord.fields["Device Type"] ?? projectData.deviceType, - deviceStatus: - createdRecord.fields["Device Status"] === "Installed" - ? "ACTIVE" - : "INACTIVE", - operator: createdRecord.fields["Operator"] ?? projectData.operator, - installedTime: - createdRecord.fields["Installed Time"] ?? projectData.installedTime, - communicationTime: - createdRecord.fields["Communication Time"] ?? - projectData.communicationTime, - instructionManual: - createdRecord.fields["Instruction Manual"] ?? - projectData.instructionManual, - }; - }; - - const updateProject = async ( - id: string, - projectData: Omit - ): Promise => { - const res = await fetch(API_URL, { - method: "PATCH", - headers: getAuthHeaders(), - body: JSON.stringify({ - id: parseInt(id), - fields: { - "Area name": projectData.areaName, - "Device S/N": projectData.deviceSN, - "Device Name": projectData.deviceName, - "Device Type": projectData.deviceType, - "Device Status": - projectData.deviceStatus === "ACTIVE" ? "Installed" : "Inactive", - Operator: projectData.operator, - "Installed Time": projectData.installedTime, - "Communication Time": projectData.communicationTime, - "Instruction Manual": projectData.instructionManual, - }, - }), - }); - - if (!res.ok) { - if (res.status === 400) { - const errorData = await res.json(); - throw new Error( - `Bad Request: ${errorData.msg || "Invalid data provided"}` - ); - } - throw new Error( - `Failed to update project: ${res.status} ${res.statusText}` - ); - } - - const data = await res.json(); - - const updatedRecord = data.records?.[0]; - if (!updatedRecord) { - throw new Error("Invalid response format: no record returned"); - } - - return { - id: updatedRecord.id.toString(), - areaName: updatedRecord.fields["Area name"] ?? projectData.areaName, - deviceSN: updatedRecord.fields["Device S/N"] ?? projectData.deviceSN, - deviceName: updatedRecord.fields["Device Name"] ?? projectData.deviceName, - deviceType: updatedRecord.fields["Device Type"] ?? projectData.deviceType, - deviceStatus: - updatedRecord.fields["Device Status"] === "Installed" - ? "ACTIVE" - : "INACTIVE", - operator: updatedRecord.fields["Operator"] ?? projectData.operator, - installedTime: - updatedRecord.fields["Installed Time"] ?? projectData.installedTime, - communicationTime: - updatedRecord.fields["Communication Time"] ?? - projectData.communicationTime, - instructionManual: - updatedRecord.fields["Instruction Manual"] ?? - projectData.instructionManual, - }; - }; - - const deleteProject = async (id: string): Promise => { - const res = await fetch(API_URL, { - method: "DELETE", - headers: getAuthHeaders(), - body: JSON.stringify({ - id: id, - }), - }); - - if (!res.ok) { - if (res.status === 400) { - const errorData = await res.json(); - throw new Error( - `Bad Request: ${errorData.msg || "Invalid data provided"}` - ); - } - throw new Error( - `Failed to delete project: ${res.status} ${res.statusText}` - ); - } - }; const handleSave = async () => { try { if (editingId) { - const updatedProject = await updateProject(editingId, form); + const updatedProject = await apiUpdateProject(editingId, form); setProjects((prev) => prev.map((p) => (p.id === editingId ? updatedProject : p)) ); } else { - const newProject = await createProject(form); + const newProject = await apiCreateProject(form); setProjects((prev) => [...prev, newProject]); } @@ -286,7 +88,7 @@ export default function ProjectsPage() { if (!confirmDelete) return; try { - await deleteProject(activeProject.id); + await apiDeleteProject(activeProject.id); setProjects((prev) => prev.filter((p) => p.id !== activeProject.id)); setActiveProject(null); } catch (error) { From c4d568553a1a33ae4e1c2a5c14030629e8e56ae5 Mon Sep 17 00:00:00 2001 From: Esteban Date: Fri, 19 Dec 2025 00:10:05 -0600 Subject: [PATCH 6/9] Concentrator list and CRUD logic --- src/api/concentrators.ts | 206 ++++++++++++++++++ src/api/projects.ts | 2 +- src/pages/concentrators/ConcentratorsPage.tsx | 143 ++++++------ src/pages/concentrators/concentrators.api.ts | 29 --- 4 files changed, 287 insertions(+), 93 deletions(-) create mode 100644 src/api/concentrators.ts delete mode 100644 src/pages/concentrators/concentrators.api.ts diff --git a/src/api/concentrators.ts b/src/api/concentrators.ts new file mode 100644 index 0000000..5f8034a --- /dev/null +++ b/src/api/concentrators.ts @@ -0,0 +1,206 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +export const CONCENTRATORS_API_URL = `${API_BASE_URL}/api/v3/data/ppfu31vhv5gf6i0/mqqvi3woqdw5ziq/records`; +const API_TOKEN = import.meta.env.VITE_API_TOKEN; + +const getAuthHeaders = () => ({ + Authorization: `Bearer ${API_TOKEN}`, + "Content-Type": "application/json", +}); + +export interface ConcentratorRecord { + id: string; + fields: { + "Area Name": string; + "Device S/N": string; + "Device Name": string; + "Device Time": string; + "Device Status": string; + "Operator": string; + "Installed Time": string; + "Communication Time": string; + "Instruction Manual": string; + }; +} + +export interface ConcentratorsResponse { + records: ConcentratorRecord[]; + next?: string; + prev?: string; + nestedNext?: string; + nestedPrev?: string; +} + +export interface Concentrator { + id: string; + "Area Name": string; + "Device S/N": string; + "Device Name": string; + "Device Time": string; + "Device Status": string; + "Operator": string; + "Installed Time": string; + "Communication Time": string; + "Instruction Manual": string; +} + +export const fetchConcentrators = async (): Promise => { + try { + const response = await fetch(CONCENTRATORS_API_URL, { + method: "GET", + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to fetch concentrators"); + } + + const data: ConcentratorsResponse = await response.json(); + + return data.records.map((r: ConcentratorRecord) => ({ + id: r.id, + "Area Name": r.fields["Area Name"] || "", + "Device S/N": r.fields["Device S/N"] || "", + "Device Name": r.fields["Device Name"] || "", + "Device Time": r.fields["Device Time"] || "", + "Device Status": r.fields["Device Status"] || "", + "Operator": r.fields["Operator"] || "", + "Installed Time": r.fields["Installed Time"] || "", + "Communication Time": r.fields["Communication Time"] || "", + "Instruction Manual": r.fields["Instruction Manual"] || "", + })); + } catch (error) { + console.error("Error fetching concentrators:", error); + throw error; + } +}; + +export const createConcentrator = async ( + concentratorData: Omit +): Promise => { + try { + const response = await fetch(CONCENTRATORS_API_URL, { + method: "POST", + headers: getAuthHeaders(), + body: JSON.stringify({ + fields: { + "Area Name": concentratorData["Area Name"], + "Device S/N": concentratorData["Device S/N"], + "Device Name": concentratorData["Device Name"], + "Device Time": concentratorData["Device Time"], + "Device Status": concentratorData["Device Status"], + "Operator": concentratorData["Operator"], + "Installed Time": concentratorData["Installed Time"], + "Communication Time": concentratorData["Communication Time"], + "Instruction Manual": concentratorData["Instruction Manual"], + }, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to create concentrator: ${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, + "Area Name": createdRecord.fields["Area Name"] || concentratorData["Area Name"], + "Device S/N": createdRecord.fields["Device S/N"] || concentratorData["Device S/N"], + "Device Name": createdRecord.fields["Device Name"] || concentratorData["Device Name"], + "Device Time": createdRecord.fields["Device Time"] || concentratorData["Device Time"], + "Device Status": createdRecord.fields["Device Status"] || concentratorData["Device Status"], + "Operator": createdRecord.fields["Operator"] || concentratorData["Operator"], + "Installed Time": createdRecord.fields["Installed Time"] || concentratorData["Installed Time"], + "Communication Time": createdRecord.fields["Communication Time"] || concentratorData["Communication Time"], + "Instruction Manual": createdRecord.fields["Instruction Manual"] || concentratorData["Instruction Manual"], + }; + } catch (error) { + console.error("Error creating concentrator:", error); + throw error; + } +}; + +export const updateConcentrator = async ( + id: string, + concentratorData: Omit +): Promise => { + try { + const response = await fetch(CONCENTRATORS_API_URL, { + method: "PATCH", + headers: getAuthHeaders(), + body: JSON.stringify({ + id: id, + fields: { + "Area Name": concentratorData["Area Name"], + "Device S/N": concentratorData["Device S/N"], + "Device Name": concentratorData["Device Name"], + "Device Time": concentratorData["Device Time"], + "Device Status": concentratorData["Device Status"], + "Operator": concentratorData["Operator"], + "Installed Time": concentratorData["Installed Time"], + "Communication Time": concentratorData["Communication Time"], + "Instruction Manual": concentratorData["Instruction Manual"], + }, + }), + }); + + 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 concentrator: ${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, + "Area Name": updatedRecord.fields["Area Name"] || concentratorData["Area Name"], + "Device S/N": updatedRecord.fields["Device S/N"] || concentratorData["Device S/N"], + "Device Name": updatedRecord.fields["Device Name"] || concentratorData["Device Name"], + "Device Time": updatedRecord.fields["Device Time"] || concentratorData["Device Time"], + "Device Status": updatedRecord.fields["Device Status"] || concentratorData["Device Status"], + "Operator": updatedRecord.fields["Operator"] || concentratorData["Operator"], + "Installed Time": updatedRecord.fields["Installed Time"] || concentratorData["Installed Time"], + "Communication Time": updatedRecord.fields["Communication Time"] || concentratorData["Communication Time"], + "Instruction Manual": updatedRecord.fields["Instruction Manual"] || concentratorData["Instruction Manual"], + }; + } catch (error) { + console.error("Error updating concentrator:", error); + throw error; + } +}; + +export const deleteConcentrator = async (id: string): Promise => { + try { + const response = await fetch(CONCENTRATORS_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 concentrator: ${response.status} ${response.statusText}`); + } + } catch (error) { + console.error("Error deleting concentrator:", error); + throw error; + } +}; diff --git a/src/api/projects.ts b/src/api/projects.ts index 831b6d4..755cbec 100644 --- a/src/api/projects.ts +++ b/src/api/projects.ts @@ -72,7 +72,7 @@ export const fetchProjectNames = async (): Promise => { return projectNames; } catch (error) { console.error("Error fetching project names:", error); - return ["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"]; + return []; } }; diff --git a/src/pages/concentrators/ConcentratorsPage.tsx b/src/pages/concentrators/ConcentratorsPage.tsx index 3a7eec8..875f020 100644 --- a/src/pages/concentrators/ConcentratorsPage.tsx +++ b/src/pages/concentrators/ConcentratorsPage.tsx @@ -2,20 +2,15 @@ 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 { createConcentrator } from "./concentrators.api"; +import { + fetchConcentrators, + createConcentrator, + updateConcentrator, + deleteConcentrator, + type Concentrator, +} from "../../api/concentrators"; /* ================= TYPES ================= */ -interface Concentrator { - "Area Name": string; - "Device S/N": string; - "Device Name": string; - "Device Time": string; - "Device Status": string; - "Operator": string; - "Installed Time": string; - "Communication Time": string; - "Instruction Manual": string; -} interface User { name: string; @@ -34,6 +29,7 @@ export default function ConcentratorsPage() { const [allProjects, setAllProjects] = useState([]); const [loadingProjects, setLoadingProjects] = useState(true); + const [loadingConcentrators, setLoadingConcentrators] = useState(true); useEffect(() => { const loadProjects = async () => { @@ -42,7 +38,7 @@ export default function ConcentratorsPage() { setAllProjects(projects); } catch (error) { console.error('Error loading projects:', error); - setAllProjects(["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"]); + setAllProjects([]); } finally { setLoadingProjects(false); } @@ -62,6 +58,7 @@ export default function ConcentratorsPage() { ); const [selectedProject, setSelectedProject] = useState(""); + const [concentrators, setConcentrators] = useState([]); useEffect(() => { if (visibleProjects.length > 0 && !selectedProject) { @@ -69,41 +66,22 @@ export default function ConcentratorsPage() { } }, [visibleProjects, selectedProject]); - const [concentrators, setConcentrators] = useState([ - { - "Area Name": "GRH (PADRE)", - "Device S/N": "SN001", - "Device Name": "Concentrador A", - "Device Time": "2025-12-17T10:00:00Z", - "Device Status": "ACTIVE", - "Operator": "Operador 1", - "Installed Time": "2025-12-17", - "Communication Time": "2025-12-17T10:30:00Z", - "Instruction Manual": "Manual A", - }, - { - "Area Name": "CESPT", - "Device S/N": "SN002", - "Device Name": "Concentrador B", - "Device Time": "2025-12-16T11:00:00Z", - "Device Status": "INACTIVE", - "Operator": "Operador 2", - "Installed Time": "2025-12-16", - "Communication Time": "2025-12-16T11:30:00Z", - "Instruction Manual": "Manual B", - }, - { - "Area Name": "Proyecto A", - "Device S/N": "SN003", - "Device Name": "Concentrador C", - "Device Time": "2025-12-15T12:00:00Z", - "Device Status": "ACTIVE", - "Operator": "Operador 3", - "Installed Time": "2025-12-15", - "Communication Time": "2025-12-15T12:30:00Z", - "Instruction Manual": "Manual C", - }, - ]); + const loadConcentrators = async () => { + setLoadingConcentrators(true); + try { + const data = await fetchConcentrators(); + setConcentrators(data); + } catch (error) { + console.error("Error loading concentrators:", error); + setConcentrators([]); + } finally { + setLoadingConcentrators(false); + } + }; + + useEffect(() => { + loadConcentrators(); + }, []); const [activeConcentrator, setActiveConcentrator] = useState(null); const [search, setSearch] = useState(""); @@ -129,14 +107,20 @@ export default function ConcentratorsPage() { const handleSave = async () => { try { if (editingSerial) { + const concentratorToUpdate = concentrators.find(c => c["Device S/N"] === editingSerial); + if (!concentratorToUpdate) { + throw new Error("Concentrator to update not found"); + } + + const updatedConcentrator = await updateConcentrator(concentratorToUpdate.id, form); setConcentrators((prev) => prev.map((c) => - c["Device S/N"] === editingSerial ? { ...form } : c + c.id === concentratorToUpdate.id ? updatedConcentrator : c ) ); } else { - await createConcentrator(form); - setConcentrators((prev) => [...prev, { ...form }]); + const newConcentrator = await createConcentrator(form); + setConcentrators((prev) => [...prev, newConcentrator]); } setShowModal(false); setEditingSerial(null); @@ -144,20 +128,35 @@ export default function ConcentratorsPage() { setActiveConcentrator(null); } catch (error) { console.error('Error saving concentrator:', error); - setConcentrators((prev) => [...prev, { ...form }]); - setShowModal(false); - setEditingSerial(null); - setForm({ ...getEmptyConcentrator(), "Area Name": selectedProject }); - setActiveConcentrator(null); + alert( + `Error saving concentrator: ${ + error instanceof Error ? error.message : "Please try again." + }` + ); } }; - const handleDelete = () => { + const handleDelete = async () => { if (!activeConcentrator) return; - setConcentrators((prev) => - prev.filter((c) => c["Device S/N"] !== activeConcentrator["Device S/N"]) + + const confirmDelete = window.confirm( + `Are you sure you want to delete the concentrator "${activeConcentrator["Device Name"]}"?` ); - setActiveConcentrator(null); + + if (!confirmDelete) return; + + try { + await deleteConcentrator(activeConcentrator.id); + setConcentrators((prev) => prev.filter((c) => c.id !== activeConcentrator.id)); + setActiveConcentrator(null); + } catch (error) { + console.error("Error deleting concentrator:", error); + alert( + `Error deleting concentrator: ${ + error instanceof Error ? error.message : "Please try again." + }` + ); + } }; /* ================= FILTER ================= */ @@ -232,7 +231,17 @@ export default function ConcentratorsPage() { onClick={() => { if (!activeConcentrator) return; setEditingSerial(activeConcentrator["Device S/N"]); - setForm(activeConcentrator); + setForm({ + "Area Name": activeConcentrator["Area Name"], + "Device S/N": activeConcentrator["Device S/N"], + "Device Name": activeConcentrator["Device Name"], + "Device Time": activeConcentrator["Device Time"], + "Device Status": activeConcentrator["Device Status"], + "Operator": activeConcentrator["Operator"], + "Installed Time": activeConcentrator["Installed Time"], + "Communication Time": activeConcentrator["Communication Time"], + "Instruction Manual": activeConcentrator["Instruction Manual"], + }); setShowModal(true); }} disabled={!activeConcentrator} @@ -250,7 +259,7 @@ export default function ConcentratorsPage() {
    diff --git a/src/pages/concentrators/concentrators.api.ts b/src/pages/concentrators/concentrators.api.ts deleted file mode 100644 index abac5f7..0000000 --- a/src/pages/concentrators/concentrators.api.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const createConcentrator = async (concentratorData: { - "Area Name": string; - "Device S/N": string; - "Device Name": string; - "Device Time": string; - "Device Status": string; - "Operator": string; - "Installed Time": string; - "Communication Time": string; - "Instruction Manual": string; -}): Promise => { - try { - // const response = await fetch('/api/v3/data/ppfu31vhv5gf6i0/mqqvi3woqdw5ziq/records', { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // }, - // body: JSON.stringify({ fields: concentratorData }), - // }); - // if (!response.ok) { - // throw new Error('Failed to create concentrator'); - // } - - console.log('Creating concentrator with data:', concentratorData); - } catch (error) { - console.error('Error creating concentrator:', error); - throw error; - } -}; From 55bcea33e021b039b0108345c2161d5b247eec59 Mon Sep 17 00:00:00 2001 From: Esteban Date: Fri, 19 Dec 2025 00:25:17 -0600 Subject: [PATCH 7/9] Meters list and CRUD logic --- src/api/meters.ts | 262 +++++++++++++++++ src/pages/meters/MeterPage.tsx | 481 ++++++++++++++++++++++++-------- src/pages/meters/meters.tapi.ts | 0 3 files changed, 621 insertions(+), 122 deletions(-) create mode 100644 src/api/meters.ts delete mode 100644 src/pages/meters/meters.tapi.ts 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 From ebccdcebb067d182604c58d9bbbe075079614a89 Mon Sep 17 00:00:00 2001 From: Esteban Date: Fri, 19 Dec 2025 00:27:38 -0600 Subject: [PATCH 8/9] Delete comments --- vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 437d9be..53ecb8e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,8 +7,8 @@ export default defineConfig({ plugins: [react(),tailwindcss()], server: { - host: '0.0.0.0', // Esto permite que el servidor escuche en todas las IP disponibles - port: 5173, // Puerto por defecto de Vite (puedes cambiarlo si lo deseas) + host: '0.0.0.0', + port: 5173, }, }) From c3655f52228aceb5e46ecd9fcac45ccf99d8ce04 Mon Sep 17 00:00:00 2001 From: Esteban Date: Fri, 19 Dec 2025 00:39:46 -0600 Subject: [PATCH 9/9] Meter new fields --- src/api/meters.ts | 217 +++++++++---------- src/pages/meters/MeterPage.tsx | 376 ++++++++++++++++----------------- 2 files changed, 277 insertions(+), 316 deletions(-) diff --git a/src/api/meters.ts b/src/api/meters.ts index a7f9ca3..90fc743 100644 --- a/src/api/meters.ts +++ b/src/api/meters.ts @@ -10,23 +10,20 @@ const getAuthHeaders = () => ({ 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; + device_id: string; + meter_address: string; + manufacturer_code: string; + forward_cumulative_flow: number; + reverse_cumulative_flow: number; + forward_instantaneous_flow: number; + water_temperature: number; + voltage: number; + echo_amplitude: number; + ultrasonic_flight_time: number; + timestamp: string; + alarm_bytes: string; + checksum_ok: boolean; + received_at: string; }; } @@ -40,23 +37,20 @@ export interface MetersResponse { 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; + deviceId: string; + meterAddress: string; + manufacturerCode: string; + forwardCumulativeFlow: number; + reverseCumulativeFlow: number; + forwardInstantaneousFlow: number; + waterTemperature: number; + voltage: number; + echoAmplitude: number; + ultrasonicFlightTime: number; + timestamp: string; + alarmBytes: string; + checksumOk: boolean; + receivedAt: string; } export const fetchMeters = async (): Promise => { @@ -74,23 +68,20 @@ export const fetchMeters = async (): Promise => { 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"] || "", + deviceId: r.fields.device_id || "", + meterAddress: r.fields.meter_address || "", + manufacturerCode: r.fields.manufacturer_code || "", + forwardCumulativeFlow: r.fields.forward_cumulative_flow || 0, + reverseCumulativeFlow: r.fields.reverse_cumulative_flow || 0, + forwardInstantaneousFlow: r.fields.forward_instantaneous_flow || 0, + waterTemperature: r.fields.water_temperature || 0, + voltage: r.fields.voltage || 0, + echoAmplitude: r.fields.echo_amplitude || 0, + ultrasonicFlightTime: r.fields.ultrasonic_flight_time || 0, + timestamp: r.fields.timestamp || "", + alarmBytes: r.fields.alarm_bytes || "", + checksumOk: r.fields.checksum_ok || false, + receivedAt: r.fields.received_at || "", })); } catch (error) { console.error("Error fetching meters:", error); @@ -107,23 +98,20 @@ export const createMeter = async ( 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, + device_id: meterData.deviceId, + meter_address: meterData.meterAddress, + manufacturer_code: meterData.manufacturerCode, + forward_cumulative_flow: meterData.forwardCumulativeFlow, + reverse_cumulative_flow: meterData.reverseCumulativeFlow, + forward_instantaneous_flow: meterData.forwardInstantaneousFlow, + water_temperature: meterData.waterTemperature, + voltage: meterData.voltage, + echo_amplitude: meterData.echoAmplitude, + ultrasonic_flight_time: meterData.ultrasonicFlightTime, + timestamp: meterData.timestamp, + alarm_bytes: meterData.alarmBytes, + checksum_ok: meterData.checksumOk, + received_at: meterData.receivedAt, }, }), }); @@ -141,23 +129,20 @@ export const createMeter = async ( 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, + deviceId: createdRecord.fields.device_id || meterData.deviceId, + meterAddress: createdRecord.fields.meter_address || meterData.meterAddress, + manufacturerCode: createdRecord.fields.manufacturer_code || meterData.manufacturerCode, + forwardCumulativeFlow: createdRecord.fields.forward_cumulative_flow || meterData.forwardCumulativeFlow, + reverseCumulativeFlow: createdRecord.fields.reverse_cumulative_flow || meterData.reverseCumulativeFlow, + forwardInstantaneousFlow: createdRecord.fields.forward_instantaneous_flow || meterData.forwardInstantaneousFlow, + waterTemperature: createdRecord.fields.water_temperature || meterData.waterTemperature, + voltage: createdRecord.fields.voltage || meterData.voltage, + echoAmplitude: createdRecord.fields.echo_amplitude || meterData.echoAmplitude, + ultrasonicFlightTime: createdRecord.fields.ultrasonic_flight_time || meterData.ultrasonicFlightTime, + timestamp: createdRecord.fields.timestamp || meterData.timestamp, + alarmBytes: createdRecord.fields.alarm_bytes || meterData.alarmBytes, + checksumOk: createdRecord.fields.checksum_ok || meterData.checksumOk, + receivedAt: createdRecord.fields.received_at || meterData.receivedAt, }; } catch (error) { console.error("Error creating meter:", error); @@ -176,23 +161,20 @@ export const updateMeter = async ( 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, + device_id: meterData.deviceId, + meter_address: meterData.meterAddress, + manufacturer_code: meterData.manufacturerCode, + forward_cumulative_flow: meterData.forwardCumulativeFlow, + reverse_cumulative_flow: meterData.reverseCumulativeFlow, + forward_instantaneous_flow: meterData.forwardInstantaneousFlow, + water_temperature: meterData.waterTemperature, + voltage: meterData.voltage, + echo_amplitude: meterData.echoAmplitude, + ultrasonic_flight_time: meterData.ultrasonicFlightTime, + timestamp: meterData.timestamp, + alarm_bytes: meterData.alarmBytes, + checksum_ok: meterData.checksumOk, + received_at: meterData.receivedAt, }, }), }); @@ -214,23 +196,20 @@ export const updateMeter = async ( 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, + deviceId: updatedRecord.fields.device_id || meterData.deviceId, + meterAddress: updatedRecord.fields.meter_address || meterData.meterAddress, + manufacturerCode: updatedRecord.fields.manufacturer_code || meterData.manufacturerCode, + forwardCumulativeFlow: updatedRecord.fields.forward_cumulative_flow || meterData.forwardCumulativeFlow, + reverseCumulativeFlow: updatedRecord.fields.reverse_cumulative_flow || meterData.reverseCumulativeFlow, + forwardInstantaneousFlow: updatedRecord.fields.forward_instantaneous_flow || meterData.forwardInstantaneousFlow, + waterTemperature: updatedRecord.fields.water_temperature || meterData.waterTemperature, + voltage: updatedRecord.fields.voltage || meterData.voltage, + echoAmplitude: updatedRecord.fields.echo_amplitude || meterData.echoAmplitude, + ultrasonicFlightTime: updatedRecord.fields.ultrasonic_flight_time || meterData.ultrasonicFlightTime, + timestamp: updatedRecord.fields.timestamp || meterData.timestamp, + alarmBytes: updatedRecord.fields.alarm_bytes || meterData.alarmBytes, + checksumOk: updatedRecord.fields.checksum_ok || meterData.checksumOk, + receivedAt: updatedRecord.fields.received_at || meterData.receivedAt, }; } catch (error) { console.error("Error updating meter:", error); diff --git a/src/pages/meters/MeterPage.tsx b/src/pages/meters/MeterPage.tsx index 52201db..7b041a9 100644 --- a/src/pages/meters/MeterPage.tsx +++ b/src/pages/meters/MeterPage.tsx @@ -73,23 +73,20 @@ export default function MeterManagement() { const [editingId, setEditingId] = useState(null); const emptyMeter: Omit = { - 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), + deviceId: "", + meterAddress: "", + manufacturerCode: "", + forwardCumulativeFlow: 0, + reverseCumulativeFlow: 0, + forwardInstantaneousFlow: 0, + waterTemperature: 0, + voltage: 0, + echoAmplitude: 0, + ultrasonicFlightTime: 0, + timestamp: new Date().toISOString(), + alarmBytes: "", + checksumOk: true, + receivedAt: new Date().toISOString(), }; const [form, setForm] = useState>(emptyMeter); @@ -131,7 +128,7 @@ export default function MeterManagement() { } setShowModal(false); setEditingId(null); - setForm({ ...emptyMeter, areaName: selectedProject }); + setForm(emptyMeter); setActiveMeter(null); } catch (error) { console.error('Error saving meter:', error); @@ -147,7 +144,7 @@ export default function MeterManagement() { if (!activeMeter) return; const confirmDelete = window.confirm( - `Are you sure you want to delete the meter "${activeMeter.meterName}"?` + `Are you sure you want to delete the meter "${activeMeter.deviceId}"?` ); if (!confirmDelete) return; @@ -174,10 +171,9 @@ export default function MeterManagement() { /* ================= FILTER ================= */ const filtered = meters.filter( (m) => - (m.meterName.toLowerCase().includes(search.toLowerCase()) || - m.meterSN.toLowerCase().includes(search.toLowerCase()) || - m.areaName.toLowerCase().includes(search.toLowerCase())) && - m.areaName === selectedProject + (m.deviceId.toLowerCase().includes(search.toLowerCase()) || + m.meterAddress.toLowerCase().includes(search.toLowerCase()) || + m.manufacturerCode.toLowerCase().includes(search.toLowerCase())) ); /* ================= UI ================= */ @@ -233,7 +229,7 @@ export default function MeterManagement() {
    -
    - -
    - - 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 })} + value={form.deviceId} + onChange={(e) => setForm({ ...form, deviceId: e.target.value })} />
    - + setForm({ ...form, deviceName: e.target.value })} + placeholder="Meter Address" + value={form.meterAddress} + onChange={(e) => setForm({ ...form, meterAddress: e.target.value })} />
    - + setForm({ ...form, deviceType: e.target.value })} + placeholder="Manufacturer Code" + value={form.manufacturerCode} + onChange={(e) => setForm({ ...form, manufacturerCode: e.target.value })} />
    - + setForm({ ...form, usageAnalysisType: e.target.value })} + placeholder="0.000" + value={form.forwardCumulativeFlow} + onChange={(e) => setForm({ ...form, forwardCumulativeFlow: parseFloat(e.target.value) || 0 })} />
    - + setForm({ ...form, installedTime: e.target.value })} + placeholder="0.000" + value={form.reverseCumulativeFlow} + onChange={(e) => setForm({ ...form, reverseCumulativeFlow: parseFloat(e.target.value) || 0 })} + /> +
    + +
    + + setForm({ ...form, forwardInstantaneousFlow: parseFloat(e.target.value) || 0 })} + /> +
    + +
    + + setForm({ ...form, waterTemperature: parseFloat(e.target.value) || 0 })} + /> +
    + +
    + + setForm({ ...form, voltage: parseFloat(e.target.value) || 0 })} + /> +
    + +
    + + setForm({ ...form, echoAmplitude: parseInt(e.target.value) || 0 })} + /> +
    + +
    + + setForm({ ...form, ultrasonicFlightTime: parseInt(e.target.value) || 0 })} + /> +
    + +
    + + setForm({ ...form, timestamp: new Date(e.target.value).toISOString() })} + /> +
    + +
    + + setForm({ ...form, alarmBytes: e.target.value })} + /> +
    + +
    + + +
    + +
    + + setForm({ ...form, receivedAt: new Date(e.target.value).toISOString() })} />