Meter edit and create logic form & concentrators project select option

This commit is contained in:
2025-12-22 00:19:09 -06:00
parent a5fa0cfa64
commit 820e14463c
2 changed files with 454 additions and 193 deletions

View File

@@ -1,7 +1,6 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react"; import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core"; import MaterialTable from "@material-table/core";
import { fetchProjectNames } from "../../api/projects";
import { import {
fetchConcentrators, fetchConcentrators,
createConcentrator, createConcentrator,
@@ -40,22 +39,6 @@ export default function ConcentratorsPage() {
const [loadingProjects, setLoadingProjects] = useState(true); const [loadingProjects, setLoadingProjects] = useState(true);
const [loadingConcentrators, setLoadingConcentrators] = 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 // Proyectos visibles según el usuario
const visibleProjects = useMemo(() => const visibleProjects = useMemo(() =>
currentUser.role === "SUPER_ADMIN" currentUser.role === "SUPER_ADMIN"
@@ -79,12 +62,16 @@ export default function ConcentratorsPage() {
setLoadingConcentrators(true); setLoadingConcentrators(true);
try { try {
const data = await fetchConcentrators(); const data = await fetchConcentrators();
const projectsArray = data.map((record) => record["Area Name"]);
setAllProjects(projectsArray);
setConcentrators(data); setConcentrators(data);
} catch (error) { } catch (error) {
console.error("Error loading concentrators:", error); console.error("Error loading concentrators:", error);
setAllProjects([]);
setConcentrators([]); setConcentrators([]);
} finally { } finally {
setLoadingConcentrators(false); setLoadingConcentrators(false);
setLoadingProjects(false);
} }
}; };

View File

@@ -18,6 +18,14 @@ interface User {
project?: string; // asignado si no es superadmin project?: string; // asignado si no es superadmin
} }
interface DeviceData {
"Device ID": number;
"Device EUI": string;
"Join EUI": string;
"AppKey": string;
meterId?: string;
}
/* ================= COMPONENT ================= */ /* ================= COMPONENT ================= */
export default function MeterManagement() { export default function MeterManagement() {
// Simulación de usuario actual // Simulación de usuario actual
@@ -94,7 +102,16 @@ export default function MeterManagement() {
installedTime: new Date().toISOString(), installedTime: new Date().toISOString(),
}; };
const emptyDeviceData: DeviceData = {
"Device ID": 0,
"Device EUI": "",
"Join EUI": "",
"AppKey": "",
};
const [form, setForm] = useState<Omit<Meter, "id">>(emptyMeter); const [form, setForm] = useState<Omit<Meter, "id">>(emptyMeter);
const [deviceForm, setDeviceForm] = useState<DeviceData>(emptyDeviceData);
const [errors, setErrors] = useState<{ [key: string]: boolean }>({});
const loadMeters = async () => { const loadMeters = async () => {
setLoadingMeters(true); setLoadingMeters(true);
@@ -113,8 +130,49 @@ export default function MeterManagement() {
loadMeters(); loadMeters();
}, []); }, []);
const createOrUpdateDevice = async (deviceData: DeviceData): Promise<void> => {
//await fetch('/api/devices', { method: 'POST', body: JSON.stringify(deviceData) })
return new Promise((resolve) => {
setTimeout(() => {
console.log('Device data that would be sent to API:', deviceData);
resolve();
}, 500);
});
};
const validateForm = (): boolean => {
const newErrors: { [key: string]: boolean } = {};
if (!form.meterName.trim()) newErrors["meterName"] = true;
if (!form.meterSerialNumber.trim()) newErrors["meterSerialNumber"] = true;
if (!form.areaName.trim()) newErrors["areaName"] = true;
if (!form.deviceName.trim()) newErrors["deviceName"] = true;
if (!form.deviceType.trim()) newErrors["deviceType"] = true;
if (!form.protocolType.trim()) newErrors["protocolType"] = true;
if (!form.supplyTypes.trim()) newErrors["supplyTypes"] = true;
if (!form.usageAnalysisType.trim()) newErrors["usageAnalysisType"] = true;
if (!form.installedTime) newErrors["installedTime"] = true;
if (!deviceForm["Device ID"] || deviceForm["Device ID"] === 0) {
newErrors["Device ID"] = true;
}
if (!deviceForm["Device EUI"].trim()) newErrors["Device EUI"] = true;
if (!deviceForm["Join EUI"].trim()) newErrors["Join EUI"] = true;
if (!deviceForm["AppKey"].trim()) newErrors["AppKey"] = true;
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSave = async () => { const handleSave = async () => {
if (!validateForm()) {
return;
}
try { try {
let savedMeter: Meter;
if (editingId) { if (editingId) {
const meterToUpdate = meters.find(m => m.id === editingId); const meterToUpdate = meters.find(m => m.id === editingId);
if (!meterToUpdate) { if (!meterToUpdate) {
@@ -127,13 +185,30 @@ export default function MeterManagement() {
m.id === editingId ? updatedMeter : m m.id === editingId ? updatedMeter : m
) )
); );
savedMeter = updatedMeter;
} else { } else {
const newMeter = await createMeter(form); const newMeter = await createMeter(form);
setMeters((prev) => [...prev, newMeter]); setMeters((prev) => [...prev, newMeter]);
savedMeter = newMeter;
} }
try {
const deviceDataWithRef = {
...deviceForm,
meterId: savedMeter.id,
};
await createOrUpdateDevice(deviceDataWithRef);
console.log('Device data saved successfully');
} catch (deviceError) {
console.error('Error saving device data:', deviceError);
alert('Meter saved, but there was an error saving device data.');
}
setShowModal(false); setShowModal(false);
setEditingId(null); setEditingId(null);
setForm(emptyMeter); setForm(emptyMeter);
setDeviceForm(emptyDeviceData);
setErrors({});
setActiveMeter(null); setActiveMeter(null);
} catch (error) { } catch (error) {
console.error('Error saving meter:', error); console.error('Error saving meter:', error);
@@ -236,6 +311,8 @@ export default function MeterManagement() {
<button <button
onClick={() => { onClick={() => {
setForm(emptyMeter); setForm(emptyMeter);
setDeviceForm(emptyDeviceData);
setErrors({});
setEditingId(null); setEditingId(null);
setShowModal(true); setShowModal(true);
}} }}
@@ -270,6 +347,8 @@ export default function MeterManagement() {
usageAnalysisType: activeMeter.usageAnalysisType, usageAnalysisType: activeMeter.usageAnalysisType,
installedTime: activeMeter.installedTime, installedTime: activeMeter.installedTime,
}); });
setDeviceForm(emptyDeviceData);
setErrors({});
setShowModal(true); setShowModal(true);
}} }}
disabled={!activeMeter} disabled={!activeMeter}
@@ -359,123 +438,235 @@ export default function MeterManagement() {
{/* MODAL */} {/* MODAL */}
{showModal && ( {showModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center"> <div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-96 max-h-[80vh] overflow-y-auto space-y-3"> <div className="bg-white rounded-xl p-6 w-[700px] max-h-[90vh] overflow-y-auto space-y-4">
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
{editingId ? "Edit Meter" : "Add Meter"} {editingId ? "Edit Meter" : "Add Meter"}
</h2> </h2>
<div className="space-y-1"> <div className="space-y-3">
<h3 className="text-sm font-semibold text-gray-700 border-b pb-2">
Meter Information
</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<input <input
className="w-full border px-3 py-2 rounded" className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
placeholder="Meter Name" errors["meterName"] ? "border-red-500" : ""
}`}
placeholder="Meter Name *"
value={form.meterName} value={form.meterName}
onChange={(e) => setForm({ ...form, meterName: e.target.value })} onChange={(e) => {
/> setForm({ ...form, meterName: e.target.value });
</div> if (errors["meterName"]) {
setErrors({ ...errors, "meterName": false });
<div className="space-y-1">
<input
className="w-full border px-3 py-2 rounded"
placeholder="Meter Serial Number"
value={form.meterSerialNumber}
onChange={(e) =>
setForm({ ...form, meterSerialNumber: e.target.value })
} }
}}
required
/> />
{errors["meterName"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div> </div>
<div className="space-y-1"> <div>
<input <input
className="w-full border px-3 py-2 rounded" className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
placeholder="Area Name" errors["meterSerialNumber"] ? "border-red-500" : ""
}`}
placeholder="Meter Serial Number *"
value={form.meterSerialNumber}
onChange={(e) => {
setForm({ ...form, meterSerialNumber: e.target.value });
if (errors["meterSerialNumber"]) {
setErrors({ ...errors, "meterSerialNumber": false });
}
}}
required
/>
{errors["meterSerialNumber"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
</div>
<div>
<input
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["areaName"] ? "border-red-500" : ""
}`}
placeholder="Area Name *"
value={form.areaName} value={form.areaName}
onChange={(e) => setForm({ ...form, areaName: e.target.value })} onChange={(e) => {
setForm({ ...form, areaName: e.target.value });
if (errors["areaName"]) {
setErrors({ ...errors, "areaName": false });
}
}}
required
/> />
{errors["areaName"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div> </div>
<div className="space-y-1"> <div className="grid grid-cols-2 gap-3">
<div>
<input <input
className="w-full border px-3 py-2 rounded" className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
placeholder="Device ID" errors["deviceName"] ? "border-red-500" : ""
value={form.deviceId} }`}
onChange={(e) => setForm({ ...form, deviceId: e.target.value })} placeholder="Device Name *"
/>
</div>
<div className="space-y-1">
<input
className="w-full border px-3 py-2 rounded"
placeholder="Device Name"
value={form.deviceName} value={form.deviceName}
onChange={(e) => setForm({ ...form, deviceName: e.target.value })} onChange={(e) => {
setForm({ ...form, deviceName: e.target.value });
if (errors["deviceName"]) {
setErrors({ ...errors, "deviceName": false });
}
}}
required
/> />
{errors["deviceName"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div> </div>
<div className="space-y-1"> <div>
<input <input
className="w-full border px-3 py-2 rounded" className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
placeholder="Device Type" errors["deviceType"] ? "border-red-500" : ""
}`}
placeholder="Device Type *"
value={form.deviceType} value={form.deviceType}
onChange={(e) => setForm({ ...form, deviceType: e.target.value })} onChange={(e) => {
setForm({ ...form, deviceType: e.target.value });
if (errors["deviceType"]) {
setErrors({ ...errors, "deviceType": false });
}
}}
required
/> />
{errors["deviceType"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
</div> </div>
<div className="space-y-1"> <div>
<select <select
className="w-full border px-3 py-2 rounded" className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={form.meterStatus} value={form.meterStatus}
onChange={(e) => setForm({ ...form, meterStatus: e.target.value })} onChange={(e) => setForm({ ...form, meterStatus: e.target.value })}
> >
<option value="Installed">Meter Status: Installed</option> <option value="Installed">Installed</option>
<option value="Uninstalled">Meter Status: Uninstalled</option> <option value="Uninstalled">Uninstalled</option>
<option value="Maintenance">Meter Status: Maintenance</option> <option value="Maintenance">Maintenance</option>
</select> </select>
</div> </div>
<div className="space-y-1"> <div className="grid grid-cols-2 gap-3">
<div>
<input <input
className="w-full border px-3 py-2 rounded" className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
placeholder="Protocol Type" errors["protocolType"] ? "border-red-500" : ""
}`}
placeholder="Protocol Type *"
value={form.protocolType} value={form.protocolType}
onChange={(e) => setForm({ ...form, protocolType: e.target.value })} onChange={(e) => {
/> setForm({ ...form, protocolType: e.target.value });
</div> if (errors["protocolType"]) {
setErrors({ ...errors, "protocolType": false });
<div className="space-y-1">
<input
className="w-full border px-3 py-2 rounded"
placeholder="Supply Types"
value={form.supplyTypes}
onChange={(e) => setForm({ ...form, supplyTypes: e.target.value })}
/>
</div>
<div className="space-y-1">
<input
className="w-full border px-3 py-2 rounded"
placeholder="Usage Analysis Type"
value={form.usageAnalysisType}
onChange={(e) =>
setForm({ ...form, usageAnalysisType: e.target.value })
} }
}}
required
/> />
{errors["protocolType"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div> </div>
<div className="space-y-1"> <div>
<input <input
className="w-full border px-3 py-2 rounded" className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["supplyTypes"] ? "border-red-500" : ""
}`}
placeholder="Supply Types *"
value={form.supplyTypes}
onChange={(e) => {
setForm({ ...form, supplyTypes: e.target.value });
if (errors["supplyTypes"]) {
setErrors({ ...errors, "supplyTypes": false });
}
}}
required
/>
{errors["supplyTypes"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
</div>
<div>
<input
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["usageAnalysisType"] ? "border-red-500" : ""
}`}
placeholder="Usage Analysis Type *"
value={form.usageAnalysisType}
onChange={(e) => {
setForm({ ...form, usageAnalysisType: e.target.value });
if (errors["usageAnalysisType"]) {
setErrors({ ...errors, "usageAnalysisType": false });
}
}}
required
/>
{errors["usageAnalysisType"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
<div>
<input
type="datetime-local"
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["installedTime"] ? "border-red-500" : ""
}`}
placeholder="Installed Time *"
value={
form.installedTime
? new Date(form.installedTime).toISOString().slice(0, 16)
: ""
}
onChange={(e) => {
setForm({ ...form, installedTime: new Date(e.target.value).toISOString() });
if (errors["installedTime"]) {
setErrors({ ...errors, "installedTime": false });
}
}}
required
/>
{errors["installedTime"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
<div className="pt-2 border-t">
<p className="text-xs text-gray-500 mb-2">Optional Fields</p>
<div className="grid grid-cols-2 gap-3">
<input
className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Account Number (optional)" placeholder="Account Number (optional)"
value={form.accountNumber ?? ""} value={form.accountNumber ?? ""}
onChange={(e) => onChange={(e) =>
setForm({ ...form, accountNumber: e.target.value || null }) setForm({ ...form, accountNumber: e.target.value || null })
} }
/> />
</div>
<div className="space-y-1">
<input <input
className="w-full border px-3 py-2 rounded" className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="User Name (optional)" placeholder="User Name (optional)"
value={form.userName ?? ""} value={form.userName ?? ""}
onChange={(e) => onChange={(e) =>
@@ -484,40 +675,34 @@ export default function MeterManagement() {
/> />
</div> </div>
<div className="space-y-1">
<input <input
className="w-full border px-3 py-2 rounded" className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent mt-3"
placeholder="User Address (optional)" placeholder="User Address (optional)"
value={form.userAddress ?? ""} value={form.userAddress ?? ""}
onChange={(e) => onChange={(e) =>
setForm({ ...form, userAddress: e.target.value || null }) setForm({ ...form, userAddress: e.target.value || null })
} }
/> />
</div>
<div className="space-y-1"> <div className="grid grid-cols-3 gap-3 mt-3">
<input <input
className="w-full border px-3 py-2 rounded" className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Price No. (optional)" placeholder="Price No. (optional)"
value={form.priceNo ?? ""} value={form.priceNo ?? ""}
onChange={(e) => setForm({ ...form, priceNo: e.target.value || null })} onChange={(e) => setForm({ ...form, priceNo: e.target.value || null })}
/> />
</div>
<div className="space-y-1">
<input <input
className="w-full border px-3 py-2 rounded" className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Price Name (optional)" placeholder="Price Name (optional)"
value={form.priceName ?? ""} value={form.priceName ?? ""}
onChange={(e) => onChange={(e) =>
setForm({ ...form, priceName: e.target.value || null }) setForm({ ...form, priceName: e.target.value || null })
} }
/> />
</div>
<div className="space-y-1">
<input <input
className="w-full border px-3 py-2 rounded" className="w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="DMA Partition (optional)" placeholder="DMA Partition (optional)"
value={form.dmaPartition ?? ""} value={form.dmaPartition ?? ""}
onChange={(e) => onChange={(e) =>
@@ -525,27 +710,116 @@ export default function MeterManagement() {
} }
/> />
</div> </div>
</div>
<div className="space-y-1">
<input
type="datetime-local"
className="w-full border px-3 py-2 rounded"
value={
form.installedTime
? new Date(form.installedTime).toISOString().slice(0, 16)
: ""
}
onChange={(e) =>
setForm({ ...form, installedTime: new Date(e.target.value).toISOString() })
}
/>
</div> </div>
<div className="flex justify-end gap-2 pt-3"> <div className="space-y-3 pt-4">
<button onClick={() => setShowModal(false)}>Cancel</button> <h3 className="text-sm font-semibold text-gray-700 border-b pb-2">
Device Configuration
</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<input
type="number"
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["Device ID"] ? "border-red-500" : ""
}`}
placeholder="Device ID *"
value={deviceForm["Device ID"] || ""}
onChange={(e) => {
setDeviceForm({
...deviceForm,
"Device ID": parseInt(e.target.value) || 0,
});
if (errors["Device ID"]) {
setErrors({ ...errors, "Device ID": false });
}
}}
required
min="1"
/>
{errors["Device ID"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
<div>
<input
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["Device EUI"] ? "border-red-500" : ""
}`}
placeholder="Device EUI *"
value={deviceForm["Device EUI"]}
onChange={(e) => {
setDeviceForm({ ...deviceForm, "Device EUI": e.target.value });
if (errors["Device EUI"]) {
setErrors({ ...errors, "Device EUI": false });
}
}}
required
/>
{errors["Device EUI"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
</div>
<div>
<input
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["Join EUI"] ? "border-red-500" : ""
}`}
placeholder="Join EUI *"
value={deviceForm["Join EUI"]}
onChange={(e) => {
setDeviceForm({ ...deviceForm, "Join EUI": e.target.value });
if (errors["Join EUI"]) {
setErrors({ ...errors, "Join EUI": false });
}
}}
required
/>
{errors["Join EUI"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
<div>
<input
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["AppKey"] ? "border-red-500" : ""
}`}
placeholder="AppKey *"
value={deviceForm["AppKey"]}
onChange={(e) => {
setDeviceForm({ ...deviceForm, "AppKey": e.target.value });
if (errors["AppKey"]) {
setErrors({ ...errors, "AppKey": false });
}
}}
required
/>
{errors["AppKey"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
</div>
<div className="flex justify-end gap-2 pt-3 border-t">
<button
onClick={() => {
setShowModal(false);
setDeviceForm(emptyDeviceData);
setErrors({});
}}
className="px-4 py-2 rounded hover:bg-gray-100"
>
Cancel
</button>
<button <button
onClick={handleSave} onClick={handleSave}
className="bg-[#4c5f9e] text-white px-4 py-2 rounded" className="bg-[#4c5f9e] text-white px-4 py-2 rounded hover:bg-[#3d4d7e]"
> >
Save Save
</button> </button>