Device Management section

This commit is contained in:
2025-12-17 13:18:43 -06:00
parent fc23b98b18
commit 917016a7cf
3 changed files with 481 additions and 209 deletions

View File

@@ -153,8 +153,14 @@ export default function AreaManagement() {
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
initialState={{
pagination: {
paginationModel: {
pageSize: 5,
},
},
}}
pageSizeOptions={[5]}
sx={{ border: "none", "& .MuiDataGrid-row:hover": { backgroundColor: "rgba(0,0,0,0.03)" } }}
/>
</div>

View File

@@ -1,233 +1,503 @@
import { useState } from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { Add, Delete, Refresh, Edit } from "@mui/icons-material";
import { Button, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, TextField, CircularProgress } from "@mui/material";
import { useEffect, useState } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core";
interface Device {
id: number;
areaName: string;
deviceSn: string;
deviceName: string;
deviceType: string;
deviceStatus: string;
operator: string;
installedTime: string;
communicationTime: string;
id: string;
"Area Name": string;
"Account Number": string;
"User Name": string;
"User Address": string;
"Meter S/N": string;
"Meter Name": string;
"Meter Status": string;
"Protocol Type": string;
"Price No.": string;
"Price Name": string;
"DMA Partition": string;
"Supply Types": string;
"Device ID": string;
"Device Name": string;
"Device Type": string;
"Usage Analysis Type": string;
"Installed Time": string;
}
interface DeviceManagementProps {
subPage: string;
interface ApiResponse {
records: Device[];
next?: string;
prev?: string;
nestedNext?: string;
nestedPrev?: string;
}
export default function DeviceManagement({ subPage: _subPage }: DeviceManagementProps) {
const [rows, setRows] = useState<Device[]>([
{
id: 1,
areaName: "Operaciones",
deviceSn: "DEV001",
deviceName: "Water Meter A1",
deviceType: "Flow Sensor",
deviceStatus: "Installed",
operator: "Juan Pérez",
installedTime: "2024-01-15 10:30:00",
communicationTime: "2024-12-16 14:25:00"
},
{
id: 2,
areaName: "Calidad",
deviceSn: "DEV002",
deviceName: "Pressure Monitor B2",
deviceType: "Pressure Sensor",
deviceStatus: "Installed",
operator: "María García",
installedTime: "2024-02-20 09:15:00",
communicationTime: "2024-12-16 13:45:00"
},
{
id: 3,
areaName: "Mantenimiento",
deviceSn: "DEV003",
deviceName: "Temperature Sensor C3",
deviceType: "Temp Sensor",
deviceStatus: "Uninstalled",
operator: "Carlos López",
installedTime: "2024-03-10 11:00:00",
communicationTime: "2024-12-15 16:30:00"
},
]);
const [dialogOpen, setDialogOpen] = useState(false);
const [editMode, setEditMode] = useState(false);
const [currentDevice, setCurrentDevice] = useState<Device>({
id: 0,
areaName: "",
deviceSn: "",
deviceName: "",
deviceType: "",
deviceStatus: "",
operator: "",
installedTime: "",
communicationTime: "",
});
export default function DeviceManagement() {
const [devices, setDevices] = useState<Device[]>([]);
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [activeDevice, setActiveDevice] = useState<Device | null>(null);
const [loading, setLoading] = useState(false);
const columns: GridColDef[] = [
{ field: "areaName", headerName: "Area Name", width: 150 },
{ field: "deviceSn", headerName: "Device S/N", width: 130 },
{ field: "deviceName", headerName: "Device Name", width: 180 },
{ field: "deviceType", headerName: "Device Type", width: 130 },
{ field: "deviceStatus", headerName: "Device Status", width: 130 },
{ field: "operator", headerName: "Operator", width: 150 },
{ field: "installedTime", headerName: "Installed Time", width: 180 },
{ field: "communicationTime", headerName: "Communication Time", width: 180 },
{
field: "actions",
headerName: "Actions",
width: 150,
renderCell: (params) => (
<div className="flex gap-2">
<IconButton color="primary" size="small" onClick={() => handleEdit(params.row)}>
<Edit fontSize="small" />
</IconButton>
<IconButton color="error" size="small" onClick={() => handleDelete(params.row.id)}>
<Delete fontSize="small" />
</IconButton>
</div>
),
},
];
const emptyDevice: Omit<Device, "id"> = {
"Area Name": "",
"Account Number": "",
"User Name": "",
"User Address": "",
"Meter S/N": "",
"Meter Name": "",
"Meter Status": "",
"Protocol Type": "",
"Price No.": "",
"Price Name": "",
"DMA Partition": "",
"Supply Types": "",
"Device ID": "",
"Device Name": "",
"Device Type": "",
"Usage Analysis Type": "",
"Installed Time": "",
};
const handleDelete = (id: number) => {
if (confirm("¿Deseas eliminar este dispositivo?")) {
setRows(rows.filter(row => row.id !== id));
const [form, setForm] = useState<Omit<Device, "id">>(emptyDevice);
const loadData = async () => {
setLoading(true);
try {
const response = await fetch(
"/api/v3/data/ppfu31vhv5gf6i0/mp1izvcpok5rk6s/records"
);
const data: ApiResponse = await response.json();
setDevices(data.records);
setActiveDevice(null);
} catch (error) {
console.error("Error loading devices:", error);
const mockData: Device[] = [
{
id: "1",
"Area Name": "Operaciones",
"Account Number": "ACC001",
"User Name": "Juan Pérez",
"User Address": "Calle Principal 123",
"Meter S/N": "DEV001",
"Meter Name": "Water Meter A1",
"Meter Status": "Active",
"Protocol Type": "MQTT",
"Price No.": "P001",
"Price Name": "Standard Rate",
"DMA Partition": "Zone A",
"Supply Types": "Water",
"Device ID": "D001",
"Device Name": "Flow Sensor",
"Device Type": "Flow Sensor",
"Usage Analysis Type": "Daily",
"Installed Time": "2024-01-15 10:30:00",
},
{
id: "2",
"Area Name": "Calidad",
"Account Number": "ACC002",
"User Name": "María García",
"User Address": "Avenida Central 456",
"Meter S/N": "DEV002",
"Meter Name": "Pressure Monitor B2",
"Meter Status": "Active",
"Protocol Type": "LoRa",
"Price No.": "P002",
"Price Name": "Premium Rate",
"DMA Partition": "Zone B",
"Supply Types": "Water",
"Device ID": "D002",
"Device Name": "Pressure Sensor",
"Device Type": "Pressure Sensor",
"Usage Analysis Type": "Hourly",
"Installed Time": "2024-02-20 09:15:00",
},
];
setDevices(mockData);
} finally {
setLoading(false);
}
};
const handleEdit = (device: Device) => {
setCurrentDevice(device);
setEditMode(true);
setDialogOpen(true);
useEffect(() => {
loadData();
}, []);
const handleSave = () => {
if (editingId) {
setDevices((prev) =>
prev.map((device) =>
device.id === editingId ? { ...device, ...form } : device
)
);
} else {
const newDevice: Device = {
id: Date.now().toString(),
...form,
};
setDevices((prev) => [...prev, newDevice]);
}
setShowModal(false);
setEditingId(null);
setForm(emptyDevice);
};
const handleAdd = () => {
const newId = rows.length ? Math.max(...rows.map(r => r.id)) + 1 : 1;
setRows([...rows, { ...currentDevice, id: newId }]);
setDialogOpen(false);
resetForm();
const handleEdit = () => {
if (!activeDevice) return;
setEditingId(activeDevice.id);
setForm({ ...activeDevice });
setShowModal(true);
};
const handleUpdate = () => {
setRows(rows.map(r => (r.id === currentDevice.id ? currentDevice : r)));
setDialogOpen(false);
resetForm();
const handleDelete = () => {
if (!activeDevice) return;
if (confirm("¿Deseas eliminar este dispositivo?")) {
setDevices((prev) =>
prev.filter((device) => device.id !== activeDevice.id)
);
setActiveDevice(null);
}
};
const resetForm = () => {
setCurrentDevice({
id: 0,
areaName: "",
deviceSn: "",
deviceName: "",
deviceType: "",
deviceStatus: "",
operator: "",
installedTime: "",
communicationTime: "",
});
setEditMode(false);
};
const handleRefresh = () => {
setLoading(true);
setTimeout(() => {
setRows([...rows]);
setLoading(false);
}, 1000);
};
const filteredDevices = devices.filter((device) => {
const q = search.toLowerCase();
return (
device["Area Name"].toLowerCase().includes(q) ||
device["User Name"].toLowerCase().includes(q) ||
device["Meter S/N"].toLowerCase().includes(q) ||
device["Device Name"].toLowerCase().includes(q)
);
});
return (
<div className="flex flex-col gap-6 p-6 h-full">
<div className="flex gap-6 p-6 w-full bg-gray-100">
<div className="flex-1 flex flex-col gap-6">
<div
className="rounded-xl shadow p-6 text-white flex justify-between items-center"
style={{
background:
"linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8, #3d4e87)",
backgroundSize: "350% 350%",
animation: "gradientMove 10s ease infinite",
}}
>
<div>
<h1 className="text-2xl font-bold">Device Management</h1>
<p className="text-sm text-blue-100">Water Meter Devices</p>
</div>
<div
className="flex justify-between items-center p-5 rounded-xl"
style={{
background: "linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8, #3d4e87)",
backgroundSize: "350% 350%",
animation: "gradientMove 10s ease infinite",
color: "white",
backdropFilter: "blur(10px)",
border: "1px solid rgba(255,255,255,0.25)",
boxShadow: "0px 8px 22px rgba(0,0,0,0.25)",
}}
>
<h1 className="text-xl font-bold">Device Management</h1>
<div className="flex gap-3">
<Button
variant="outlined"
startIcon={<Add />}
sx={{
color: "white",
borderColor: "rgba(255,255,255,0.4)",
"&:hover": { borderColor: "white", background: "rgba(255,255,255,0.15)" },
}}
onClick={() => setDialogOpen(true)}
>
Agregar
</Button>
<div className="flex items-center gap-3">
<button
onClick={() => {
setForm(emptyDevice);
setEditingId(null);
setShowModal(true);
}}
className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg"
>
<Plus size={16} /> Add
</button>
<Button
variant="outlined"
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <Refresh />}
sx={{
color: "white",
borderColor: "rgba(255,255,255,0.4)",
"&:hover": { borderColor: "white", background: "rgba(255,255,255,0.15)" },
}}
onClick={handleRefresh}
>
{loading ? "Recargando..." : "Refrescar"}
</Button>
<button
onClick={handleEdit}
disabled={!activeDevice}
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
${!activeDevice ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"}`}
>
<Pencil size={16} /> Edit
</button>
<button
onClick={handleDelete}
disabled={!activeDevice}
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
${!activeDevice ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"}`}
>
<Trash2 size={16} /> Delete
</button>
<button
onClick={loadData}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg hover:bg-white/10"
>
<RefreshCcw size={16} /> Refresh
</button>
</div>
</div>
</div>
<div className="flex-1 bg-white rounded-xl overflow-hidden shadow-md">
<DataGrid
rows={rows}
columns={columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 5,
<input
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder="Search devices..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<MaterialTable
title="Devices"
columns={[
{ title: "Area Name", field: "Area Name" },
{ title: "Account Number", field: "Account Number" },
{ title: "User Name", field: "User Name" },
{ title: "User Address", field: "User Address" },
{ title: "Meter S/N", field: "Meter S/N" },
{ title: "Meter Name", field: "Meter Name" },
{
title: "Meter Status",
field: "Meter Status",
render: (rowData) => (
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border ${
rowData["Meter Status"] === "Active"
? "text-blue-600 border-blue-600"
: "text-red-600 border-red-600"
}`}
>
{rowData["Meter Status"]}
</span>
),
},
{ title: "Protocol Type", field: "Protocol Type" },
{ title: "Price No.", field: "Price No." },
{ title: "Price Name", field: "Price Name" },
{ title: "DMA Partition", field: "DMA Partition" },
{ title: "Supply Types", field: "Supply Types" },
{ title: "Device ID", field: "Device ID" },
{ title: "Device Name", field: "Device Name" },
{ title: "Device Type", field: "Device Type" },
{ title: "Usage Analysis Type", field: "Usage Analysis Type" },
{
title: "Installed Time",
field: "Installed Time",
type: "datetime",
},
]}
data={filteredDevices}
onRowClick={(_event, rowData) => {
setActiveDevice(rowData as Device);
}}
actions={[
{
icon: () => <Pencil size={16} />,
tooltip: "Edit Device",
onClick: (_event, rowData) => {
setActiveDevice(rowData as Device);
setEditingId((rowData as Device).id);
setForm({ ...(rowData as Device) });
setShowModal(true);
},
},
{
icon: () => <Trash2 size={16} />,
tooltip: "Delete Device",
onClick: (_event, rowData) => {
setActiveDevice(rowData as Device);
handleDelete();
},
},
]}
options={{
actionsColumnIndex: -1,
search: false,
paging: true,
sorting: true,
headerStyle: {
textAlign: "center",
fontWeight: 600,
},
maxBodyHeight: "500px",
tableLayout: "fixed",
rowStyle: (rowData) => ({
backgroundColor:
activeDevice?.id === (rowData as Device).id
? "#EEF2FF"
: "#FFFFFF",
}),
}}
pageSizeOptions={[5]}
sx={{ border: "none", "& .MuiDataGrid-row:hover": { backgroundColor: "rgba(0,0,0,0.03)" } }}
isLoading={loading}
/>
</div>
<Dialog open={dialogOpen} onClose={() => { setDialogOpen(false); resetForm(); }}>
<DialogTitle>{editMode ? "Editar Dispositivo" : "Agregar Nuevo Dispositivo"}</DialogTitle>
<DialogContent className="flex flex-col gap-3 min-w-[400px]">
{["areaName","deviceSn","deviceName","deviceType","deviceStatus","operator","installedTime","communicationTime"].map((field) => (
<TextField
key={field}
label={field.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
type="text"
value={String(currentDevice[field as keyof Device] || "")}
onChange={(e) => setCurrentDevice({ ...currentDevice, [field]: e.target.value })}
fullWidth
/>
))}
</DialogContent>
<DialogActions>
<Button onClick={() => { setDialogOpen(false); resetForm(); }}>Cancelar</Button>
<Button variant="contained" onClick={editMode ? handleUpdate : handleAdd}>
{editMode ? "Actualizar" : "Agregar"}
</Button>
</DialogActions>
</Dialog>
{showModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center">
<div className="bg-white rounded-xl p-6 w-[600px] max-h-[80vh] overflow-y-auto space-y-3">
<h2 className="text-lg font-semibold">
{editingId ? "Edit Device" : "Add Device"}
</h2>
<div className="grid grid-cols-2 gap-3">
<input
className="w-full border px-3 py-2 rounded"
placeholder="Area Name"
value={form["Area Name"]}
onChange={(e) =>
setForm({ ...form, "Area Name": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Account Number"
value={form["Account Number"]}
onChange={(e) =>
setForm({ ...form, "Account Number": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="User Name"
value={form["User Name"]}
onChange={(e) =>
setForm({ ...form, "User Name": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="User Address"
value={form["User Address"]}
onChange={(e) =>
setForm({ ...form, "User Address": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Meter S/N"
value={form["Meter S/N"]}
onChange={(e) =>
setForm({ ...form, "Meter S/N": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Meter Name"
value={form["Meter Name"]}
onChange={(e) =>
setForm({ ...form, "Meter Name": e.target.value })
}
/>
<select
className="w-full border px-3 py-2 rounded"
value={form["Meter Status"]}
onChange={(e) =>
setForm({ ...form, "Meter Status": e.target.value })
}
>
<option value="">Select Status</option>
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="Maintenance">Maintenance</option>
</select>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Protocol Type"
value={form["Protocol Type"]}
onChange={(e) =>
setForm({ ...form, "Protocol Type": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Price No."
value={form["Price No."]}
onChange={(e) =>
setForm({ ...form, "Price No.": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Price Name"
value={form["Price Name"]}
onChange={(e) =>
setForm({ ...form, "Price Name": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="DMA Partition"
value={form["DMA Partition"]}
onChange={(e) =>
setForm({ ...form, "DMA Partition": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Supply Types"
value={form["Supply Types"]}
onChange={(e) =>
setForm({ ...form, "Supply Types": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Device ID"
value={form["Device ID"]}
onChange={(e) =>
setForm({ ...form, "Device ID": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Device Name"
value={form["Device Name"]}
onChange={(e) =>
setForm({ ...form, "Device Name": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Device Type"
value={form["Device Type"]}
onChange={(e) =>
setForm({ ...form, "Device Type": e.target.value })
}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Usage Analysis Type"
value={form["Usage Analysis Type"]}
onChange={(e) =>
setForm({ ...form, "Usage Analysis Type": e.target.value })
}
/>
<input
type="datetime-local"
className="w-full border px-3 py-2 rounded"
value={form["Installed Time"]}
onChange={(e) =>
setForm({ ...form, "Installed Time": e.target.value })
}
/>
</div>
<div className="flex justify-end gap-2 pt-3">
<button onClick={() => setShowModal(false)}>Cancel</button>
<button
onClick={handleSave}
className="bg-[#4c5f9e] text-white px-4 py-2 rounded"
>
Save
</button>
</div>
</div>
</div>
)}
<style>
{`