Device management table edit and delete logic
This commit is contained in:
@@ -37,6 +37,7 @@ export default function DeviceManagement() {
|
||||
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"> = {
|
||||
@@ -70,6 +71,7 @@ export default function DeviceManagement() {
|
||||
const data: ApiResponse = await response.json();
|
||||
setDevices(data.records);
|
||||
setActiveDevice(null);
|
||||
setSelectedRows([]);
|
||||
} catch (error) {
|
||||
console.error("Error loading devices:", error);
|
||||
const mockData: Device[] = [
|
||||
@@ -115,6 +117,7 @@ export default function DeviceManagement() {
|
||||
},
|
||||
];
|
||||
setDevices(mockData);
|
||||
setSelectedRows([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -152,12 +155,17 @@ export default function DeviceManagement() {
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!activeDevice) return;
|
||||
if (selectedRows.length === 0) return;
|
||||
|
||||
if (confirm("¿Deseas eliminar este dispositivo?")) {
|
||||
const message = selectedRows.length === 1
|
||||
? "¿Deseas eliminar este dispositivo?"
|
||||
: `¿Deseas eliminar ${selectedRows.length} dispositivos?`;
|
||||
|
||||
if (confirm(message)) {
|
||||
setDevices((prev) =>
|
||||
prev.filter((device) => device.id !== activeDevice.id)
|
||||
prev.filter((device) => !selectedRows.some(selected => selected.id === device.id))
|
||||
);
|
||||
setSelectedRows([]);
|
||||
setActiveDevice(null);
|
||||
}
|
||||
};
|
||||
@@ -203,20 +211,25 @@ export default function DeviceManagement() {
|
||||
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
disabled={!activeDevice}
|
||||
disabled={selectedRows.length !== 1}
|
||||
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
|
||||
${!activeDevice ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"}`}
|
||||
${
|
||||
selectedRows.length !== 1 ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
<Pencil size={16} /> Edit
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={!activeDevice}
|
||||
disabled={selectedRows.length === 0}
|
||||
className={`flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg
|
||||
${!activeDevice ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"}`}
|
||||
${
|
||||
selectedRows.length === 0 ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
<Trash2 size={16} /> Delete
|
||||
<Trash2 size={16} />
|
||||
{selectedRows.length > 0 ? `Delete (${selectedRows.length})` : "Delete"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -236,7 +249,7 @@ export default function DeviceManagement() {
|
||||
/>
|
||||
|
||||
<MaterialTable
|
||||
title="Devices"
|
||||
title={`Devices ${selectedRows.length > 0 ? `(${selectedRows.length} selected)` : ""}`}
|
||||
columns={[
|
||||
{ title: "Area Name", field: "Area Name" },
|
||||
{ title: "Account Number", field: "Account Number" },
|
||||
@@ -275,8 +288,11 @@ export default function DeviceManagement() {
|
||||
},
|
||||
]}
|
||||
data={filteredDevices}
|
||||
onRowClick={(_event, rowData) => {
|
||||
setActiveDevice(rowData as Device);
|
||||
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={[
|
||||
{
|
||||
@@ -303,18 +319,13 @@ export default function DeviceManagement() {
|
||||
search: false,
|
||||
paging: true,
|
||||
sorting: true,
|
||||
selection: true,
|
||||
headerStyle: {
|
||||
textAlign: "center",
|
||||
fontWeight: 600,
|
||||
},
|
||||
maxBodyHeight: "500px",
|
||||
tableLayout: "fixed",
|
||||
rowStyle: (rowData) => ({
|
||||
backgroundColor:
|
||||
activeDevice?.id === (rowData as Device).id
|
||||
? "#EEF2FF"
|
||||
: "#FFFFFF",
|
||||
}),
|
||||
}}
|
||||
isLoading={loading}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import MaterialTable from "@material-table/core";
|
||||
|
||||
|
||||
/* ================= TYPES ================= */
|
||||
interface Operator {
|
||||
id: number;
|
||||
@@ -98,16 +97,16 @@ export default function OperatorManagement() {
|
||||
|
||||
/* ================= TREE ================= */
|
||||
const toggleExpand = (id: number) => {
|
||||
setExpandedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
|
||||
setExpandedIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const updateArea = (list: Area[]): Area[] =>
|
||||
list.map(area => {
|
||||
list.map((area) => {
|
||||
if (area.id === selectedArea?.id) {
|
||||
const operators = editingId
|
||||
? area.operators.map(op =>
|
||||
? area.operators.map((op) =>
|
||||
op.id === editingId ? { ...op, ...form } : op
|
||||
)
|
||||
: [...area.operators, { id: Date.now(), ...form }];
|
||||
@@ -122,13 +121,12 @@ export default function OperatorManagement() {
|
||||
|
||||
/* ================= CRUD ================= */
|
||||
const handleSave = () => {
|
||||
setAreas(prev => {
|
||||
setAreas((prev) => {
|
||||
const updated = updateArea(prev);
|
||||
|
||||
// 🔑 volver a apuntar al área actual actualizada
|
||||
const refreshedArea = updated.find(
|
||||
a => a.id === selectedArea?.id
|
||||
) || null;
|
||||
const refreshedArea =
|
||||
updated.find((a) => a.id === selectedArea?.id) || null;
|
||||
|
||||
setSelectedArea(refreshedArea);
|
||||
return updated;
|
||||
@@ -150,12 +148,12 @@ export default function OperatorManagement() {
|
||||
if (!selectedArea || !activeOperator) return;
|
||||
|
||||
const deleteFromTree = (list: Area[]): Area[] =>
|
||||
list.map(area => {
|
||||
list.map((area) => {
|
||||
if (area.id === selectedArea.id) {
|
||||
return {
|
||||
...area,
|
||||
operators: area.operators.filter(
|
||||
op => op.id !== activeOperator.id
|
||||
(op) => op.id !== activeOperator.id
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -165,14 +163,14 @@ export default function OperatorManagement() {
|
||||
return area;
|
||||
});
|
||||
|
||||
setAreas(prev => deleteFromTree(prev));
|
||||
setAreas((prev) => deleteFromTree(prev));
|
||||
setActiveOperator(null);
|
||||
};
|
||||
|
||||
/* ================= FILTER ================= */
|
||||
const filtered =
|
||||
selectedArea?.operators.filter(
|
||||
op =>
|
||||
(op) =>
|
||||
op.loginName.toLowerCase().includes(search.toLowerCase()) ||
|
||||
op.userName.toLowerCase().includes(search.toLowerCase())
|
||||
) || [];
|
||||
@@ -195,28 +193,22 @@ export default function OperatorManagement() {
|
||||
>
|
||||
{area.children && (
|
||||
<button onClick={() => toggleExpand(area.id)}>
|
||||
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
{expanded ? (
|
||||
<ChevronDown size={14} />
|
||||
) : (
|
||||
<ChevronRight size={14} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{area.name}
|
||||
</div>
|
||||
|
||||
{expanded &&
|
||||
area.children?.map(child => renderTree(child, level + 1))}
|
||||
area.children?.map((child) => renderTree(child, level + 1))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredOperators: Operator[] =
|
||||
selectedArea?.operators.filter(op => {
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
op.loginName.toLowerCase().includes(q) ||
|
||||
op.userName.toLowerCase().includes(q) ||
|
||||
op.cellPhone.toLowerCase().includes(q)
|
||||
);
|
||||
}) || [];
|
||||
|
||||
/* ================= UI ================= */
|
||||
return (
|
||||
<div className="flex gap-6 p-6 w-full bg-gray-100">
|
||||
@@ -225,7 +217,7 @@ export default function OperatorManagement() {
|
||||
<h3 className="text-xs font-semibold text-gray-500 mb-3">
|
||||
Organizational Structure
|
||||
</h3>
|
||||
{areas.map(a => renderTree(a))}
|
||||
{areas.map((a) => renderTree(a))}
|
||||
</div>
|
||||
|
||||
{/* MAIN */}
|
||||
@@ -246,56 +238,50 @@ export default function OperatorManagement() {
|
||||
</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>
|
||||
{/* 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
|
||||
{/* 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"
|
||||
!activeOperator ? "opacity-70 cursor-not-allowed" : "hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
<Pencil size={16} /> Edit
|
||||
</button>
|
||||
>
|
||||
<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
|
||||
{/* 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"
|
||||
!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>
|
||||
|
||||
>
|
||||
<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 */}
|
||||
@@ -303,107 +289,100 @@ export default function OperatorManagement() {
|
||||
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
|
||||
placeholder="Search operator..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
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,
|
||||
},
|
||||
cellStyle: {
|
||||
textAlign: "center",
|
||||
},
|
||||
maxBodyHeight: "400px",
|
||||
tableLayout: "fixed",
|
||||
rowStyle: rowData => ({
|
||||
backgroundColor:
|
||||
activeOperator?.id === (rowData as Operator).id
|
||||
? "#EEF2FF"
|
||||
: "#FFFFFF",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<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 */}
|
||||
@@ -418,9 +397,7 @@ export default function OperatorManagement() {
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
placeholder="Login Name"
|
||||
value={form.loginName}
|
||||
onChange={e =>
|
||||
setForm({ ...form, loginName: e.target.value })
|
||||
}
|
||||
onChange={(e) => setForm({ ...form, loginName: e.target.value })}
|
||||
/>
|
||||
|
||||
<button
|
||||
@@ -437,9 +414,7 @@ export default function OperatorManagement() {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
setForm({ ...form, isDisabled: !form.isDisabled })
|
||||
}
|
||||
onClick={() => setForm({ ...form, isDisabled: !form.isDisabled })}
|
||||
className={`w-full border rounded px-3 py-2 ${
|
||||
form.isDisabled
|
||||
? "text-red-600 border-red-600"
|
||||
@@ -453,27 +428,21 @@ export default function OperatorManagement() {
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
placeholder="User Name"
|
||||
value={form.userName}
|
||||
onChange={e =>
|
||||
setForm({ ...form, userName: e.target.value })
|
||||
}
|
||||
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 })
|
||||
}
|
||||
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 })
|
||||
}
|
||||
onChange={(e) => setForm({ ...form, createdAt: e.target.value })}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-3">
|
||||
|
||||
Reference in New Issue
Block a user