Users filter in dashboard as organismos operadores
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user