refactor: reorganización de estructura, pages y components

This commit is contained in:
Marlene-Angel
2025-12-17 17:21:51 -08:00
parent a6d9de5682
commit ae43042ac6
16 changed files with 1000 additions and 1612 deletions

View File

@@ -1,202 +0,0 @@
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";
interface Area {
id: number;
name: string;
no: string;
code: string;
sort: number;
pushAddress: string;
note: string;
time: string;
}
export default function AreaManagement() {
const [rows, setRows] = useState<Area[]>([
{ id: 1, name: "Operaciones", no: "001", code: "OP01", sort: 1, pushAddress: "Calle 123", note: "Área principal", time: "08:00-17:00" },
{ id: 2, name: "Calidad", no: "002", code: "QA02", sort: 2, pushAddress: "Calle 456", note: "Revisión diaria", time: "09:00-18:00" },
{ id: 3, name: "Mantenimiento", no: "003", code: "MT03", sort: 3, pushAddress: "Calle 789", note: "Turno A", time: "07:00-15:00" },
]);
const [dialogOpen, setDialogOpen] = useState(false);
const [editMode, setEditMode] = useState(false);
const [currentArea, setCurrentArea] = useState<Area>({
id: 0,
name: "",
no: "",
code: "",
sort: 0,
pushAddress: "",
note: "",
time: "",
});
const [loading, setLoading] = useState(false);
// Columns del DataGrid
const columns: GridColDef[] = [
{ field: "id", headerName: "ID", width: 70 },
{ field: "name", headerName: "Área", width: 150 },
{ field: "no", headerName: "Área No.", width: 120 },
{ field: "code", headerName: "Código", width: 120 },
{ field: "sort", headerName: "Sort", width: 80 },
{ field: "pushAddress", headerName: "Push Address", width: 180 },
{ field: "note", headerName: "Notas", width: 200 },
{ field: "time", headerName: "Time", width: 120 },
{
field: "operate",
headerName: "Operar",
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>
),
},
];
// FUNCIONES CRUD
const handleDelete = (id: number) => {
if (confirm("¿Deseas eliminar esta área?")) {
setRows(rows.filter(row => row.id !== id));
}
};
const handleEdit = (area: Area) => {
setCurrentArea(area);
setEditMode(true);
setDialogOpen(true);
};
const handleAdd = () => {
const newId = rows.length ? Math.max(...rows.map(r => r.id)) + 1 : 1;
setRows([...rows, { ...currentArea, id: newId }]);
setDialogOpen(false);
resetForm();
};
const handleUpdate = () => {
setRows(rows.map(r => (r.id === currentArea.id ? currentArea : r)));
setDialogOpen(false);
resetForm();
};
const resetForm = () => {
setCurrentArea({ id: 0, name: "", no: "", code: "", sort: 0, pushAddress: "", note: "", time: "" });
setEditMode(false);
};
const handleRefresh = () => {
setLoading(true);
setTimeout(() => {
setRows([...rows]); // podrías reemplazar con fetch real
setLoading(false);
}, 1000);
};
return (
<div className="flex flex-col gap-6 p-6 h-full">
{/* HEADER */}
<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">Area 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>
<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>
</div>
</div>
{/* TABLA */}
<div className="flex-1 bg-white rounded-xl overflow-hidden shadow-md">
<DataGrid
rows={rows}
columns={columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 5,
},
},
}}
pageSizeOptions={[5]}
sx={{ border: "none", "& .MuiDataGrid-row:hover": { backgroundColor: "rgba(0,0,0,0.03)" } }}
/>
</div>
{/* DIALOG FORM */}
<Dialog open={dialogOpen} onClose={() => { setDialogOpen(false); resetForm(); }}>
<DialogTitle>{editMode ? "Editar Área" : "Agregar Nueva Área"}</DialogTitle>
<DialogContent className="flex flex-col gap-3 min-w-[400px]">
{["name","no","code","sort","pushAddress","note","time"].map((field) => (
<TextField
key={field}
label={field.charAt(0).toUpperCase() + field.slice(1)}
type={field === "sort" ? "number" : "text"}
value={(currentArea as any)[field]}
onChange={(e) => setCurrentArea({ ...currentArea, [field]: field === "sort" ? Number(e.target.value) : 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>
<style>
{`
@keyframes gradientMove {
0% {background-position: 0% 50%;}
50% {background-position: 100% 50%;}
100% {background-position: 0% 50%;}
}
`}
</style>
</div>
);
}

View File

@@ -1,203 +0,0 @@
import { useState } from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { Refresh } from "@mui/icons-material";
import { Button, CircularProgress } from "@mui/material";
interface DataMonitoringItem {
id: number;
sort: number;
areaName: string;
meterSn: string;
communicationTime: string;
positiveTotalFlow: number;
batteryStatus: string;
emDisturbance: string;
valveStatus: string;
positiveFlowRate: string;
deviceId: number;
imei: string;
pci: string;
snr: string;
imsi: string;
}
interface DataMonitoringProps {
subPage: string;
}
export default function DataMonitoring({ subPage: _subPage }: DataMonitoringProps) {
const [rows, setRows] = useState<DataMonitoringItem[]>([
{
id: 1,
sort: 1,
areaName: "Operaciones",
meterSn: "MTR001",
communicationTime: "2024-12-16 14:25:00",
positiveTotalFlow: 1250.5,
batteryStatus: "Good",
emDisturbance: "Normal",
valveStatus: "Open",
positiveFlowRate: "15.2 L/min",
deviceId: 1001,
imei: "351756051523999",
pci: "100",
snr: "12.5",
imsi: "310260123456789"
},
{
id: 2,
sort: 2,
areaName: "Calidad",
meterSn: "MTR002",
communicationTime: "2024-12-16 13:45:00",
positiveTotalFlow: 890.3,
batteryStatus: "Low",
emDisturbance: "High",
valveStatus: "Open",
positiveFlowRate: "8.7 L/min",
deviceId: 1002,
imei: "351756051524000",
pci: "101",
snr: "10.8",
imsi: "310260123456790"
},
{
id: 3,
sort: 3,
areaName: "Mantenimiento",
meterSn: "MTR003",
communicationTime: "2024-12-16 12:30:00",
positiveTotalFlow: 2100.8,
batteryStatus: "Good",
emDisturbance: "Normal",
valveStatus: "Closed",
positiveFlowRate: "0.0 L/min",
deviceId: 1003,
imei: "351756051524001",
pci: "102",
snr: "14.2",
imsi: "310260123456791"
},
{
id: 4,
sort: 4,
areaName: "Operaciones",
meterSn: "MTR004",
communicationTime: "2024-12-16 11:15:00",
positiveTotalFlow: 567.2,
batteryStatus: "Critical",
emDisturbance: "Normal",
valveStatus: "Open",
positiveFlowRate: "22.1 L/min",
deviceId: 1004,
imei: "351756051524002",
pci: "103",
snr: "9.3",
imsi: "310260123456792"
},
{
id: 5,
sort: 5,
areaName: "Calidad",
meterSn: "MTR005",
communicationTime: "2024-12-16 10:00:00",
positiveTotalFlow: 3340.1,
batteryStatus: "Good",
emDisturbance: "Low",
valveStatus: "Open",
positiveFlowRate: "18.9 L/min",
deviceId: 1005,
imei: "351756051524003",
pci: "104",
snr: "13.7",
imsi: "310260123456793"
},
]);
const [loading, setLoading] = useState(false);
const columns: GridColDef[] = [
{ field: "sort", headerName: "Sort", width: 80, type: "number" },
{ field: "areaName", headerName: "Area Name", width: 120 },
{ field: "meterSn", headerName: "Meter S/N", width: 120 },
{ field: "communicationTime", headerName: "Communication Time", width: 160 },
{ field: "positiveTotalFlow", headerName: "Positive Total Flow", width: 140, type: "number" },
{ field: "batteryStatus", headerName: "Battery Status", width: 120 },
{ field: "emDisturbance", headerName: "EM Disturbance", width: 120 },
{ field: "valveStatus", headerName: "Valve Status", width: 110 },
{ field: "positiveFlowRate", headerName: "Positive Flow Rate", width: 140 },
{ field: "deviceId", headerName: "Device ID", width: 100, type: "number" },
{ field: "imei", headerName: "IMEI", width: 140 },
{ field: "pci", headerName: "PCI", width: 80 },
{ field: "snr", headerName: "SNR", width: 80 },
{ field: "imsi", headerName: "IMSI", width: 140 },
];
const handleRefresh = () => {
setLoading(true);
setTimeout(() => {
setRows([...rows]);
setLoading(false);
}, 1000);
};
return (
<div className="flex flex-col gap-6 p-6 h-full">
<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">Data Monitoring</h1>
<div className="flex gap-3">
<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>
</div>
</div>
<div className="flex-1 bg-white rounded-xl overflow-hidden shadow-md">
<DataGrid
rows={rows}
columns={columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 5,
},
},
}}
pageSizeOptions={[5]}
sx={{ border: "none", "& .MuiDataGrid-row:hover": { backgroundColor: "rgba(0,0,0,0.03)" } }}
/>
</div>
<style>
{`
@keyframes gradientMove {
0% {background-position: 0% 50%;}
50% {background-position: 100% 50%;}
100% {background-position: 0% 50%;}
}
`}
</style>
</div>
);
}

View File

@@ -1,174 +0,0 @@
import { useState } from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { Refresh } from "@mui/icons-material";
import { Button, CircularProgress } from "@mui/material";
interface DataQueryItem {
id: number;
sort: number;
areaName: string;
meterSn: string;
communicationTime: string;
positiveTotalFlow: number;
batteryStatus: string;
}
interface DataQueryProps {
subPage: string;
}
export default function DataQuery({ subPage: _subPage }: DataQueryProps) {
const [rows, setRows] = useState<DataQueryItem[]>([
{
id: 1,
sort: 1,
areaName: "Operaciones",
meterSn: "MTR001",
communicationTime: "2024-12-16 14:25:00",
positiveTotalFlow: 88.97,
batteryStatus: "Good"
},
{
id: 2,
sort: 2,
areaName: "Calidad",
meterSn: "MTR002",
communicationTime: "2024-12-16 13:45:00",
positiveTotalFlow: 122.82,
batteryStatus: "Low"
},
{
id: 3,
sort: 3,
areaName: "Mantenimiento",
meterSn: "MTR003",
communicationTime: "2024-12-16 12:30:00",
positiveTotalFlow: 67.45,
batteryStatus: "Good"
},
{
id: 4,
sort: 4,
areaName: "Operaciones",
meterSn: "MTR004",
communicationTime: "2024-12-16 11:15:00",
positiveTotalFlow: 234.67,
batteryStatus: "Critical"
},
{
id: 5,
sort: 5,
areaName: "Calidad",
meterSn: "MTR005",
communicationTime: "2024-12-16 10:00:00",
positiveTotalFlow: 156.23,
batteryStatus: "Good"
},
{
id: 6,
sort: 6,
areaName: "Mantenimiento",
meterSn: "MTR006",
communicationTime: "2024-12-16 09:30:00",
positiveTotalFlow: 78.91,
batteryStatus: "Low"
},
{
id: 7,
sort: 7,
areaName: "Operaciones",
meterSn: "MTR007",
communicationTime: "2024-12-16 08:45:00",
positiveTotalFlow: 189.34,
batteryStatus: "Good"
},
{
id: 8,
sort: 8,
areaName: "Calidad",
meterSn: "MTR008",
communicationTime: "2024-12-16 07:20:00",
positiveTotalFlow: 145.78,
batteryStatus: "Critical"
},
]);
const [loading, setLoading] = useState(false);
const columns: GridColDef[] = [
{ field: "sort", headerName: "Sort", width: 80, type: "number" },
{ field: "areaName", headerName: "Area Name", width: 150 },
{ field: "meterSn", headerName: "Meter S/N", width: 130 },
{ field: "communicationTime", headerName: "Communication Time", width: 180 },
{ field: "positiveTotalFlow", headerName: "Positive Total Flow", width: 160, type: "number" },
{ field: "batteryStatus", headerName: "Battery Status", width: 130 },
];
const handleRefresh = () => {
setLoading(true);
setTimeout(() => {
setRows([...rows]);
setLoading(false);
}, 1000);
};
return (
<div className="flex flex-col gap-6 p-6 h-full w-full">
<div
className="flex justify-between items-center p-5 rounded-xl w-full"
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">Data Query</h1>
<div className="flex gap-3">
<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>
</div>
</div>
<div className="w-full bg-white rounded-xl overflow-hidden shadow-md">
<DataGrid
rows={rows}
columns={columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 5,
},
},
}}
pageSizeOptions={[5]}
sx={{ border: "none", "& .MuiDataGrid-row:hover": { backgroundColor: "rgba(0,0,0,0.03)" } }}
/>
</div>
<style>
{`
@keyframes gradientMove {
0% {background-position: 0% 50%;}
50% {background-position: 100% 50%;}
100% {background-position: 0% 50%;}
}
`}
</style>
</div>
);
}

