From 82048045112bc7c62dcf6a78d0e9fd04b5e903c4 Mon Sep 17 00:00:00 2001 From: Esteban Date: Thu, 18 Dec 2025 23:14:16 -0600 Subject: [PATCH] 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 }) + } + />
- +