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/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/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/meters.ts b/src/api/meters.ts new file mode 100644 index 0000000..90fc743 --- /dev/null +++ b/src/api/meters.ts @@ -0,0 +1,241 @@ +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: { + 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; + }; +} + +export interface MetersResponse { + records: MeterRecord[]; + next?: string; + prev?: string; + nestedNext?: string; + nestedPrev?: string; +} + +export interface Meter { + id: 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 => { + 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, + 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); + 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: { + 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, + }, + }), + }); + + 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, + 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); + 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: { + 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, + }, + }), + }); + + 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, + 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); + 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/api/projects.ts b/src/api/projects.ts new file mode 100644 index 0000000..755cbec --- /dev/null +++ b/src/api/projects.ts @@ -0,0 +1,253 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +export const PROJECTS_API_URL = `${API_BASE_URL}/api/v3/data/ppfu31vhv5gf6i0/m05u6wpquvdbv3c/records`; +const API_TOKEN = import.meta.env.VITE_API_TOKEN; + +export const getAuthHeaders = () => ({ + Authorization: `Bearer ${API_TOKEN}`, + "Content-Type": "application/json", +}); + +export interface ProjectRecord { + 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; + }; +} + +export interface ProjectsResponse { + records: ProjectRecord[]; + next?: string; + prev?: string; + nestedNext?: string; + nestedPrev?: string; +} + +export interface Project { + id: string; + areaName: string; + deviceSN: string; + deviceName: string; + deviceType: string; + deviceStatus: "ACTIVE" | "INACTIVE"; + operator: string; + installedTime: string; + communicationTime: string; + instructionManual: string; +} + +export const fetchProjectNames = async (): Promise => { + try { + const response = await fetch(PROJECTS_API_URL, { + method: "GET", + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to fetch projects"); + } + + const data: ProjectsResponse = await response.json(); + + if (!data.records || data.records.length === 0) { + console.warn("No project records found from API"); + return []; + } + + const projectNames = [ + ...new Set( + data.records + .map((record) => record.fields["Area name"] || "") + .filter((name) => name) + ), + ]; + + return projectNames; + } catch (error) { + console.error("Error fetching project names:", error); + return []; + } +}; + +export const fetchProjects = async (): Promise => { + try { + const response = await fetch(PROJECTS_API_URL, { + method: "GET", + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error("Failed to fetch projects"); + } + + const data: ProjectsResponse = await response.json(); + + return data.records.map((r: ProjectRecord) => ({ + 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"] ?? "", + })); + } catch (error) { + console.error("Error fetching projects:", error); + throw error; + } +}; + +export const createProject = async ( + projectData: Omit +): Promise => { + const response = await fetch(PROJECTS_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 (!response.ok) { + throw new Error( + `Failed to create project: ${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.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, + }; +}; + +export const updateProject = async ( + id: string, + projectData: Omit +): Promise => { + const response = await fetch(PROJECTS_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 (!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 project: ${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.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, + }; +}; + +export const deleteProject = async (id: string): Promise => { + const response = await fetch(PROJECTS_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 project: ${response.status} ${response.statusText}` + ); + } +}; 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/concentrators/ConcentratorsPage.tsx b/src/pages/concentrators/ConcentratorsPage.tsx index ab9492d..875f020 100644 --- a/src/pages/concentrators/ConcentratorsPage.tsx +++ b/src/pages/concentrators/ConcentratorsPage.tsx @@ -1,16 +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 { + fetchConcentrators, + createConcentrator, + updateConcentrator, + deleteConcentrator, + type Concentrator, +} from "../../api/concentrators"; /* ================= TYPES ================= */ -interface Concentrator { - id: number; - name: string; - location: string; - status: "ACTIVE" | "INACTIVE"; - project: string; - createdAt: string; -} interface User { name: string; @@ -27,96 +27,144 @@ 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); + const [loadingConcentrators, setLoadingConcentrators] = useState(true); + + useEffect(() => { + const loadProjects = async () => { + try { + const projects = await fetchProjectNames(); + setAllProjects(projects); + } catch (error) { + console.error('Error loading projects:', error); + setAllProjects([]); + } 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 [concentrators, setConcentrators] = useState([ - { - id: 1, - name: "Concentrador A", - location: "Planta 1", - status: "ACTIVE", - project: "GRH (PADRE)", - createdAt: "2025-12-17", - }, - { - id: 2, - name: "Concentrador B", - location: "Planta 2", - status: "INACTIVE", - project: "CESPT", - createdAt: "2025-12-16", - }, - { - id: 3, - name: "Concentrador C", - location: "Planta 3", - status: "ACTIVE", - project: "Proyecto A", - createdAt: "2025-12-15", - }, - ]); + const [selectedProject, setSelectedProject] = useState(""); + const [concentrators, setConcentrators] = useState([]); + + useEffect(() => { + if (visibleProjects.length > 0 && !selectedProject) { + setSelectedProject(visibleProjects[0]); + } + }, [visibleProjects, selectedProject]); + + 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(""); const [showModal, setShowModal] = useState(false); - const [editingId, setEditingId] = useState(null); + const [editingSerial, setEditingSerial] = useState(null); - const emptyConcentrator: Omit = { - name: "", - location: "", - status: "ACTIVE", - project: selectedProject, - createdAt: new Date().toISOString().slice(0, 10), - }; + const getEmptyConcentrator = (): Omit => ({ + "Area Name": selectedProject, + "Device S/N": "", + "Device Name": "", + "Device Time": new Date().toISOString(), + "Device Status": "ACTIVE", + "Operator": "", + "Installed Time": new Date().toISOString().slice(0, 10), + "Communication Time": new Date().toISOString(), + "Instruction Manual": "", + }); - const [form, setForm] = useState>(emptyConcentrator); + const [form, setForm] = useState>(getEmptyConcentrator()); /* ================= CRUD ================= */ - const handleSave = () => { - if (editingId) { - setConcentrators((prev) => - prev.map((c) => - c.id === editingId ? { id: editingId, ...form } : c - ) + 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.id === concentratorToUpdate.id ? updatedConcentrator : c + ) + ); + } else { + const newConcentrator = await createConcentrator(form); + setConcentrators((prev) => [...prev, newConcentrator]); + } + setShowModal(false); + setEditingSerial(null); + setForm({ ...getEmptyConcentrator(), "Area Name": selectedProject }); + setActiveConcentrator(null); + } catch (error) { + console.error('Error saving concentrator:', error); + alert( + `Error saving concentrator: ${ + error instanceof Error ? error.message : "Please try again." + }` ); - } else { - const newId = Date.now(); - setConcentrators((prev) => [...prev, { id: newId, ...form }]); } - setShowModal(false); - setEditingId(null); - setForm({ ...emptyConcentrator, project: selectedProject }); - setActiveConcentrator(null); }; - const handleDelete = () => { + const handleDelete = async () => { if (!activeConcentrator) return; - setConcentrators((prev) => - prev.filter((c) => c.id !== activeConcentrator.id) + + 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 ================= */ const filtered = concentrators.filter( (c) => - (c.name.toLowerCase().includes(search.toLowerCase()) || - c.location.toLowerCase().includes(search.toLowerCase())) && - c.project === selectedProject + (c["Device Name"].toLowerCase().includes(search.toLowerCase()) || + c["Device S/N"].toLowerCase().includes(search.toLowerCase())) && + c["Area Name"] === selectedProject ); /* ================= UI ================= */ @@ -132,13 +180,26 @@ export default function ConcentratorsPage() { 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 */} @@ -156,11 +217,12 @@ export default function ConcentratorsPage() {
    @@ -168,8 +230,18 @@ export default function ConcentratorsPage() {
    @@ -249,41 +330,93 @@ export default function ConcentratorsPage() {

    - {editingId ? "Edit Concentrator" : "Add Concentrator"} + {editingSerial ? "Edit Concentrator" : "Add Concentrator"}

    - setForm({ ...form, name: e.target.value })} - /> +
    + + setForm({ ...form, "Device Name": e.target.value })} + /> +
    - setForm({ ...form, location: e.target.value })} - /> +
    + + setForm({ ...form, "Device S/N": e.target.value })} + /> +
    - +
    + + 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 deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/meters/MeterPage.tsx b/src/pages/meters/MeterPage.tsx index 5e4c1b9..7b041a9 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,107 @@ export default function MeterManagement() { const [editingId, setEditingId] = useState(null); const emptyMeter: Omit = { - serialNumber: "", - status: "ACTIVE", - project: selectedProject, - createdAt: 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); - /* ================= 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); + 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.deviceId}"?` + ); + + 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.deviceId.toLowerCase().includes(search.toLowerCase()) || + m.meterAddress.toLowerCase().includes(search.toLowerCase()) || + m.manufacturerCode.toLowerCase().includes(search.toLowerCase())) ); /* ================= UI ================= */ @@ -137,13 +189,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 +229,12 @@ export default function MeterManagement() {
    @@ -177,7 +243,22 @@ export default function MeterManagement() { onClick={() => { if (!activeMeter) return; setEditingId(activeMeter.id); - setForm({ ...activeMeter }); + setForm({ + deviceId: activeMeter.deviceId, + meterAddress: activeMeter.meterAddress, + manufacturerCode: activeMeter.manufacturerCode, + forwardCumulativeFlow: activeMeter.forwardCumulativeFlow, + reverseCumulativeFlow: activeMeter.reverseCumulativeFlow, + forwardInstantaneousFlow: activeMeter.forwardInstantaneousFlow, + waterTemperature: activeMeter.waterTemperature, + voltage: activeMeter.voltage, + echoAmplitude: activeMeter.echoAmplitude, + ultrasonicFlightTime: activeMeter.ultrasonicFlightTime, + timestamp: activeMeter.timestamp, + alarmBytes: activeMeter.alarmBytes, + checksumOk: activeMeter.checksumOk, + receivedAt: activeMeter.receivedAt, + }); setShowModal(true); }} disabled={!activeMeter} @@ -214,25 +295,43 @@ export default function MeterManagement() { {/* TABLE */} `${rowData.forwardCumulativeFlow?.toFixed(3) || 0} m³` + }, + { + title: "Water Temp", + field: "waterTemperature", + render: (rowData) => `${rowData.waterTemperature?.toFixed(1) || 0}°C` + }, + { + title: "Voltage", + field: "voltage", + render: (rowData) => `${rowData.voltage || 0}V` + }, + { + title: "Checksum OK", + field: "checksumOk", render: (rowData) => ( - {rowData.status} + {rowData.checksumOk ? "OK" : "ERROR"} ), }, - { title: "Project", field: "project" }, - { title: "Created", field: "createdAt", type: "date" }, + { title: "Timestamp", field: "timestamp", type: "datetime" }, + { title: "Received At", field: "receivedAt", type: "datetime" }, ]} data={filtered} onRowClick={(_, rowData) => setActiveMeter(rowData as Meter)} @@ -248,55 +347,175 @@ 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, deviceId: e.target.value })} + /> +
    - +
    + + setForm({ ...form, meterAddress: e.target.value })} + /> +
    - - setForm({ ...form, project: e.target.value }) - } - /> +
    + + setForm({ ...form, manufacturerCode: e.target.value })} + /> +
    - - setForm({ ...form, createdAt: e.target.value }) - } - /> +
    + + setForm({ ...form, forwardCumulativeFlow: parseFloat(e.target.value) || 0 })} + /> +
    + +
    + + 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() })} + /> +
    diff --git a/src/pages/meters/meters.tapi.ts b/src/pages/meters/meters.tapi.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/projects/ProjectsPage.tsx b/src/pages/projects/ProjectsPage.tsx new file mode 100644 index 0000000..c366004 --- /dev/null +++ b/src/pages/projects/ProjectsPage.tsx @@ -0,0 +1,336 @@ +import { useEffect, useState } from "react"; +import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; +import MaterialTable from "@material-table/core"; +import { + Project, + fetchProjects, + createProject as apiCreateProject, + updateProject as apiUpdateProject, + deleteProject as apiDeleteProject, +} from "../../api/projects"; + +/* ================= COMPONENT ================= */ +export default function ProjectsPage() { + const [projects, setProjects] = useState([]); + 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 emptyProject: Omit = { + areaName: "", + deviceSN: "", + deviceName: "", + deviceType: "", + deviceStatus: "ACTIVE", + operator: "", + installedTime: "", + communicationTime: "", + instructionManual: "", + }; + + const [form, setForm] = useState>(emptyProject); + + /* ================= LOAD ================= */ + const loadProjects = async () => { + setLoading(true); + try { + const data = await fetchProjects(); + setProjects(data); + } catch (error) { + console.error("Error loading projects:", error); + setProjects([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadProjects(); + }, []); + + + const handleSave = async () => { + try { + if (editingId) { + const updatedProject = await apiUpdateProject(editingId, form); + setProjects((prev) => + prev.map((p) => (p.id === editingId ? updatedProject : p)) + ); + } else { + const newProject = await apiCreateProject(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; + + const confirmDelete = window.confirm( + `Are you sure you want to delete the project "${activeProject.deviceName}"?` + ); + + if (!confirmDelete) return; + + try { + await apiDeleteProject(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 ================= */ + 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", + }), + }} + localization={{ + body: { + emptyDataSourceMessage: loading + ? "Loading projects..." + : "No projects found. Click 'Add' to create your first project.", + }, + }} + /> +
    + + {/* 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..53ecb8e 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', + port: 5173, + }, + }) +