refactor: reorganización de estructura, pages y components
This commit is contained in:
55
src/App.tsx
55
src/App.tsx
@@ -1,49 +1,38 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Sidebar from "./pages/Sidebar";
|
import Sidebar from "./components/layout/Sidebar";
|
||||||
import TopMenu from "./pages/TopMenu";
|
import TopMenu from "./components/layout/TopMenu";
|
||||||
import AreaManagement from "./pages/AreaManagement";
|
|
||||||
import OperatorManagement from "./pages/OperatorManagement";
|
|
||||||
import DeviceManagement from "./pages/DeviceManagement";
|
|
||||||
import DataMonitoring from "./pages/DataMonitoring";
|
|
||||||
import DataQuery from "./pages/DataQuery";
|
|
||||||
import Home from "./pages/Home";
|
|
||||||
|
|
||||||
|
import Home from "./pages/Home";
|
||||||
|
import MetersPage from "./pages/meters/MeterPage";
|
||||||
|
import ConcentratorsPage from "./pages/concentrators/ConcentratorsPage";
|
||||||
|
import UsersPage from "./pages/UsersPage"; // nueva página
|
||||||
|
import RolesPage from "./pages/RolesPage"; // nueva página
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [page, setPage] = useState<string>("home");
|
const [page, setPage] = useState("home");
|
||||||
const [subPage, setSubPage] = useState<string>("default");
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderPage = () => {
|
||||||
switch (page) {
|
switch (page) {
|
||||||
|
case "meters":
|
||||||
|
return <MetersPage />;
|
||||||
|
case "concentrators":
|
||||||
|
return <ConcentratorsPage />;
|
||||||
|
case "users":
|
||||||
|
return <UsersPage />; // nueva
|
||||||
|
case "roles":
|
||||||
|
return <RolesPage />; // nueva
|
||||||
case "home":
|
case "home":
|
||||||
return <Home />;
|
|
||||||
case "area":
|
|
||||||
return <AreaManagement />;
|
|
||||||
case "operator":
|
|
||||||
return <OperatorManagement />;
|
|
||||||
case "device-management":
|
|
||||||
return <DeviceManagement />;
|
|
||||||
case "data-monitoring":
|
|
||||||
return <DataMonitoring subPage={subPage} />;
|
|
||||||
case "data-query":
|
|
||||||
return <DataQuery subPage={subPage} />;
|
|
||||||
default:
|
default:
|
||||||
return <div>Selecciona una opción</div>;
|
return <Home />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen overflow-hidden">
|
<div className="flex h-screen">
|
||||||
{/* SIDEBAR */}
|
|
||||||
<Sidebar setPage={setPage} />
|
<Sidebar setPage={setPage} />
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
{/* MAIN */}
|
<TopMenu />
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<main className="flex-1 overflow-auto">{renderPage()}</main>
|
||||||
<TopMenu page={page} subPage={subPage} setSubPage={setSubPage} />
|
|
||||||
|
|
||||||
<main className="flex-1 overflow-auto bg-gray-100 p-4">
|
|
||||||
{renderContent()}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,11 +6,18 @@ import {
|
|||||||
ExpandMore,
|
ExpandMore,
|
||||||
ExpandLess,
|
ExpandLess,
|
||||||
Menu,
|
Menu,
|
||||||
|
People,
|
||||||
|
Key
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
|
||||||
export default function Sidebar({ setPage }: any) {
|
interface SidebarProps {
|
||||||
|
setPage: (page: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Sidebar({ setPage }: SidebarProps) {
|
||||||
const [systemOpen, setSystemOpen] = useState(true);
|
const [systemOpen, setSystemOpen] = useState(true);
|
||||||
const [waterOpen, setWaterOpen] = useState(true);
|
const [waterOpen, setWaterOpen] = useState(true);
|
||||||
|
const [usersOpen, setUsersOpen] = useState(true); // Nuevo
|
||||||
const [pinned, setPinned] = useState(false);
|
const [pinned, setPinned] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
@@ -34,7 +41,6 @@ export default function Sidebar({ setPage }: any) {
|
|||||||
>
|
>
|
||||||
<Menu />
|
<Menu />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<span className="text-lg font-bold text-white whitespace-nowrap">
|
<span className="text-lg font-bold text-white whitespace-nowrap">
|
||||||
Water System
|
Water System
|
||||||
@@ -57,7 +63,7 @@ export default function Sidebar({ setPage }: any) {
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/* SYSTEM SETTINGS */}
|
{/* PROJECT MANAGEMENT */}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => isExpanded && setSystemOpen(!systemOpen)}
|
onClick={() => isExpanded && setSystemOpen(!systemOpen)}
|
||||||
@@ -66,9 +72,7 @@ export default function Sidebar({ setPage }: any) {
|
|||||||
<Settings className="w-5 h-5 shrink-0" />
|
<Settings className="w-5 h-5 shrink-0" />
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<>
|
<>
|
||||||
<span className="ml-3 flex-1 text-left">
|
<span className="ml-3 flex-1 text-left">Project Management</span>
|
||||||
System Settings
|
|
||||||
</span>
|
|
||||||
{systemOpen ? <ExpandLess /> : <ExpandMore />}
|
{systemOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -78,25 +82,25 @@ export default function Sidebar({ setPage }: any) {
|
|||||||
<ul className="mt-1 space-y-1 text-xs">
|
<ul className="mt-1 space-y-1 text-xs">
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage("area")}
|
onClick={() => setPage("concentrators")}
|
||||||
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Area Management
|
Concentradores
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage("operator")}
|
onClick={() => setPage("meters")}
|
||||||
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Operator Management
|
Medidores
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{/* WATER METER SYSTEM */}
|
{/* WATER METER SYSTEM
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => isExpanded && setWaterOpen(!waterOpen)}
|
onClick={() => isExpanded && setWaterOpen(!waterOpen)}
|
||||||
@@ -105,9 +109,7 @@ export default function Sidebar({ setPage }: any) {
|
|||||||
<WaterDrop className="w-5 h-5 shrink-0" />
|
<WaterDrop className="w-5 h-5 shrink-0" />
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<>
|
<>
|
||||||
<span className="ml-3 flex-1 text-left">
|
<span className="ml-3 flex-1 text-left">Water Meter System Management</span>
|
||||||
Water Meter System Management
|
|
||||||
</span>
|
|
||||||
{waterOpen ? <ExpandLess /> : <ExpandMore />}
|
{waterOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -135,6 +137,45 @@ export default function Sidebar({ setPage }: any) {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
*}
|
||||||
|
|
||||||
|
{/* SYSTEM USERS */}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => isExpanded && setUsersOpen(!usersOpen)}
|
||||||
|
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
|
||||||
|
>
|
||||||
|
<People className="w-5 h-5 shrink-0" />
|
||||||
|
{isExpanded && (
|
||||||
|
<>
|
||||||
|
<span className="ml-3 flex-1 text-left">Users Management</span>
|
||||||
|
{usersOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && usersOpen && (
|
||||||
|
<ul className="mt-1 space-y-1 text-xs">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage("users")}
|
||||||
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage("roles")}
|
||||||
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
|
>
|
||||||
|
Roles
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
0
src/components/layout/common/ConfirmModal.tsx
Normal file
0
src/components/layout/common/ConfirmModal.tsx
Normal 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,29 @@ import { Cpu, Settings, BarChart3, Bell } from "lucide-react";
|
|||||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from "recharts";
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from "recharts";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
// Datos de ejemplo para empresas
|
||||||
const companies = [
|
const companies = [
|
||||||
{ name: "Empresa A", tomas: 12, alerts: 2, consumption: 320 },
|
{ name: "Empresa A", tomas: 12, alerts: 2, consumption: 320 },
|
||||||
{ name: "Empresa B", tomas: 8, alerts: 0, consumption: 210 },
|
{ name: "Empresa B", tomas: 8, alerts: 0, consumption: 210 },
|
||||||
{ name: "Empresa C", tomas: 15, alerts: 1, consumption: 450 },
|
{ name: "Empresa C", tomas: 15, alerts: 1, consumption: 450 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Alertas recientes
|
||||||
const alerts = [
|
const alerts = [
|
||||||
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" },
|
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" },
|
||||||
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
|
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
|
||||||
{ company: "Empresa B", type: "Inactividad", time: "Hace 8 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 (
|
return (
|
||||||
<div className="flex flex-col p-6 gap-8 w-full">
|
<div className="flex flex-col p-6 gap-8 w-full">
|
||||||
|
|
||||||
@@ -77,6 +88,24 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Últimas alertas */}
|
||||||
<div className="bg-white rounded-xl shadow p-6">
|
<div className="bg-white rounded-xl shadow p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">Últimas Alertas</h2>
|
<h2 className="text-lg font-semibold mb-4">Últimas Alertas</h2>
|
||||||
|
|||||||
@@ -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
149
src/pages/RolesPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
128
src/pages/UsersPage.tsx
Normal file
128
src/pages/UsersPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
302
src/pages/concentrators/ConcentratorsPage.tsx
Normal file
302
src/pages/concentrators/ConcentratorsPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/pages/concentrators/concentrators.api.ts
Normal file
0
src/pages/concentrators/concentrators.api.ts
Normal file
315
src/pages/meters/MeterPage.tsx
Normal file
315
src/pages/meters/MeterPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/pages/meters/meters.tapi.ts
Normal file
0
src/pages/meters/meters.tapi.ts
Normal file
Reference in New Issue
Block a user