View File

@@ -1,524 +0,0 @@
import { useEffect, useState } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core";
interface Device {
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 ApiResponse {
records: Device[];
next?: string;
prev?: string;
nestedNext?: string;
nestedPrev?: string;
}
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 [selectedRows, setSelectedRows] = useState<Device[]>([]);
const [loading, setLoading] = useState(false);
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 [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);
setSelectedRows([]);
} 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);
setSelectedRows([]);
} finally {
setLoading(false);
}
};
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 handleEdit = () => {
if (!activeDevice) return;
setEditingId(activeDevice.id);
setForm({ ...activeDevice });
setShowModal(true);
};
const handleDelete = () => {
if (selectedRows.length === 0) return;
const message = selectedRows.length === 1
? "¿Deseas eliminar este dispositivo?"
: `¿Deseas eliminar ${selectedRows.length} dispositivos?`;
if (confirm(message)) {
setDevices((prev) =>
prev.filter((device) => !selectedRows.some(selected => selected.id === device.id))
);
setSelectedRows([]);
setActiveDevice(null);
}
};
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 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 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
onClick={handleEdit}
disabled={selectedRows.length !== 1}
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
${
selectedRows.length !== 1 ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"
}`}
>
<Pencil size={16} /> Edit
</button>
<button
onClick={handleDelete}
disabled={selectedRows.length === 0}
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
${
selectedRows.length === 0 ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"
}`}
>
<Trash2 size={16} />
{selectedRows.length > 0 ? `Delete (${selectedRows.length})` : "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>
<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 ${selectedRows.length > 0 ? `(${selectedRows.length} selected)` : ""}`}
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}
onSelectionChange={(rows) => {
const selectedDevices = rows as Device[];
setSelectedRows(selectedDevices);
// Set active device to the first selected item for editing
setActiveDevice(selectedDevices.length > 0 ? selectedDevices[0] : null);
}}
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,
selection: true,
headerStyle: {
textAlign: "center",
fontWeight: 600,
},
maxBodyHeight: "500px",
tableLayout: "fixed",
}}
isLoading={loading}
/>
</div>
{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>
{`
@keyframes gradientMove {
0% {background-position: 0% 50%;}
50% {background-position: 100% 50%;}
100% {background-position: 0% 50%;}
}
`}
</style>
</div>
);
}

View File

@@ -2,18 +2,29 @@ import { Cpu, Settings, BarChart3, Bell } from "lucide-react";
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from "recharts";
export default function Home() {
// Datos de ejemplo para empresas
const companies = [
{ name: "Empresa A", tomas: 12, alerts: 2, consumption: 320 },
{ name: "Empresa B", tomas: 8, alerts: 0, consumption: 210 },
{ name: "Empresa C", tomas: 15, alerts: 1, consumption: 450 },
];
// Alertas recientes
const alerts = [
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" },
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
{ company: "Empresa B", type: "Inactividad", time: "Hace 8 horas" },
];
// Historial tipo Google
const history = [
{ user: "GRH", action: "Creó un nuevo medidor", target: "SN001", time: "Hace 5 minutos" },
{ user: "CESPT", action: "Actualizó concentrador", target: "Planta 1", time: "Hace 20 minutos" },
{ user: "GRH", action: "Eliminó un usuario", target: "Juan Pérez", time: "Hace 1 hora" },
{ user: "CESPT", action: "Creó un payload", target: "Payload 12", time: "Hace 2 horas" },
{ user: "GRH", action: "Actualizó medidor", target: "SN002", time: "Hace 3 horas" },
];
return (
<div className="flex flex-col p-6 gap-8 w-full">
@@ -77,6 +88,24 @@ export default function Home() {
</div>
</div>
{/* Historial tipo Google */}
<div className="bg-white rounded-xl shadow p-6">
<h2 className="text-lg font-semibold mb-4">Historial Reciente</h2>
<ul className="divide-y divide-gray-200 max-h-60 overflow-y-auto">
{history.map((h, i) => (
<li key={i} className="py-2 flex items-start gap-3">
<span className="text-gray-400 mt-1"></span>
<div className="flex-1">
<p className="text-sm text-gray-700">
<span className="font-semibold">{h.user}</span> {h.action} <span className="font-medium">{h.target}</span>
</p>
<p className="text-xs text-gray-400">{h.time}</p>
</div>
</li>
))}
</ul>
</div>
{/* Últimas alertas */}
<div className="bg-white rounded-xl shadow p-6">
<h2 className="text-lg font-semibold mb-4">Últimas Alertas</h2>

View File

@@ -1,462 +0,0 @@
import { useEffect, useState } from "react";
import {
Plus,
Trash2,
Pencil,
RefreshCcw,
ChevronRight,
ChevronDown,
} from "lucide-react";
import MaterialTable from "@material-table/core";
/* ================= TYPES ================= */
interface Operator {
id: number;
loginName: string;
isSuperAdmin: boolean;
isDisabled: boolean;
userName: string;
cellPhone: string;
createdAt: string;
}
interface Area {
id: number;
name: string;
operators: Operator[];
children?: Area[];
}
/* ================= COMPONENT ================= */
export default function OperatorManagement() {
const [areas, setAreas] = useState<Area[]>([]);
const [selectedArea, setSelectedArea] = useState<Area | null>(null);
const [expandedIds, setExpandedIds] = useState<number[]>([]);
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [activeOperator, setActiveOperator] = useState<Operator | null>(null);
const emptyOperator: Omit<Operator, "id"> = {
loginName: "",
isSuperAdmin: false,
isDisabled: false,
userName: "",
cellPhone: "",
createdAt: new Date().toISOString().slice(0, 10),
};
const [form, setForm] = useState<Omit<Operator, "id">>(emptyOperator);
/* ================= DATA ================= */
const loadData = () => {
const mock: Area[] = [
{
id: 1,
name: "GRH",
operators: [
{
id: 1,
loginName: "admin_grh",
isSuperAdmin: true,
isDisabled: false,
userName: "Juan Pérez",
cellPhone: "664-123-4567",
createdAt: "2024-01-10",
},
],
children: [
{
id: 2,
name: "CESPT",
operators: [
{
id: 2,
loginName: "cespt_admin",
isSuperAdmin: false,
isDisabled: false,
userName: "Carlos Ruiz",
cellPhone: "664-555-8899",
createdAt: "2024-02-02",
},
],
},
],
},
];
setAreas(mock);
setSelectedArea(mock[0]);
setExpandedIds([1]);
setActiveOperator(null);
};
useEffect(() => {
loadData();
}, []);
/* ================= TREE ================= */
const toggleExpand = (id: number) => {
setExpandedIds((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
};
const updateArea = (list: Area[]): Area[] =>
list.map((area) => {
if (area.id === selectedArea?.id) {
const operators = editingId
? area.operators.map((op) =>
op.id === editingId ? { ...op, ...form } : op
)
: [...area.operators, { id: Date.now(), ...form }];
return { ...area, operators };
}
if (area.children) {
return { ...area, children: updateArea(area.children) };
}
return area;
});
/* ================= CRUD ================= */
const handleSave = () => {
setAreas((prev) => {
const updated = updateArea(prev);
// 🔑 volver a apuntar al área actual actualizada
const refreshedArea =
updated.find((a) => a.id === selectedArea?.id) || null;
setSelectedArea(refreshedArea);
return updated;
});
setShowModal(false);
setEditingId(null);
setForm(emptyOperator);
};
const handleEdit = () => {
if (!activeOperator) return;
setEditingId(activeOperator.id);
setForm({ ...activeOperator });
setShowModal(true);
};
const handleDelete = () => {
if (!selectedArea || !activeOperator) return;
const deleteFromTree = (list: Area[]): Area[] =>
list.map((area) => {
if (area.id === selectedArea.id) {
return {
...area,
operators: area.operators.filter(
(op) => op.id !== activeOperator.id
),
};
}
if (area.children) {
return { ...area, children: deleteFromTree(area.children) };
}
return area;
});
setAreas((prev) => deleteFromTree(prev));
setActiveOperator(null);
};
/* ================= FILTER ================= */
const filtered =
selectedArea?.operators.filter(
(op) =>
op.loginName.toLowerCase().includes(search.toLowerCase()) ||
op.userName.toLowerCase().includes(search.toLowerCase())
) || [];
/* ================= TREE RENDER ================= */
const renderTree = (area: Area, level = 0) => {
const expanded = expandedIds.includes(area.id);
return (
<div key={area.id}>
<div
className={`flex items-center gap-1 px-2 py-1 rounded cursor-pointer text-sm
${
selectedArea?.id === area.id
? "bg-blue-50 text-blue-700 font-semibold"
: "hover:bg-gray-50"
}`}
style={{ marginLeft: level * 12 }}
onClick={() => setSelectedArea(area)}
>
{area.children && (
<button onClick={() => toggleExpand(area.id)}>
{expanded ? (
<ChevronDown size={14} />
) : (
<ChevronRight size={14} />
)}
</button>
)}
{area.name}
</div>
{expanded &&
area.children?.map((child) => renderTree(child, level + 1))}
</div>
);
};
/* ================= UI ================= */
return (
<div className="flex gap-6 p-6 w-full bg-gray-100">
{/* SIDEBAR */}
<div className="w-72 bg-white rounded-xl shadow p-4">
<h3 className="text-xs font-semibold text-gray-500 mb-3">
Organizational Structure
</h3>
{areas.map((a) => renderTree(a))}
</div>
{/* MAIN */}
<div className="flex-1 flex flex-col gap-6">
{/* HEADER */}
<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">Operator Management</h1>
<p className="text-sm text-blue-100">{selectedArea?.name}</p>
</div>
<div className="flex items-center gap-3">
{/* ADD */}
<button
onClick={() => {
setForm(emptyOperator);
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>
{/* EDIT */}
<button
onClick={handleEdit}
disabled={!activeOperator}
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
${
!activeOperator ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"
}`}
>
<Pencil size={16} /> Edit
</button>
{/* DELETE */}
<button
onClick={handleDelete}
disabled={!activeOperator}
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
${
!activeOperator ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"
}`}
>
<Trash2 size={16} /> Delete
</button>
{/* REFRESH */}
<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>
{/* SEARCH */}
<input
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder="Search operator..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<MaterialTable
title={selectedArea?.name || "Operators"}
columns={[
{ title: "Login", field: "loginName" },
{
title: "Super Admin",
field: "isSuperAdmin",
render: (rowData) => (
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border ${
rowData.isSuperAdmin
? "text-blue-600 border-blue-600"
: "text-red-600 border-red-600"
}`}
>
{rowData.isSuperAdmin ? "Yes" : "No"}
</span>
),
},
{
title: "Status",
field: "isDisabled",
render: (rowData) => (
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border ${
rowData.isDisabled
? "text-red-600 border-red-600"
: "text-blue-600 border-blue-600"
}`}
>
{rowData.isDisabled ? "Off" : "Active"}
</span>
),
},
{ title: "User", field: "userName" },
{ title: "Phone", field: "cellPhone" },
{ title: "Created", field: "createdAt", type: "date" },
]}
data={filtered}
onRowClick={(_event, rowData) => {
setActiveOperator(rowData as Operator);
}}
actions={[
{
icon: () => <Plus size={16} />,
tooltip: "Add Operator",
isFreeAction: true,
onClick: () => {
setForm(emptyOperator);
setEditingId(null);
setShowModal(true);
},
},
{
icon: () => <Pencil size={16} />,
tooltip: "Edit Operator",
onClick: (_event, rowData) => {
setActiveOperator(rowData as Operator);
setEditingId((rowData as Operator).id);
setForm({ ...(rowData as Operator) });
setShowModal(true);
},
},
{
icon: () => <Trash2 size={16} />,
tooltip: "Delete Operator",
onClick: (_event, rowData) => {
setActiveOperator(rowData as Operator);
handleDelete();
},
},
]}
options={{
actionsColumnIndex: -1,
search: false,
paging: true,
sorting: true,
headerStyle: {
textAlign: "center",
fontWeight: 600,
},
maxBodyHeight: "400px",
tableLayout: "fixed",
rowStyle: (rowData) => ({
backgroundColor:
activeOperator?.id === (rowData as Operator).id
? "#EEF2FF"
: "#FFFFFF",
}),
}}
/>
</div>
{/* 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 space-y-3">
<h2 className="text-lg font-semibold">
{editingId ? "Edit Operator" : "Add Operator"}
</h2>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Login Name"
value={form.loginName}
onChange={(e) => setForm({ ...form, loginName: e.target.value })}
/>
<button
onClick={() =>
setForm({ ...form, isSuperAdmin: !form.isSuperAdmin })
}
className={`w-full border rounded px-3 py-2 ${
form.isSuperAdmin
? "text-blue-600 border-blue-600"
: "text-red-600 border-red-600"
}`}
>
Super Admin: {form.isSuperAdmin ? "Yes" : "No"}
</button>
<button
onClick={() => setForm({ ...form, isDisabled: !form.isDisabled })}
className={`w-full border rounded px-3 py-2 ${
form.isDisabled
? "text-red-600 border-red-600"
: "text-blue-600 border-blue-600"
}`}
>
Status: {form.isDisabled ? "Off" : "Active"}
</button>
<input
className="w-full border px-3 py-2 rounded"
placeholder="User Name"
value={form.userName}
onChange={(e) => setForm({ ...form, userName: e.target.value })}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Cell Phone"
value={form.cellPhone}
onChange={(e) => setForm({ ...form, cellPhone: e.target.value })}
/>
<input
type="date"
className="w-full border px-3 py-2 rounded"
value={form.createdAt}
onChange={(e) => setForm({ ...form, createdAt: e.target.value })}
/>
<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>
)}
</div>
);
}

149
src/pages/RolesPage.tsx Normal file
View File

@@ -0,0 +1,149 @@
import { useState, useEffect } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core";
export interface Role {
id: string;
name: string;
description: string;
status: "ACTIVE" | "INACTIVE";
createdAt: string;
}
export default function RolesPage() {
const initialRoles: Role[] = [
{ id: "1", name: "SUPER_ADMIN", description: "Full access", status: "ACTIVE", createdAt: "2025-12-17" },
{ id: "2", name: "USER", description: "Regular user", status: "ACTIVE", createdAt: "2025-12-16" },
];
const [roles, setRoles] = useState<Role[]>(initialRoles);
const [activeRole, setActiveRole] = useState<Role | null>(null);
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const emptyRole: Omit<Role, "id"> = {
name: "",
description: "",
status: "ACTIVE",
createdAt: new Date().toISOString().slice(0, 10),
};
const [form, setForm] = useState<Omit<Role, "id">>(emptyRole);
const handleSave = () => {
if (editingId) {
setRoles(prev => prev.map(r => r.id === editingId ? { id: editingId, ...form } : r));
} else {
const newId = Date.now().toString();
setRoles(prev => [...prev, { id: newId, ...form }]);
}
setShowModal(false);
setEditingId(null);
setForm(emptyRole);
};
const handleDelete = () => {
if (!activeRole) return;
setRoles(prev => prev.filter(r => r.id !== activeRole.id));
setActiveRole(null);
};
const filtered = roles.filter(r => r.name.toLowerCase().includes(search.toLowerCase()));
return (
<div className="flex gap-6 p-6 w-full bg-gray-100">
{/* LEFT INFO SIDEBAR */}
<div className="w-72 bg-white rounded-xl shadow p-4">
<h3 className="text-xs font-semibold text-gray-500 mb-3">Role Information</h3>
<p className="text-sm text-gray-700">Aquí se listan los roles disponibles en el sistema.</p>
</div>
{/* MAIN */}
<div className="flex-1 flex flex-col gap-6">
{/* HEADER */}
<div className="rounded-xl shadow p-6 text-white flex justify-between items-center"
style={{ background: "linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8)" }}>
<div>
<h1 className="text-2xl font-bold">Role Management</h1>
<p className="text-sm text-blue-100">Roles registrados</p>
</div>
<div className="flex gap-3">
<button onClick={() => { setForm(emptyRole); 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 onClick={() => { if (!activeRole) return; setEditingId(activeRole.id); setForm({...activeRole}); setShowModal(true); }}
disabled={!activeRole}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60">
<Pencil size={16} /> Edit
</button>
<button onClick={handleDelete}
disabled={!activeRole}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60">
<Trash2 size={16} /> Delete
</button>
<button onClick={() => setRoles([...roles])}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg">
<RefreshCcw size={16} /> Refresh
</button>
</div>
</div>
{/* SEARCH */}
<input className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder="Search role..."
value={search}
onChange={e => setSearch(e.target.value)} />
{/* TABLE */}
<MaterialTable
title="Roles"
columns={[
{ title: "Name", field: "name" },
{ title: "Description", field: "description" },
{
title: "Status",
field: "status",
render: (rowData) => (
<span className={`px-3 py-1 rounded-full text-xs font-semibold border ${rowData.status === "ACTIVE" ? "text-blue-600 border-blue-600" : "text-red-600 border-red-600"}`}>
{rowData.status}
</span>
)
},
{ title: "Created", field: "createdAt", type: "date" }
]}
data={filtered}
onRowClick={(_, rowData) => setActiveRole(rowData as Role)}
options={{
actionsColumnIndex: -1,
search: false,
paging: true,
sorting: true,
rowStyle: (rowData) => ({ backgroundColor: activeRole?.id === (rowData as Role).id ? "#EEF2FF" : "#FFFFFF" })
}}
/>
</div>
{/* 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 space-y-3">
<h2 className="text-lg font-semibold">{editingId ? "Edit Role" : "Add Role"}</h2>
<input className="w-full border px-3 py-2 rounded" placeholder="Name" value={form.name} onChange={e => setForm({...form, name: e.target.value})} />
<input className="w-full border px-3 py-2 rounded" placeholder="Description" value={form.description} onChange={e => setForm({...form, description: e.target.value})} />
<button onClick={() => setForm({...form, status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE"})} className="w-full border rounded px-3 py-2">
Status: {form.status}
</button>
<input type="date" className="w-full border px-3 py-2 rounded" value={form.createdAt} onChange={e => setForm({...form, createdAt: e.target.value})} />
<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>
)}
</div>
);
}

View File

@@ -1,143 +0,0 @@
import { useState } from "react";
import {
Home,
Settings,
WaterDrop,
ExpandMore,
ExpandLess,
Menu,
} from "@mui/icons-material";
export default function Sidebar({ setPage }: any) {
const [systemOpen, setSystemOpen] = useState(true);
const [waterOpen, setWaterOpen] = useState(true);
const [pinned, setPinned] = useState(false);
const [hovered, setHovered] = useState(false);
const isExpanded = pinned || hovered;
return (
<aside
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={`
h-full bg-[#1f2a48] border-r border-white/10
transition-all duration-300 flex flex-col overflow-hidden
${isExpanded ? "w-72" : "w-16"}
`}
>
{/* HEADER */}
<div className="border-b border-white/10 px-4 py-3 flex items-center gap-3">
<button
onClick={() => setPinned(!pinned)}
className="text-white opacity-90 hover:opacity-100"
>
<Menu />
</button>
{isExpanded && (
<span className="text-lg font-bold text-white whitespace-nowrap">
Water System
</span>
)}
</div>
{/* MENU */}
<div className="flex-1 py-4 px-2 overflow-y-auto">
<ul className="space-y-1 text-white text-sm">
{/* DASHBOARD */}
<li>
<button
onClick={() => setPage("home")}
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
>
<Home className="w-5 h-5 shrink-0" />
{isExpanded && <span className="ml-3">Dashboard</span>}
</button>
</li>
{/* SYSTEM SETTINGS */}
<li>
<button
onClick={() => isExpanded && setSystemOpen(!systemOpen)}
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
>
<Settings className="w-5 h-5 shrink-0" />
{isExpanded && (
<>
<span className="ml-3 flex-1 text-left">
System Settings
</span>
{systemOpen ? <ExpandLess /> : <ExpandMore />}
</>
)}
</button>
{isExpanded && systemOpen && (
<ul className="mt-1 space-y-1 text-xs">
<li>
<button
onClick={() => setPage("area")}
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
>
Area Management
</button>
</li>
<li>
<button
onClick={() => setPage("operator")}
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
>
Operator Management
</button>
</li>
</ul>
)}
</li>
{/* WATER METER SYSTEM */}
<li>
<button
onClick={() => isExpanded && setWaterOpen(!waterOpen)}
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
>
<WaterDrop className="w-5 h-5 shrink-0" />
{isExpanded && (
<>
<span className="ml-3 flex-1 text-left">
Water Meter System Management
</span>
{waterOpen ? <ExpandLess /> : <ExpandMore />}
</>
)}
</button>
{isExpanded && waterOpen && (
<ul className="mt-1 space-y-1 text-xs">
{[
["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]) => (
<li key={key}>
<button
onClick={() => setPage(key)}
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
>
{label}
</button>
</li>
))}
</ul>
)}
</li>
</ul>
</div>
</aside>
);
}

View File

@@ -1,61 +0,0 @@
import React from "react";
import { Bell, User, Settings } from "lucide-react";
interface TopMenuProps {
page: string;
subPage: string;
setSubPage: (subPage: string) => void;
}
const TopMenu: React.FC<TopMenuProps> = ({ page, subPage, setSubPage }) => {
return (
<header
className="h-14 shrink-0 flex items-center justify-between px-4 text-white"
style={{
background:
"linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8, #3d4e87)",
}}
>
{/* IZQUIERDA */}
<div className="flex items-center gap-2 text-sm font-medium opacity-90">
{page !== "home" && (
<>
<span className="capitalize">{page}</span>
{subPage !== "default" && (
<>
<span className="opacity-60">/</span>
<span className="capitalize">{subPage}</span>
</>
)}
</>
)}
</div>
{/* DERECHA */}
<div className="flex items-center gap-3">
<button
aria-label="Notificaciones"
className="p-2 rounded-full hover:bg-white/10 transition"
>
<Bell size={20} />
</button>
<button
aria-label="Configuración"
className="p-2 rounded-full hover:bg-white/10 transition"
>
<Settings size={20} />
</button>
<div
className="w-9 h-9 rounded-full bg-white/15 flex items-center justify-center cursor-pointer hover:bg-white/25 transition"
title="Perfil"
>
<User size={20} />
</div>
</div>
</header>
);
};
export default TopMenu;

128
src/pages/UsersPage.tsx Normal file
View File

@@ -0,0 +1,128 @@
import { useState, useEffect } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core";
import { Role } from "./RolesPage"; // Importa los tipos de roles
interface User {
id: string;
name: string;
email: string;
roleId: string;
roleName: string;
status: "ACTIVE" | "INACTIVE";
createdAt: string;
}
export default function UsersPage() {
const initialRoles: Role[] = [
{ id: "1", name: "SUPER_ADMIN", description: "Full access", status: "ACTIVE", createdAt: "2025-12-17" },
{ id: "2", name: "USER", description: "Regular user", status: "ACTIVE", createdAt: "2025-12-16" },
];
const initialUsers: User[] = [
{ id: "1", name: "Admin GRH", email: "grh@domain.com", roleId: "1", roleName: "SUPER_ADMIN", status: "ACTIVE", createdAt: "2025-12-17" },
{ id: "2", name: "User CESPT", email: "cespt@domain.com", roleId: "2", roleName: "USER", status: "ACTIVE", createdAt: "2025-12-16" },
];
const [users, setUsers] = useState<User[]>(initialUsers);
const [activeUser, setActiveUser] = useState<User | null>(null);
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [roles, setRoles] = useState<Role[]>(initialRoles);
const emptyUser: Omit<User, "id" | "roleName"> = { name: "", email: "", roleId: "", status: "ACTIVE", createdAt: new Date().toISOString().slice(0,10) };
const [form, setForm] = useState<Omit<User, "id" | "roleName">>(emptyUser);
const handleSave = () => {
const roleName = roles.find(r => r.id === form.roleId)?.name || "";
if (editingId) {
setUsers(prev => prev.map(u => u.id === editingId ? { id: editingId, roleName, ...form } : u));
} else {
const newId = Date.now().toString();
setUsers(prev => [...prev, { id: newId, roleName, ...form }]);
}
setShowModal(false);
setEditingId(null);
setForm(emptyUser);
};
const handleDelete = () => {
if (!activeUser) return;
setUsers(prev => prev.filter(u => u.id !== activeUser.id));
setActiveUser(null);
};
const filtered = users.filter(u => u.name.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase()));
return (
<div className="flex gap-6 p-6 w-full bg-gray-100">
{/* LEFT INFO SIDEBAR */}
<div className="w-72 bg-white rounded-xl shadow p-4">
<h3 className="text-xs font-semibold text-gray-500 mb-3">Project Information</h3>
<p className="text-sm text-gray-700">Usuarios disponibles y sus roles.</p>
<select value={form.roleId} onChange={e => setForm({...form, roleId: e.target.value})} className="w-full border px-3 py-2 rounded mt-2">
<option value="">Select Role</option>
{roles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
</select>
</div>
{/* MAIN */}
<div className="flex-1 flex flex-col gap-6">
{/* HEADER */}
<div className="rounded-xl shadow p-6 text-white flex justify-between items-center"
style={{ background: "linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8)" }}>
<div>
<h1 className="text-2xl font-bold">User Management</h1>
<p className="text-sm text-blue-100">Usuarios registrados</p>
</div>
<div className="flex gap-3">
<button onClick={() => { setForm(emptyUser); 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 onClick={() => { if(!activeUser) return; setEditingId(activeUser.id); setForm({...activeUser}); setShowModal(true); }} disabled={!activeUser} className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"><Pencil size={16}/> Edit</button>
<button onClick={handleDelete} disabled={!activeUser} className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"><Trash2 size={16}/> Delete</button>
<button onClick={() => setUsers([...users])} className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg"><RefreshCcw size={16}/> Refresh</button>
</div>
</div>
{/* SEARCH */}
<input className="bg-white rounded-lg shadow px-4 py-2 text-sm" placeholder="Search user..." value={search} onChange={e => setSearch(e.target.value)} />
{/* TABLE */}
<MaterialTable
title="Users"
columns={[
{ title: "Name", field: "name" },
{ title: "Email", field: "email" },
{ title: "Role", field: "roleName" },
{ title: "Status", field: "status", render: rowData => <span className={`px-3 py-1 rounded-full text-xs font-semibold border ${rowData.status === "ACTIVE" ? "text-blue-600 border-blue-600" : "text-red-600 border-red-600"}`}>{rowData.status}</span> },
{ title: "Created", field: "createdAt", type: "date" }
]}
data={filtered}
onRowClick={(_, rowData) => setActiveUser(rowData as User)}
options={{ actionsColumnIndex: -1, search: false, paging: true, sorting: true, rowStyle: rowData => ({ backgroundColor: activeUser?.id === (rowData as User).id ? "#EEF2FF" : "#FFFFFF" }) }}
/>
</div>
{/* 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 space-y-3">
<h2 className="text-lg font-semibold">{editingId ? "Edit User" : "Add User"}</h2>
<input className="w-full border px-3 py-2 rounded" placeholder="Name" value={form.name} onChange={e => setForm({...form, name: e.target.value})} />
<input className="w-full border px-3 py-2 rounded" placeholder="Email" value={form.email} onChange={e => setForm({...form, email: e.target.value})} />
<select value={form.roleId} onChange={e => setForm({...form, roleId: e.target.value})} className="w-full border px-3 py-2 rounded mt-2">
<option value="">Select Role</option>
{roles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
</select>
<button onClick={() => setForm({...form, status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE"})} className="w-full border rounded px-3 py-2">Status: {form.status}</button>
<input type="date" className="w-full border px-3 py-2 rounded" value={form.createdAt} onChange={e => setForm({...form, createdAt: e.target.value})} />
<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>
)}
</div>
);
}

View File

@@ -0,0 +1,302 @@
import { useState } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core";
/* ================= TYPES ================= */
interface Concentrator {
id: number;
name: string;
location: string;
status: "ACTIVE" | "INACTIVE";
project: string;
createdAt: string;
}
interface User {
name: string;
role: "SUPER_ADMIN" | "USER";
project?: string; // asignado si no es superadmin
}
/* ================= COMPONENT ================= */
export default function ConcentratorsPage() {
// Simulación de usuario actual
const currentUser: User = {
name: "Admin GRH",
role: "SUPER_ADMIN", // cambiar a USER para probar otro caso
project: "CESPT",
};
// Lista de proyectos disponibles
const allProjects = ["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"];
// Proyectos visibles según el usuario
const visibleProjects =
currentUser.role === "SUPER_ADMIN"
? allProjects
: currentUser.project
? [currentUser.project]
: [];
const [selectedProject, setSelectedProject] = useState(
visibleProjects[0] || ""
);
const [concentrators, setConcentrators] = useState<Concentrator[]>([
{
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 [activeConcentrator, setActiveConcentrator] = useState<Concentrator | null>(null);
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const emptyConcentrator: Omit<Concentrator, "id"> = {
name: "",
location: "",
status: "ACTIVE",
project: selectedProject,
createdAt: new Date().toISOString().slice(0, 10),
};
const [form, setForm] = useState<Omit<Concentrator, "id">>(emptyConcentrator);
/* ================= CRUD ================= */
const handleSave = () => {
if (editingId) {
setConcentrators((prev) =>
prev.map((c) =>
c.id === editingId ? { id: editingId, ...form } : c
)
);
} else {
const newId = Date.now();
setConcentrators((prev) => [...prev, { id: newId, ...form }]);
}
setShowModal(false);
setEditingId(null);
setForm({ ...emptyConcentrator, project: selectedProject });
setActiveConcentrator(null);
};
const handleDelete = () => {
if (!activeConcentrator) return;
setConcentrators((prev) =>
prev.filter((c) => c.id !== activeConcentrator.id)
);
setActiveConcentrator(null);
};
/* ================= FILTER ================= */
const filtered = concentrators.filter(
(c) =>
(c.name.toLowerCase().includes(search.toLowerCase()) ||
c.location.toLowerCase().includes(search.toLowerCase())) &&
c.project === selectedProject
);
/* ================= UI ================= */
return (
<div className="flex gap-6 p-6 w-full bg-gray-100">
{/* LEFT INFO SIDEBAR */}
<div className="w-72 bg-white rounded-xl shadow p-4">
<h3 className="text-xs font-semibold text-gray-500 mb-3">
Project Information
</h3>
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
className="w-full border px-3 py-2 rounded"
>
{visibleProjects.map((proj) => (
<option key={proj} value={proj}>
{proj}
</option>
))}
</select>
</div>
{/* MAIN */}
<div className="flex-1 flex flex-col gap-6">
{/* HEADER */}
<div
className="rounded-xl shadow p-6 text-white flex justify-between items-center"
style={{ background: "linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8)" }}
>
<div>
<h1 className="text-2xl font-bold">Concentrator Management</h1>
<p className="text-sm text-blue-100">Concentradores registrados</p>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setForm({ ...emptyConcentrator, project: selectedProject });
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
onClick={() => {
if (!activeConcentrator) return;
setEditingId(activeConcentrator.id);
setForm({ ...activeConcentrator });
setShowModal(true);
}}
disabled={!activeConcentrator}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Pencil size={16} /> Edit
</button>
<button
onClick={handleDelete}
disabled={!activeConcentrator}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Trash2 size={16} /> Delete
</button>
<button
onClick={() => setConcentrators([...concentrators])}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg"
>
<RefreshCcw size={16} /> Refresh
</button>
</div>
</div>
{/* SEARCH */}
<input
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder="Search concentrator..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{/* TABLE */}
<MaterialTable
title="Concentrators"
columns={[
{ title: "Name", field: "name" },
{
title: "Status",
field: "status",
render: (rowData) => (
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border ${
rowData.status === "ACTIVE"
? "text-blue-600 border-blue-600"
: "text-red-600 border-red-600"
}`}
>
{rowData.status}
</span>
),
},
{ title: "Location", field: "location" },
{ title: "Project", field: "project" },
{ title: "Created", field: "createdAt", type: "date" },
]}
data={filtered}
onRowClick={(_, rowData) => setActiveConcentrator(rowData as Concentrator)}
options={{
actionsColumnIndex: -1,
search: false,
paging: true,
sorting: true,
rowStyle: (rowData) => ({
backgroundColor:
activeConcentrator?.id === (rowData as Concentrator).id
? "#EEF2FF"
: "#FFFFFF",
}),
}}
/>
</div>
{/* 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 space-y-3">
<h2 className="text-lg font-semibold">
{editingId ? "Edit Concentrator" : "Add Concentrator"}
</h2>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Name"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Location"
value={form.location}
onChange={(e) => setForm({ ...form, location: e.target.value })}
/>
<button
onClick={() =>
setForm({
...form,
status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE",
})
}
className="w-full border rounded px-3 py-2"
>
Status: {form.status}
</button>
<input
type="date"
className="w-full border px-3 py-2 rounded"
value={form.createdAt}
onChange={(e) => setForm({ ...form, createdAt: e.target.value })}
/>
<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>
)}
</div>
);
}

View File

@@ -0,0 +1,315 @@
import { useState } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import MaterialTable from "@material-table/core";
/* ================= TYPES ================= */
export interface Meter {
id: string; // recordId
serialNumber: string;
status: "ACTIVE" | "INACTIVE";
project: string;
createdAt: string;
}
interface User {
name: string;
role: "SUPER_ADMIN" | "USER";
project?: string; // asignado si no es superadmin
}
/* ================= COMPONENT ================= */
export default function MeterManagement() {
// Simulación de usuario actual
const currentUser: User = {
name: "Admin GRH",
role: "SUPER_ADMIN", // cambiar a USER para probar otro caso
project: "CESPT",
};
// Lista de proyectos disponibles
const allProjects = ["GRH (PADRE)", "CESPT", "Proyecto A", "Proyecto B"];
// Proyectos visibles según el usuario
const visibleProjects =
currentUser.role === "SUPER_ADMIN"
? allProjects
: currentUser.project
? [currentUser.project]
: [];
const [selectedProject, setSelectedProject] = useState(
visibleProjects[0] || ""
);
// 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 [meters, setMeters] = useState<Meter[]>(initialMeters);
const [activeMeter, setActiveMeter] = useState<Meter | null>(null);
const [search, setSearch] = useState("");
const [showModal, setShowModal] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const emptyMeter: Omit<Meter, "id"> = {
serialNumber: "",
status: "ACTIVE",
project: selectedProject,
createdAt: new Date().toISOString().slice(0, 10),
};
const [form, setForm] = useState<Omit<Meter, "id">>(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]);
}
setShowModal(false);
setEditingId(null);
setForm({ ...emptyMeter, project: selectedProject });
setActiveMeter(null);
};
const handleDelete = () => {
if (!activeMeter) return;
setMeters((prev) => prev.filter((m) => m.id !== activeMeter.id));
setActiveMeter(null);
};
const handleRefresh = () => {
// Simula recargar los datos originales
setMeters(initialMeters);
setActiveMeter(null);
};
/* ================= FILTER ================= */
const filtered = meters.filter(
(m) =>
(m.serialNumber.toLowerCase().includes(search.toLowerCase()) ||
m.project.toLowerCase().includes(search.toLowerCase())) &&
m.project === selectedProject
);
/* ================= UI ================= */
return (
<div className="flex gap-6 p-6 w-full bg-gray-100">
{/* LEFT INFO SIDEBAR */}
<div className="w-72 bg-white rounded-xl shadow p-4">
<h3 className="text-xs font-semibold text-gray-500 mb-3">
Project Information
</h3>
<select
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
className="w-full border px-3 py-2 rounded"
>
{visibleProjects.map((proj) => (
<option key={proj} value={proj}>
{proj}
</option>
))}
</select>
</div>
{/* MAIN */}
<div className="flex-1 flex flex-col gap-6">
{/* HEADER */}
<div
className="rounded-xl shadow p-6 text-white flex justify-between items-center"
style={{
background:
"linear-gradient(135deg, #4c5f9e, #2a355d, #566bb8)",
}}
>
<div>
<h1 className="text-2xl font-bold">Meter Management</h1>
<p className="text-sm text-blue-100">Medidores registrados</p>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setForm({ ...emptyMeter, project: selectedProject });
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
onClick={() => {
if (!activeMeter) return;
setEditingId(activeMeter.id);
setForm({ ...activeMeter });
setShowModal(true);
}}
disabled={!activeMeter}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Pencil size={16} /> Edit
</button>
<button
onClick={handleDelete}
disabled={!activeMeter}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Trash2 size={16} /> Delete
</button>
<button
onClick={handleRefresh}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg"
>
<RefreshCcw size={16} /> Refresh
</button>
</div>
</div>
{/* SEARCH */}
<input
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder="Search meter..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{/* TABLE */}
<MaterialTable
title="Meters"
columns={[
{ title: "Serial", field: "serialNumber" },
{
title: "Status",
field: "status",
render: (rowData) => (
<span
className={`px-3 py-1 rounded-full text-xs font-semibold border ${
rowData.status === "ACTIVE"
? "text-blue-600 border-blue-600"
: "text-red-600 border-red-600"
}`}
>
{rowData.status}
</span>
),
},
{ title: "Project", field: "project" },
{ title: "Created", field: "createdAt", type: "date" },
]}
data={filtered}
onRowClick={(_, rowData) => setActiveMeter(rowData as Meter)}
options={{
actionsColumnIndex: -1,
search: false,
paging: true,
sorting: true,
rowStyle: (rowData) => ({
backgroundColor:
activeMeter?.id === (rowData as Meter).id
? "#EEF2FF"
: "#FFFFFF",
}),
}}
/>
</div>
{/* 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 space-y-3">
<h2 className="text-lg font-semibold">
{editingId ? "Edit Meter" : "Add Meter"}
</h2>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Serial Number"
value={form.serialNumber}
onChange={(e) =>
setForm({ ...form, serialNumber: e.target.value })
}
/>
<button
onClick={() =>
setForm({
...form,
status: form.status === "ACTIVE" ? "INACTIVE" : "ACTIVE",
})
}
className="w-full border rounded px-3 py-2"
>
Status: {form.status}
</button>
<input
className="w-full border px-3 py-2 rounded"
placeholder="Project"
value={form.project}
onChange={(e) =>
setForm({ ...form, project: e.target.value })
}
/>
<input
type="date"
className="w-full border px-3 py-2 rounded"
value={form.createdAt}
onChange={(e) =>
setForm({ ...form, createdAt: e.target.value })
}
/>
<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>
)}
</div>
);
}

View File