Files
GRH/src/pages/concentrators/ConcentratorsPage.tsx

377 lines
13 KiB
TypeScript

// src/pages/concentrators/ConcentratorsPage.tsx
import { useMemo, useState } from "react";
import { Plus, Trash2, Pencil, RefreshCcw } from "lucide-react";
import ConfirmModal from "../../components/layout/common/ConfirmModal";
import { createConcentrator, deleteConcentrator, updateConcentrator, type Concentrator } from "../../api/concentrators";
// ✅ hook es named export y pide currentUser
import { useConcentrators } from "./useConcentrators";
// ✅ UI pieces
import ConcentratorsSidebar from "./ConcentratorsSidebar";
import ConcentratorsTable from "./ConcentratorsTable";
import ConcentratorsModal from "./ConcentratorsModal";
export type SampleView = "GENERAL" | "LORA" | "LORAWAN" | "GRANDES";
export type ProjectStatus = "ACTIVO" | "INACTIVO";
export type ProjectCard = {
name: string;
region: string;
projects: number;
concentrators: number;
activeAlerts: number;
lastSync: string;
contact: string;
status: ProjectStatus;
};
type User = {
role: "SUPER_ADMIN" | "USER";
project?: string;
};
export type GatewayData = {
"Gateway ID": number;
"Gateway EUI": string;
"Gateway Name": string;
"Gateway Description": string;
"Antenna Placement": "Indoor" | "Outdoor";
concentratorId?: string;
};
export default function ConcentratorsPage() {
// ✅ Simulación de usuario actual
const currentUser: User = {
role: "SUPER_ADMIN",
project: "CESPT",
};
// ✅ Hook (solo cubre: projects + fetch + sampleView + selectedProject + loading + projectsData)
const c = useConcentrators(currentUser);
const [typesMenuOpen, setTypesMenuOpen] = useState(false);
const [search, setSearch] = useState("");
const [activeConcentrator, setActiveConcentrator] = useState<Concentrator | null>(null);
const [confirmOpen, setConfirmOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const [showModal, setShowModal] = useState(false);
const [editingSerial, setEditingSerial] = useState<string | null>(null);
const getEmptyConcentrator = (): Omit<Concentrator, "id"> => ({
"Area Name": c.selectedProject,
"Device S/N": "",
"Device Name": "",
"Device Time": new Date().toISOString(),
"Device Status": "ACTIVE",
Operator: "",
"Installed Time": new Date().toISOString().slice(0, 10),
"Communication Time": new Date().toISOString(),
"Instruction Manual": "",
});
const getEmptyGatewayData = (): GatewayData => ({
"Gateway ID": 0,
"Gateway EUI": "",
"Gateway Name": "",
"Gateway Description": "",
"Antenna Placement": "Indoor",
});
const [form, setForm] = useState<Omit<Concentrator, "id">>(getEmptyConcentrator());
const [gatewayForm, setGatewayForm] = useState<GatewayData>(getEmptyGatewayData());
const [errors, setErrors] = useState<{ [key: string]: boolean }>({});
// ✅ Tabla filtrada por search (usa lo que YA filtró el hook por proyecto)
const searchFiltered = useMemo(() => {
if (!c.isGeneral) return [];
return c.filteredConcentrators.filter((row) => {
const q = search.trim().toLowerCase();
if (!q) return true;
const name = (row["Device Name"] ?? "").toLowerCase();
const sn = (row["Device S/N"] ?? "").toLowerCase();
return name.includes(q) || sn.includes(q);
});
}, [c.filteredConcentrators, c.isGeneral, search]);
// =========================
// CRUD (solo GENERAL)
// =========================
const validateForm = () => {
const next: { [key: string]: boolean } = {};
if (!form["Device Name"].trim()) next["Device Name"] = true;
if (!form["Device S/N"].trim()) next["Device S/N"] = true;
if (!form["Operator"].trim()) next["Operator"] = true;
if (!form["Instruction Manual"].trim()) next["Instruction Manual"] = true;
if (!form["Installed Time"]) next["Installed Time"] = true;
if (!form["Device Time"]) next["Device Time"] = true;
if (!form["Communication Time"]) next["Communication Time"] = true;
if (!gatewayForm["Gateway ID"] || gatewayForm["Gateway ID"] === 0) next["Gateway ID"] = true;
if (!gatewayForm["Gateway EUI"].trim()) next["Gateway EUI"] = true;
if (!gatewayForm["Gateway Name"].trim()) next["Gateway Name"] = true;
if (!gatewayForm["Gateway Description"].trim()) next["Gateway Description"] = true;
setErrors(next);
return Object.keys(next).length === 0;
};
const handleSave = async () => {
if (!c.isGeneral) return;
if (!validateForm()) return;
try {
if (editingSerial) {
const toUpdate = c.concentrators.find((x) => x["Device S/N"] === editingSerial);
if (!toUpdate) throw new Error("Concentrator not found");
const updated = await updateConcentrator(toUpdate.id, form);
// actualiza en memoria (el hook expone setConcentrators)
c.setConcentrators((prev) => prev.map((x) => (x.id === toUpdate.id ? updated : x)));
} else {
const created = await createConcentrator(form);
c.setConcentrators((prev) => [...prev, created]);
}
setShowModal(false);
setEditingSerial(null);
setForm({ ...getEmptyConcentrator(), "Area Name": c.selectedProject });
setGatewayForm(getEmptyGatewayData());
setErrors({});
setActiveConcentrator(null);
} catch (err) {
console.error(err);
alert(`Error saving concentrator: ${err instanceof Error ? err.message : "Please try again."}`);
}
};
const handleDelete = async () => {
if (!c.isGeneral) return;
if (!activeConcentrator) return;
try {
await deleteConcentrator(activeConcentrator.id);
c.setConcentrators((prev) => prev.filter((x) => x.id !== activeConcentrator.id));
setActiveConcentrator(null);
} catch (err) {
console.error(err);
alert(`Error deleting concentrator: ${err instanceof Error ? err.message : "Please try again."}`);
}
};
// =========================
// Date helpers para modal
// =========================
function toDatetimeLocalValue(value?: string) {
if (!value) return "";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "";
const pad = (n: number) => String(n).padStart(2, "0");
const yyyy = d.getFullYear();
const mm = pad(d.getMonth() + 1);
const dd = pad(d.getDate());
const hh = pad(d.getHours());
const mi = pad(d.getMinutes());
return `${yyyy}-${mm}-${dd}T${hh}:${mi}`;
}
function fromDatetimeLocalValue(value: string) {
if (!value) return "";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "";
return d.toISOString();
}
return (
<div className="flex gap-6 p-6 w-full bg-gray-100">
<aside className="w-[420px] shrink-0">
<ConcentratorsSidebar
loadingProjects={c.loadingProjects}
sampleView={c.sampleView}
sampleViewLabel={c.sampleViewLabel}
typesMenuOpen={typesMenuOpen}
setTypesMenuOpen={setTypesMenuOpen}
onChangeSampleView={(next: SampleView) => {
c.setSampleView(next);
setTypesMenuOpen(false);
// resets UI
c.setSelectedProject("");
setActiveConcentrator(null);
setSearch("");
}}
selectedProject={c.selectedProject}
onSelectProject={(name) => {
c.setSelectedProject(name);
setActiveConcentrator(null);
setSearch("");
}}
projects={c.projectsData}
onRefresh={c.loadConcentrators}
refreshDisabled={c.loadingProjects || !c.isGeneral}
/>
</aside>
<main className="flex-1 flex flex-col gap-6 min-w-0">
<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">
{!c.isGeneral
? `Vista: ${c.sampleViewLabel} (mock)`
: c.selectedProject
? `Proyecto: ${c.selectedProject}`
: "Selecciona un proyecto desde el panel izquierdo"}
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => {
if (!c.isGeneral) return;
if (!c.selectedProject) return;
setForm({ ...getEmptyConcentrator(), "Area Name": c.selectedProject });
setGatewayForm(getEmptyGatewayData());
setErrors({});
setEditingSerial(null);
setShowModal(true);
}}
disabled={!c.isGeneral || !c.selectedProject}
className="flex items-center gap-2 px-4 py-2 bg-white text-[#4c5f9e] rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus size={16} /> Agregar
</button>
<button
onClick={() => {
if (!c.isGeneral) return;
if (!activeConcentrator) return;
const a = activeConcentrator;
setEditingSerial(a["Device S/N"]);
setForm({
"Area Name": a["Area Name"],
"Device S/N": a["Device S/N"],
"Device Name": a["Device Name"],
"Device Time": a["Device Time"],
"Device Status": a["Device Status"],
Operator: a["Operator"],
"Installed Time": a["Installed Time"],
"Communication Time": a["Communication Time"],
"Instruction Manual": a["Instruction Manual"],
});
setGatewayForm(getEmptyGatewayData());
setErrors({});
setShowModal(true);
}}
disabled={!c.isGeneral || !activeConcentrator}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Pencil size={16} /> Editar
</button>
<button
onClick={() => setConfirmOpen(true)}
disabled={!c.isGeneral || !activeConcentrator}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<Trash2 size={16} /> Eliminar
</button>
<button
onClick={c.loadConcentrators}
disabled={!c.isGeneral}
className="flex items-center gap-2 px-4 py-2 border border-white/40 rounded-lg disabled:opacity-60"
>
<RefreshCcw size={16} /> Actualizar
</button>
</div>
</div>
<input
className="bg-white rounded-lg shadow px-4 py-2 text-sm"
placeholder={c.isGeneral ? "Search concentrator..." : "Search disabled in mock views"}
value={search}
onChange={(e) => setSearch(e.target.value)}
disabled={!c.isGeneral || !c.selectedProject}
/>
<div className={!c.isGeneral || !c.selectedProject ? "opacity-60 pointer-events-none" : ""}>
<ConcentratorsTable
isLoading={c.isGeneral ? c.loadingConcentrators : false}
data={c.isGeneral ? searchFiltered : []}
activeRowId={activeConcentrator?.id}
onRowClick={(row) => setActiveConcentrator(row)}
emptyMessage={
!c.isGeneral
? `Vista "${c.sampleViewLabel}" está en modo mock (sin backend todavía).`
: !c.selectedProject
? "Select a project to view concentrators."
: c.loadingConcentrators
? "Loading concentrators..."
: "No concentrators found. Click 'Add' to create your first concentrator."
}
/>
</div>
<ConfirmModal
open={confirmOpen}
title="Eliminar concentrador"
message={`¿Estás seguro que quieres eliminar "${
activeConcentrator?.["Device Name"] ?? "este concentrador"
}"? Esta acción no se puede deshacer.`}
confirmText="Eliminar"
cancelText="Cancelar"
danger
loading={deleting}
onClose={() => setConfirmOpen(false)}
onConfirm={async () => {
if (!c.isGeneral) return;
setDeleting(true);
try {
await handleDelete();
setConfirmOpen(false);
} finally {
setDeleting(false);
}
}}
/>
</main>
{showModal && c.isGeneral && (
<ConcentratorsModal
editingSerial={editingSerial}
form={form}
setForm={setForm}
gatewayForm={gatewayForm}
setGatewayForm={setGatewayForm}
errors={errors}
setErrors={setErrors}
toDatetimeLocalValue={toDatetimeLocalValue}
fromDatetimeLocalValue={fromDatetimeLocalValue}
onClose={() => {
setShowModal(false);
setGatewayForm(getEmptyGatewayData());
setErrors({});
}}
onSave={handleSave}
/>
)}
</div>
);
}