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

View File

@@ -18,6 +18,14 @@ interface User {
project?: string; // asignado si no es superadmin
}
interface DeviceData {
"Device ID": number;
"Device EUI": string;
"Join EUI": string;
"AppKey": string;
meterId?: string;
}
/* ================= COMPONENT ================= */
export default function MeterManagement() {
// Simulación de usuario actual
@@ -94,7 +102,16 @@ export default function MeterManagement() {
installedTime: new Date().toISOString(),
};
const emptyDeviceData: DeviceData = {
"Device ID": 0,
"Device EUI": "",
"Join EUI": "",
"AppKey": "",
};
const [form, setForm] = useState<Omit<Meter, "id">>(emptyMeter);
const [deviceForm, setDeviceForm] = useState<DeviceData>(emptyDeviceData);
const [errors, setErrors] = useState<{ [key: string]: boolean }>({});
const loadMeters = async () => {
setLoadingMeters(true);
@@ -113,8 +130,49 @@ export default function MeterManagement() {
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 () => {
if (!validateForm()) {
return;
}
try {
let savedMeter: Meter;
if (editingId) {
const meterToUpdate = meters.find(m => m.id === editingId);
if (!meterToUpdate) {
@@ -127,13 +185,30 @@ export default function MeterManagement() {
m.id === editingId ? updatedMeter : m
)
);
savedMeter = updatedMeter;
} else {
const newMeter = await createMeter(form);
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);
setEditingId(null);
setForm(emptyMeter);
setDeviceForm(emptyDeviceData);
setErrors({});
setActiveMeter(null);
} catch (error) {
console.error('Error saving meter:', error);
@@ -236,6 +311,8 @@ export default function MeterManagement() {
<button
onClick={() => {
setForm(emptyMeter);
setDeviceForm(emptyDeviceData);
setErrors({});
setEditingId(null);
setShowModal(true);
}}
@@ -270,6 +347,8 @@ export default function MeterManagement() {
usageAnalysisType: activeMeter.usageAnalysisType,
installedTime: activeMeter.installedTime,
});
setDeviceForm(emptyDeviceData);
setErrors({});
setShowModal(true);
}}
disabled={!activeMeter}
@@ -359,123 +438,235 @@ export default function MeterManagement() {
{/* MODAL */}
{showModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center">
<div className="bg-white rounded-xl p-6 w-96 max-h-[80vh] overflow-y-auto space-y-3">
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<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">
{editingId ? "Edit Meter" : "Add Meter"}
</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
className="w-full border px-3 py-2 rounded"
placeholder="Meter Name"
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["meterName"] ? "border-red-500" : ""
}`}
placeholder="Meter Name *"
value={form.meterName}
onChange={(e) => setForm({ ...form, meterName: e.target.value })}
/>
</div>
<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 })
onChange={(e) => {
setForm({ ...form, meterName: e.target.value });
if (errors["meterName"]) {
setErrors({ ...errors, "meterName": false });
}
}}
required
/>
{errors["meterName"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
<div className="space-y-1">
<div>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Area Name"
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
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}
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 className="space-y-1">
<div className="grid grid-cols-2 gap-3">
<div>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Device ID"
value={form.deviceId}
onChange={(e) => setForm({ ...form, deviceId: e.target.value })}
/>
</div>
<div className="space-y-1">
<input
className="w-full border px-3 py-2 rounded"
placeholder="Device Name"
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["deviceName"] ? "border-red-500" : ""
}`}
placeholder="Device Name *"
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 className="space-y-1">
<div>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Device Type"
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["deviceType"] ? "border-red-500" : ""
}`}
placeholder="Device Type *"
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 className="space-y-1">
<div>
<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}
onChange={(e) => setForm({ ...form, meterStatus: e.target.value })}
>
<option value="Installed">Meter Status: Installed</option>
<option value="Uninstalled">Meter Status: Uninstalled</option>
<option value="Maintenance">Meter Status: Maintenance</option>
<option value="Installed">Installed</option>
<option value="Uninstalled">Uninstalled</option>
<option value="Maintenance">Maintenance</option>
</select>
</div>
<div className="space-y-1">
<div className="grid grid-cols-2 gap-3">
<div>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Protocol Type"
className={`w-full border px-3 py-2 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors["protocolType"] ? "border-red-500" : ""
}`}
placeholder="Protocol Type *"
value={form.protocolType}
onChange={(e) => setForm({ ...form, protocolType: e.target.value })}
/>
</div>
<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 })
onChange={(e) => {
setForm({ ...form, protocolType: e.target.value });
if (errors["protocolType"]) {
setErrors({ ...errors, "protocolType": false });
}
}}
required
/>
{errors["protocolType"] && (
<p className="text-red-500 text-xs mt-1">This field is required</p>
)}
</div>
<div className="space-y-1">
<div>
<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)"
value={form.accountNumber ?? ""}
onChange={(e) =>
setForm({ ...form, accountNumber: e.target.value || null })
}
/>
</div>
<div className="space-y-1">
<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)"
value={form.userName ?? ""}
onChange={(e) =>
@@ -484,40 +675,34 @@ export default function MeterManagement() {
/>
</div>
<div className="space-y-1">
<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)"
value={form.userAddress ?? ""}
onChange={(e) =>
setForm({ ...form, userAddress: e.target.value || null })
}
/>
</div>
<div className="space-y-1">
<div className="grid grid-cols-3 gap-3 mt-3">
<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)"
value={form.priceNo ?? ""}
onChange={(e) => setForm({ ...form, priceNo: e.target.value || null })}
/>
</div>
<div className="space-y-1">
<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)"
value={form.priceName ?? ""}
onChange={(e) =>
setForm({ ...form, priceName: e.target.value || null })
}
/>
</div>
<div className="space-y-1">
<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)"
value={form.dmaPartition ?? ""}
onChange={(e) =>
@@ -525,27 +710,116 @@ export default function MeterManagement() {
}
/>
</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">
<button onClick={() => setShowModal(false)}>Cancel</button>
<div className="space-y-3 pt-4">
<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
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
</button>