Users filter in dashboard as organismos operadores

This commit is contained in:
2026-02-03 01:06:40 -06:00
parent 23c3a19209
commit d1770b550a

View File

@@ -12,6 +12,8 @@ import {
import { fetchMeters, type Meter } from "../api/meters"; import { fetchMeters, type Meter } from "../api/meters";
import { getAuditLogs, type AuditLog } from "../api/audit"; import { getAuditLogs, type AuditLog } from "../api/audit";
import { fetchNotifications, type Notification } from "../api/notifications"; import { fetchNotifications, type Notification } from "../api/notifications";
import { getAllUsers, type User } from "../api/users";
import { fetchProjects, type Project } from "../api/projects";
import { getCurrentUserRole } from "../api/auth"; import { getCurrentUserRole } from "../api/auth";
import type { Page } from "../App"; import type { Page } from "../App";
import grhWatermark from "../assets/images/grhWatermark.png"; import grhWatermark from "../assets/images/grhWatermark.png";
@@ -21,6 +23,7 @@ import grhWatermark from "../assets/images/grhWatermark.png";
type OrganismStatus = "ACTIVO" | "INACTIVO"; type OrganismStatus = "ACTIVO" | "INACTIVO";
type Organism = { type Organism = {
id: string;
name: string; name: string;
region: string; region: string;
projects: number; projects: number;
@@ -29,6 +32,7 @@ type Organism = {
lastSync: string; lastSync: string;
contact: string; contact: string;
status: OrganismStatus; status: OrganismStatus;
projectId: string | null;
}; };
type AlertItem = { company: string; type: string; time: string }; type AlertItem = { company: string; type: string; time: string };
@@ -52,47 +56,7 @@ export default function Home({
const userRole = useMemo(() => getCurrentUserRole(), []); const userRole = useMemo(() => getCurrentUserRole(), []);
const isOperator = userRole?.toUpperCase() === 'OPERATOR'; const isOperator = userRole?.toUpperCase() === 'OPERATOR';
const isAdmin = userRole?.toUpperCase() === 'ADMIN';
/* ================= ORGANISMS (MOCK) ================= */
const organismsData: Organism[] = [
{
name: "CESPT TIJUANA",
region: "Tijuana, BC",
projects: 6,
meters: 128,
activeAlerts: 0,
lastSync: "Hace 12 min",
contact: "Operaciones CESPT",
status: "ACTIVO",
},
{
name: "CESPT TECATE",
region: "Tecate, BC",
projects: 3,
meters: 54,
activeAlerts: 1,
lastSync: "Hace 40 min",
contact: "Mantenimiento",
status: "ACTIVO",
},
{
name: "CESPT MEXICALI",
region: "Mexicali, BC",
projects: 4,
meters: 92,
activeAlerts: 0,
lastSync: "Hace 1 h",
contact: "Supervisión",
status: "ACTIVO",
},
];
const [selectedOrganism, setSelectedOrganism] = useState<string>(
organismsData[0]?.name ?? "CESPT TIJUANA"
);
const [showOrganisms, setShowOrganisms] = useState(false);
const [organismQuery, setOrganismQuery] = useState("");
/* ================= METERS ================= */ /* ================= METERS ================= */
@@ -112,12 +76,72 @@ export default function Home({
loadMeters(); loadMeters();
}, []); }, []);
// TODO: Reemplazar cuando el backend mande el organismo real (ej: meter.organismName) const [projects, setProjects] = useState<Project[]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getOrganismFromMeter = (_m: Meter): string => { const loadProjects = async () => {
return "CESPT TIJUANA"; try {
const data = await fetchProjects();
setProjects(data);
} catch (err) {
console.error("Error loading projects:", err);
setProjects([]);
}
}; };
useEffect(() => {
loadProjects();
}, []);
const [users, setUsers] = useState<User[]>([]);
const [loadingUsers, setLoadingUsers] = useState(false);
const [selectedOrganism, setSelectedOrganism] = useState<string>("Todos");
const [showOrganisms, setShowOrganisms] = useState(false);
const [organismQuery, setOrganismQuery] = useState("");
const loadUsers = async () => {
setLoadingUsers(true);
try {
const response = await getAllUsers({ is_active: true });
setUsers(response.data);
} catch (err) {
console.error("Error loading users:", err);
setUsers([]);
} finally {
setLoadingUsers(false);
}
};
useEffect(() => {
if (!isOperator) {
loadUsers();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const organismsData: Organism[] = useMemo(() => {
return users.map(user => {
const userMeters = user.project_id
? meters.filter(m => m.projectId === user.project_id).length
: 0;
const userProjects = user.project_id ? 1 : 0;
return {
id: user.id,
name: user.name,
region: user.email,
projects: userProjects,
meters: userMeters,
activeAlerts: 0,
lastSync: user.last_login ? `Último acceso: ${new Date(user.last_login).toLocaleDateString()}` : "Nunca",
contact: user.role?.name || "N/A",
status: user.is_active ? "ACTIVO" : "INACTIVO",
projectId: user.project_id,
};
});
}, [users, meters]);
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]); const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [loadingAuditLogs, setLoadingAuditLogs] = useState(false); const [loadingAuditLogs, setLoadingAuditLogs] = useState(false);
@@ -141,25 +165,53 @@ export default function Home({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const filteredMeters = useMemo( const filteredMeters = useMemo(() => {
() => meters.filter((m) => getOrganismFromMeter(m) === selectedOrganism), if (selectedOrganism === "Todos") {
[meters, selectedOrganism] return meters;
); }
const selectedUser = users.find(u => u.id === selectedOrganism);
if (!selectedUser || !selectedUser.project_id) {
return [];
}
return meters.filter((m) => m.projectId === selectedUser.project_id);
}, [meters, selectedOrganism, users]);
const filteredProjects = useMemo( const filteredProjects = useMemo(
() => [...new Set(filteredMeters.map((m) => m.projectName))].filter(Boolean) as string[], () => [...new Set(filteredMeters.map((m) => m.projectName))].filter(Boolean) as string[],
[filteredMeters] [filteredMeters]
); );
const chartData = useMemo( const selectedUserProjectName = useMemo(() => {
() => if (selectedOrganism === "Todos") return null;
filteredProjects.map((projectName) => ({
const selectedUser = users.find(u => u.id === selectedOrganism);
if (!selectedUser || !selectedUser.project_id) return null;
const project = projects.find(p => p.id === selectedUser.project_id);
return project?.name || null;
}, [selectedOrganism, users, projects]);
const chartData = useMemo(() => {
if (selectedOrganism === "Todos") {
return filteredProjects.map((projectName) => ({
name: projectName, name: projectName,
meterCount: filteredMeters.filter((m) => m.projectName === projectName) meterCount: filteredMeters.filter((m) => m.projectName === projectName).length,
.length, }));
})), }
[filteredProjects, filteredMeters]
); if (selectedUserProjectName) {
const meterCount = filteredMeters.length;
return [{
name: selectedUserProjectName,
meterCount: meterCount,
}];
}
return [];
}, [selectedOrganism, filteredProjects, filteredMeters, selectedUserProjectName]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleBarClick = (data: any) => { const handleBarClick = (data: any) => {
@@ -174,7 +226,7 @@ export default function Home({
const q = organismQuery.trim().toLowerCase(); const q = organismQuery.trim().toLowerCase();
if (!q) return organismsData; if (!q) return organismsData;
return organismsData.filter((o) => o.name.toLowerCase().includes(q)); return organismsData.filter((o) => o.name.toLowerCase().includes(q));
}, [organismQuery]); }, [organismQuery, organismsData]);
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const [loadingNotifications, setLoadingNotifications] = useState(false); const [loadingNotifications, setLoadingNotifications] = useState(false);
@@ -362,28 +414,32 @@ export default function Home({
</div> </div>
</div> </div>
{/* Organismos Operadores */} {isAdmin && (
<div className="bg-white rounded-xl shadow p-4"> <div className="bg-white rounded-xl shadow p-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div> <div>
<p className="text-sm text-gray-500">Organismos Operadores</p> <p className="text-sm text-gray-500">Organismos Operadores</p>
<p className="text-xs text-gray-400"> <p className="text-xs text-gray-400">
Seleccionado:{" "} Seleccionado:{" "}
<span className="font-semibold">{selectedOrganism}</span> <span className="font-semibold">
</p> {selectedOrganism === "Todos"
? "Todos"
: organismsData.find(o => o.id === selectedOrganism)?.name || "Ninguno"}
</span>
</p>
</div>
<button
type="button"
className="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700 transition"
onClick={() => setShowOrganisms(true)}
>
Organismos Operadores
</button>
</div> </div>
<button {showOrganisms && (
type="button" <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
className="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-700 transition"
onClick={() => setShowOrganisms(true)}
>
Organismos Operadores
</button>
</div>
{showOrganisms && (
<div className="fixed inset-0 z-30">
{/* Overlay */} {/* Overlay */}
<div <div
className="absolute inset-0 bg-black/40" className="absolute inset-0 bg-black/40"
@@ -394,7 +450,7 @@ export default function Home({
/> />
{/* Panel */} {/* Panel */}
<div className="absolute right-0 top-0 h-full w-full sm:w-[520px] bg-white shadow-2xl flex flex-col"> <div className="relative w-full max-w-2xl max-h-[90vh] bg-white rounded-xl shadow-2xl flex flex-col">
{/* Header */} {/* Header */}
<div className="p-5 border-b flex items-start justify-between gap-3"> <div className="p-5 border-b flex items-start justify-between gap-3">
<div> <div>
@@ -402,8 +458,7 @@ export default function Home({
Organismos Operadores Organismos Operadores
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Selecciona un organismo para filtrar la información del Selecciona un organismo para filtrar la información del dashboard
dashboard.
</p> </p>
</div> </div>
@@ -431,12 +486,59 @@ export default function Home({
{/* List */} {/* List */}
<div className="p-5 overflow-y-auto flex-1 space-y-3"> <div className="p-5 overflow-y-auto flex-1 space-y-3">
{filteredOrganisms.map((o) => { {loadingUsers ? (
const active = o.name === selectedOrganism; <div className="flex items-center justify-center py-10">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
<>
<div
className={[
"rounded-xl border p-4 transition",
selectedOrganism === "Todos"
? "border-blue-600 bg-blue-50/40"
: "border-gray-200 bg-white hover:bg-gray-50",
].join(" ")}
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-gray-800">
Todos los Organismos
</p>
<p className="text-xs text-gray-500">Ver todos los datos del sistema</p>
</div>
<span className="text-xs font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-700">
TODOS
</span>
</div>
<div className="mt-4 flex items-center justify-end gap-2">
<button
type="button"
className={[
"rounded-lg px-3 py-2 text-sm font-semibold shadow transition",
selectedOrganism === "Todos"
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-900 text-white hover:bg-gray-800",
].join(" ")}
onClick={() => {
setSelectedOrganism("Todos");
setShowOrganisms(false);
setOrganismQuery("");
}}
>
{selectedOrganism === "Todos" ? "Seleccionado" : "Seleccionar"}
</button>
</div>
</div>
{filteredOrganisms.map((o) => {
const active = o.id === selectedOrganism;
return ( return (
<div <div
key={o.name} key={o.id}
className={[ className={[
"rounded-xl border p-4 transition", "rounded-xl border p-4 transition",
active active
@@ -464,7 +566,21 @@ export default function Home({
</span> </span>
</div> </div>
<div className="mt-3 grid grid-cols-2 gap-2 text-xs"> <div className="mt-3 space-y-2 text-xs">
<div className="flex justify-between gap-2">
<span className="text-gray-500">Rol</span>
<span className="font-medium text-gray-800">
{o.contact}
</span>
</div>
<div className="flex justify-between gap-2">
<span className="text-gray-500">Email</span>
<span className="font-medium text-gray-800 truncate max-w-[200px]">
{o.region}
</span>
</div>
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<span className="text-gray-500">Proyectos</span> <span className="text-gray-500">Proyectos</span>
<span className="font-medium text-gray-800"> <span className="font-medium text-gray-800">
@@ -480,25 +596,11 @@ export default function Home({
</div> </div>
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<span className="text-gray-500">Alertas activas</span> <span className="text-gray-500">Último acceso</span>
<span className="font-medium text-gray-800">
{o.activeAlerts}
</span>
</div>
<div className="flex justify-between gap-2">
<span className="text-gray-500">Última sync</span>
<span className="font-medium text-gray-800"> <span className="font-medium text-gray-800">
{o.lastSync} {o.lastSync}
</span> </span>
</div> </div>
<div className="col-span-2 flex justify-between gap-2">
<span className="text-gray-500">Responsable</span>
<span className="font-medium text-gray-800">
{o.contact}
</span>
</div>
</div> </div>
<div className="mt-4 flex items-center justify-end gap-2"> <div className="mt-4 flex items-center justify-end gap-2">
@@ -511,7 +613,7 @@ export default function Home({
: "bg-gray-900 text-white hover:bg-gray-800", : "bg-gray-900 text-white hover:bg-gray-800",
].join(" ")} ].join(" ")}
onClick={() => { onClick={() => {
setSelectedOrganism(o.name); setSelectedOrganism(o.id);
setShowOrganisms(false); setShowOrganisms(false);
setOrganismQuery(""); setOrganismQuery("");
}} }}
@@ -522,8 +624,10 @@ export default function Home({
</div> </div>
); );
})} })}
</>
)}
{filteredOrganisms.length === 0 && ( {!loadingUsers && filteredOrganisms.length === 0 && (
<div className="text-sm text-gray-500 text-center py-10"> <div className="text-sm text-gray-500 text-center py-10">
No se encontraron organismos. No se encontraron organismos.
</div> </div>
@@ -532,13 +636,13 @@ export default function Home({
{/* Footer */} {/* Footer */}
<div className="p-5 border-t text-xs text-gray-500"> <div className="p-5 border-t text-xs text-gray-500">
Nota: Las propiedades están en modo demostración hasta integrar Mostrando {filteredOrganisms.length} organismo{filteredOrganisms.length !== 1 ? 's' : ''} de {users.length} total{users.length !== 1 ? 'es' : ''}
backend.
</div> </div>
</div> </div>
</div> </div>
)} )}
</div> </div>
)}
</div> </div>
{/* Gráfica */} {/* Gráfica */}
@@ -552,21 +656,57 @@ export default function Home({
</span> </span>
</div> </div>
<div className="h-60"> {chartData.length === 0 && selectedOrganism !== "Todos" ? (
<ResponsiveContainer width="100%" height="100%"> <div className="h-60 flex flex-col items-center justify-center">
<BarChart <p className="text-sm text-gray-500 mb-2">
data={chartData} {selectedUserProjectName
margin={{ top: 5, right: 20, left: 0, bottom: 5 }} ? "Este organismo no tiene medidores registrados"
onClick={handleBarClick} : "Este organismo no tiene un proyecto asignado"}
> </p>
<CartesianGrid strokeDasharray="3 3" /> {selectedUserProjectName && (
<XAxis dataKey="name" /> <p className="text-xs text-gray-400">
<YAxis /> Proyecto asignado: <span className="font-semibold">{selectedUserProjectName}</span>
<Tooltip /> </p>
<Bar dataKey="meterCount" fill="#4c5f9e" cursor="pointer" /> )}
</BarChart> </div>
</ResponsiveContainer> ) : chartData.length === 0 ? (
</div> <div className="h-60 flex items-center justify-center">
<p className="text-sm text-gray-500">No hay datos disponibles</p>
</div>
) : (
<>
<div className="h-60">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={chartData}
margin={{ top: 5, right: 20, left: 0, bottom: 5 }}
onClick={handleBarClick}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="meterCount" fill="#4c5f9e" cursor="pointer" />
</BarChart>
</ResponsiveContainer>
</div>
{selectedOrganism !== "Todos" && selectedUserProjectName && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex items-center justify-between text-sm">
<div>
<span className="text-gray-500">Proyecto del organismo:</span>
<span className="ml-2 font-semibold text-gray-800">{selectedUserProjectName}</span>
</div>
<div>
<span className="text-gray-500">Total de medidores:</span>
<span className="ml-2 font-semibold text-blue-600">{filteredMeters.length}</span>
</div>
</div>
</div>
)}
</>
)}
</div> </div>
{!isOperator && ( {!isOperator && (