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 { getAuditLogs, type AuditLog } from "../api/audit";
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 type { Page } from "../App";
import grhWatermark from "../assets/images/grhWatermark.png";
@@ -21,6 +23,7 @@ import grhWatermark from "../assets/images/grhWatermark.png";
type OrganismStatus = "ACTIVO" | "INACTIVO";
type Organism = {
id: string;
name: string;
region: string;
projects: number;
@@ -29,6 +32,7 @@ type Organism = {
lastSync: string;
contact: string;
status: OrganismStatus;
projectId: string | null;
};
type AlertItem = { company: string; type: string; time: string };
@@ -52,47 +56,7 @@ export default function Home({
const userRole = useMemo(() => getCurrentUserRole(), []);
const isOperator = userRole?.toUpperCase() === 'OPERATOR';
/* ================= 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("");
const isAdmin = userRole?.toUpperCase() === 'ADMIN';
/* ================= METERS ================= */
@@ -112,12 +76,72 @@ export default function Home({
loadMeters();
}, []);
// TODO: Reemplazar cuando el backend mande el organismo real (ej: meter.organismName)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getOrganismFromMeter = (_m: Meter): string => {
return "CESPT TIJUANA";
const [projects, setProjects] = useState<Project[]>([]);
const loadProjects = async () => {
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 [loadingAuditLogs, setLoadingAuditLogs] = useState(false);
@@ -141,25 +165,53 @@ export default function Home({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const filteredMeters = useMemo(
() => meters.filter((m) => getOrganismFromMeter(m) === selectedOrganism),
[meters, selectedOrganism]
);
const filteredMeters = useMemo(() => {
if (selectedOrganism === "Todos") {
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(
() => [...new Set(filteredMeters.map((m) => m.projectName))].filter(Boolean) as string[],
[filteredMeters]
);
const chartData = useMemo(
() =>
filteredProjects.map((projectName) => ({
const selectedUserProjectName = useMemo(() => {
if (selectedOrganism === "Todos") return null;
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,
meterCount: filteredMeters.filter((m) => m.projectName === projectName)
.length,
})),
[filteredProjects, filteredMeters]
);
meterCount: filteredMeters.filter((m) => m.projectName === projectName).length,
}));
}
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
const handleBarClick = (data: any) => {
@@ -174,7 +226,7 @@ export default function Home({
const q = organismQuery.trim().toLowerCase();
if (!q) return organismsData;
return organismsData.filter((o) => o.name.toLowerCase().includes(q));
}, [organismQuery]);
}, [organismQuery, organismsData]);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [loadingNotifications, setLoadingNotifications] = useState(false);
@@ -362,14 +414,18 @@ export default function Home({
</div>
</div>
{/* Organismos Operadores */}
{isAdmin && (
<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>
<p className="text-sm text-gray-500">Organismos Operadores</p>
<p className="text-xs text-gray-400">
Seleccionado:{" "}
<span className="font-semibold">{selectedOrganism}</span>
<span className="font-semibold">
{selectedOrganism === "Todos"
? "Todos"
: organismsData.find(o => o.id === selectedOrganism)?.name || "Ninguno"}
</span>
</p>
</div>
@@ -383,7 +439,7 @@ export default function Home({
</div>
{showOrganisms && (
<div className="fixed inset-0 z-30">
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Overlay */}
<div
className="absolute inset-0 bg-black/40"
@@ -394,7 +450,7 @@ export default function Home({
/>
{/* 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 */}
<div className="p-5 border-b flex items-start justify-between gap-3">
<div>
@@ -402,8 +458,7 @@ export default function Home({
Organismos Operadores
</h3>
<p className="text-sm text-gray-500">
Selecciona un organismo para filtrar la información del
dashboard.
Selecciona un organismo para filtrar la información del dashboard
</p>
</div>
@@ -431,12 +486,59 @@ export default function Home({
{/* List */}
<div className="p-5 overflow-y-auto flex-1 space-y-3">
{loadingUsers ? (
<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.name === selectedOrganism;
const active = o.id === selectedOrganism;
return (
<div
key={o.name}
key={o.id}
className={[
"rounded-xl border p-4 transition",
active
@@ -464,7 +566,21 @@ export default function Home({
</span>
</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">
<span className="text-gray-500">Proyectos</span>
<span className="font-medium text-gray-800">
@@ -480,25 +596,11 @@ export default function Home({
</div>
<div className="flex justify-between gap-2">
<span className="text-gray-500">Alertas activas</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="text-gray-500">Último acceso</span>
<span className="font-medium text-gray-800">
{o.lastSync}
</span>
</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 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",
].join(" ")}
onClick={() => {
setSelectedOrganism(o.name);
setSelectedOrganism(o.id);
setShowOrganisms(false);
setOrganismQuery("");
}}
@@ -522,8 +624,10 @@ export default function Home({
</div>
);
})}
</>
)}
{filteredOrganisms.length === 0 && (
{!loadingUsers && filteredOrganisms.length === 0 && (
<div className="text-sm text-gray-500 text-center py-10">
No se encontraron organismos.
</div>
@@ -532,13 +636,13 @@ export default function Home({
{/* Footer */}
<div className="p-5 border-t text-xs text-gray-500">
Nota: Las propiedades están en modo demostración hasta integrar
backend.
Mostrando {filteredOrganisms.length} organismo{filteredOrganisms.length !== 1 ? 's' : ''} de {users.length} total{users.length !== 1 ? 'es' : ''}
</div>
</div>
</div>
)}
</div>
)}
</div>
{/* Gráfica */}
@@ -552,6 +656,25 @@ export default function Home({
</span>
</div>
{chartData.length === 0 && selectedOrganism !== "Todos" ? (
<div className="h-60 flex flex-col items-center justify-center">
<p className="text-sm text-gray-500 mb-2">
{selectedUserProjectName
? "Este organismo no tiene medidores registrados"
: "Este organismo no tiene un proyecto asignado"}
</p>
{selectedUserProjectName && (
<p className="text-xs text-gray-400">
Proyecto asignado: <span className="font-semibold">{selectedUserProjectName}</span>
</p>
)}
</div>
) : chartData.length === 0 ? (
<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
@@ -567,6 +690,23 @@ export default function Home({
</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>
{!isOperator && (