diff --git a/src/api/auth.ts b/src/api/auth.ts
index 1c01996..546c15f 100644
--- a/src/api/auth.ts
+++ b/src/api/auth.ts
@@ -34,9 +34,19 @@ export interface AuthUser {
email: string;
name: string;
role: string;
+ projectId?: string | null;
avatar_url?: string;
}
+export interface JwtPayload {
+ userId: string;
+ roleId: string;
+ roleName: string;
+ projectId?: string | null;
+ exp?: number;
+ iat?: number;
+}
+
/**
* Login response combining tokens and user data
*/
@@ -329,3 +339,60 @@ export async function changePassword(
newPassword,
});
}
+
+/**
+ * Get current user's project ID from JWT token
+ * @returns The project ID or null if not assigned
+ */
+export function getCurrentUserProjectId(): string | null {
+ const token = getAccessToken();
+ if (!token) return null;
+
+ try {
+ const payload = parseJwtPayload(token) as JwtPayload | null;
+ return payload?.projectId || null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Get current user's role name from JWT token
+ * @returns The role name or null
+ */
+export function getCurrentUserRole(): string | null {
+ const token = getAccessToken();
+ if (!token) return null;
+
+ try {
+ const payload = parseJwtPayload(token) as JwtPayload | null;
+ return payload?.roleName || null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Get current user's ID from JWT token
+ * @returns The user ID or null
+ */
+export function getCurrentUserId(): string | null {
+ const token = getAccessToken();
+ if (!token) return null;
+
+ try {
+ const payload = parseJwtPayload(token) as JwtPayload | null;
+ return payload?.userId || null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Check if current user is an admin
+ * @returns boolean indicating if user is admin
+ */
+export function isCurrentUserAdmin(): boolean {
+ const role = getCurrentUserRole();
+ return role?.toUpperCase() === 'ADMIN';
+}
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index e653db5..57257f0 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useMemo } from "react";
import {
Home,
Settings,
@@ -8,6 +8,7 @@ import {
People,
} from "@mui/icons-material";
import { Page } from "../../App";
+import { getCurrentUserRole } from "../../api/auth";
interface SidebarProps {
setPage: (page: Page) => void;
@@ -19,6 +20,9 @@ export default function Sidebar({ setPage }: SidebarProps) {
const [pinned, setPinned] = useState(false);
const [hovered, setHovered] = useState(false);
+ const userRole = useMemo(() => getCurrentUserRole(), []);
+ const isOperator = userRole?.toUpperCase() === 'OPERATOR';
+
const isExpanded = pinned || hovered;
return (
@@ -115,56 +119,59 @@ export default function Sidebar({ setPage }: SidebarProps) {
-
-
-
+ {!isOperator && (
+
+
+
+ )}
)}
- {/* USERS MANAGEMENT */}
-
-
-
- {isExpanded && usersOpen && (
-
- -
- setPage("users")}
- className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
- >
- Users
-
-
- -
- setPage("roles")}
- className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
- >
- Roles
-
-
-
- )}
-
+
+ )}
diff --git a/src/components/layout/TopMenu.tsx b/src/components/layout/TopMenu.tsx
index 7977cc5..7d9c66d 100644
--- a/src/components/layout/TopMenu.tsx
+++ b/src/components/layout/TopMenu.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import { Bell, User, LogOut } from "lucide-react";
import NotificationDropdown from "../NotificationDropdown";
import { useNotifications } from "../../hooks/useNotifications";
+import ProjectBadge from "./common/ProjectBadge";
interface TopMenuProps {
page: string;
@@ -81,7 +82,7 @@ const TopMenu: React.FC = ({
}}
>
{/* IZQUIERDA */}
-
+
{page !== "home" && (
<>
{page}
@@ -93,6 +94,8 @@ const TopMenu: React.FC
= ({
)}
>
)}
+
+
{/* DERECHA */}
diff --git a/src/components/layout/common/ProjectBadge.tsx b/src/components/layout/common/ProjectBadge.tsx
new file mode 100644
index 0000000..704ec00
--- /dev/null
+++ b/src/components/layout/common/ProjectBadge.tsx
@@ -0,0 +1,46 @@
+import { useEffect, useState } from "react";
+import { Building2 } from "lucide-react";
+import { getCurrentUserProjectId, getCurrentUserRole } from "../../../api/auth";
+import { fetchProject } from "../../../api/projects";
+
+interface Project {
+ id: string;
+ name: string;
+}
+
+export default function ProjectBadge() {
+ const [project, setProject] = useState
(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadProject = async () => {
+ const projectId = getCurrentUserProjectId();
+ const role = getCurrentUserRole();
+
+ if (role?.toUpperCase() !== 'ADMIN' && projectId) {
+ try {
+ const projectData = await fetchProject(projectId);
+ setProject(projectData);
+ } catch (err) {
+ console.error("Error loading user project:", err);
+ }
+ }
+ setLoading(false);
+ };
+
+ loadProject();
+ }, []);
+
+ if (loading || !project) {
+ return null;
+ }
+
+ return (
+
+
+
+ Proyecto: {project.name}
+
+
+ );
+}
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 95ba6aa..38932c8 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -10,6 +10,8 @@ import {
CartesianGrid,
} from "recharts";
import { fetchMeters, type Meter } from "../api/meters";
+import { getAuditLogs, type AuditLog } from "../api/audit";
+import { getCurrentUserRole } from "../api/auth";
import type { Page } from "../App";
import grhWatermark from "../assets/images/grhWatermark.png";
@@ -46,6 +48,10 @@ export default function Home({
setPage: (page: Page) => void;
navigateToMetersWithProject: (projectName: string) => void;
}) {
+
+ const userRole = useMemo(() => getCurrentUserRole(), []);
+ const isOperator = userRole?.toUpperCase() === 'OPERATOR';
+
/* ================= ORGANISMS (MOCK) ================= */
const organismsData: Organism[] = [
@@ -111,6 +117,29 @@ export default function Home({
return "CESPT TIJUANA";
};
+ const [auditLogs, setAuditLogs] = useState([]);
+ const [loadingAuditLogs, setLoadingAuditLogs] = useState(false);
+
+ const loadAuditLogs = async () => {
+ setLoadingAuditLogs(true);
+ try {
+ const response = await getAuditLogs({ limit: 10, page: 1 });
+ setAuditLogs(response.data);
+ } catch (err) {
+ console.error("Error loading audit logs:", err);
+ setAuditLogs([]);
+ } finally {
+ setLoadingAuditLogs(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!isOperator) {
+ loadAuditLogs();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
const filteredMeters = useMemo(
() => meters.filter((m) => getOrganismFromMeter(m) === selectedOrganism),
[meters, selectedOrganism]
@@ -146,46 +175,76 @@ export default function Home({
return organismsData.filter((o) => o.name.toLowerCase().includes(q));
}, [organismQuery]);
- /* ================= MOCK ALERTS / HISTORY ================= */
-
const alerts: AlertItem[] = [
{ company: "Empresa A", type: "Fuga", time: "Hace 2 horas" },
{ company: "Empresa C", type: "Consumo alto", time: "Hace 5 horas" },
{ company: "Empresa B", type: "Inactividad", time: "Hace 8 horas" },
];
- const history: HistoryItem[] = [
- {
- user: "GRH",
- action: "Creó un nuevo medidor",
- target: "SN001",
- time: "Hace 5 minutos",
- },
- {
- user: "CESPT",
- action: "Actualizó concentrador",
- target: "Planta 1",
- time: "Hace 20 minutos",
- },
- {
- user: "GRH",
- action: "Eliminó un usuario",
- target: "Juan Pérez",
- time: "Hace 1 hora",
- },
- {
- user: "CESPT",
- action: "Creó un payload",
- target: "Payload 12",
- time: "Hace 2 horas",
- },
- {
- user: "GRH",
- action: "Actualizó medidor",
- target: "SN002",
- time: "Hace 3 horas",
- },
- ];
+ const formatAuditAction = (action: string): string => {
+ const actionMap: Record = {
+ CREATE: "creó",
+ UPDATE: "actualizó",
+ DELETE: "eliminó",
+ READ: "consultó",
+ LOGIN: "inició sesión",
+ LOGOUT: "cerró sesión",
+ EXPORT: "exportó",
+ BULK_UPLOAD: "cargó masivamente",
+ STATUS_CHANGE: "cambió estado de",
+ PERMISSION_CHANGE: "cambió permisos de",
+ };
+ return actionMap[action] || action.toLowerCase();
+ };
+
+ const formatTableName = (tableName: string): string => {
+ const tableMap: Record = {
+ meters: "medidor",
+ concentrators: "concentrador",
+ projects: "proyecto",
+ users: "usuario",
+ roles: "rol",
+ gateways: "gateway",
+ devices: "dispositivo",
+ readings: "lectura",
+ webhooks: "webhook",
+ };
+ return tableMap[tableName] || tableName;
+ };
+
+ const formatRelativeTime = (dateString: string): string => {
+ const date = new Date(dateString);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 1) return "Hace un momento";
+ if (diffMins < 60) return `Hace ${diffMins} minuto${diffMins > 1 ? "s" : ""}`;
+ if (diffHours < 24) return `Hace ${diffHours} hora${diffHours > 1 ? "s" : ""}`;
+ if (diffDays < 7) return `Hace ${diffDays} día${diffDays > 1 ? "s" : ""}`;
+ return date.toLocaleDateString();
+ };
+
+ const formatAuditLog = (log: AuditLog): HistoryItem => {
+ const action = formatAuditAction(log.action);
+ const target = formatTableName(log.table_name);
+ const recordInfo = log.description || log.record_id || target;
+
+ return {
+ user: log.user_name || log.user_email || "Sistema",
+ action: action,
+ target: recordInfo,
+ time: formatRelativeTime(log.created_at),
+ };
+ };
+
+ const history: HistoryItem[] = useMemo(
+ () => auditLogs.map(formatAuditLog),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [auditLogs]
+ );
/* ================= KPIs (Optional) ================= */
@@ -458,24 +517,35 @@ export default function Home({
- {/* Historial */}
-
-
Historial Reciente
-
- {history.map((h, i) => (
- -
- •
-
-
- {h.user} {h.action}{" "}
- {h.target}
-
-
{h.time}
-
-
- ))}
-
-
+ {!isOperator && (
+
+
Historial Reciente de Auditoría
+ {loadingAuditLogs ? (
+
+ ) : history.length === 0 ? (
+
+ No hay registros de auditoría disponibles
+
+ ) : (
+
+ {history.map((h, i) => (
+ -
+ •
+
+
+ {h.user} {h.action}{" "}
+ {h.target}
+
+
{h.time}
+
+
+ ))}
+
+ )}
+
+ )}
{/* Últimas alertas */}
diff --git a/src/pages/concentrators/ConcentratorsPage.tsx b/src/pages/concentrators/ConcentratorsPage.tsx
index ff85954..f1330eb 100644
--- a/src/pages/concentrators/ConcentratorsPage.tsx
+++ b/src/pages/concentrators/ConcentratorsPage.tsx
@@ -28,18 +28,8 @@ export type ProjectCard = {
status: ProjectStatus;
};
-type User = {
- role: "SUPER_ADMIN" | "USER";
- project?: string;
-};
-
export default function ConcentratorsPage() {
- const currentUser: User = {
- role: "SUPER_ADMIN",
- project: "CESPT",
- };
-
- const c = useConcentrators(currentUser);
+ const c = useConcentrators();
const [typesMenuOpen, setTypesMenuOpen] = useState(false);
const [search, setSearch] = useState("");
diff --git a/src/pages/concentrators/useConcentrators.ts b/src/pages/concentrators/useConcentrators.ts
index a6775fe..cdf30fd 100644
--- a/src/pages/concentrators/useConcentrators.ts
+++ b/src/pages/concentrators/useConcentrators.ts
@@ -6,14 +6,14 @@ import {
} from "../../api/concentrators";
import { fetchProjects, type Project } from "../../api/projects";
import { fetchMeterTypes, type MeterType } from "../../api/meterTypes";
+import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
import type { ProjectCard, SampleView } from "./ConcentratorsPage";
-type User = {
- role: "SUPER_ADMIN" | "USER";
- project?: string;
-};
+export function useConcentrators() {
-export function useConcentrators(currentUser: User) {
+ const userRole = getCurrentUserRole();
+ const userProjectId = getCurrentUserProjectId();
+ const isAdmin = userRole?.toUpperCase() === 'ADMIN';
const [sampleView, setSampleView] = useState("GENERAL");
const [loadingProjects, setLoadingProjects] = useState(true);
@@ -51,12 +51,12 @@ export function useConcentrators(currentUser: User) {
const visibleProjects = useMemo(
() =>
- currentUser.role === "SUPER_ADMIN"
+ isAdmin
? allProjects
- : currentUser.project
- ? [currentUser.project]
+ : userProjectId
+ ? [userProjectId]
: [],
- [allProjects, currentUser.role, currentUser.project]
+ [allProjects, isAdmin, userProjectId]
);
const loadMeterTypes = async () => {
@@ -82,8 +82,8 @@ export function useConcentrators(currentUser: User) {
setSelectedProject((prev) => {
if (prev) return prev;
- if (currentUser.role !== "SUPER_ADMIN" && currentUser.project) {
- return currentUser.project;
+ if (!isAdmin && userProjectId) {
+ return userProjectId;
}
return projectIds[0] ?? "";
});
diff --git a/src/pages/meters/useMeters.ts b/src/pages/meters/useMeters.ts
index 89c0365..d079801 100644
--- a/src/pages/meters/useMeters.ts
+++ b/src/pages/meters/useMeters.ts
@@ -1,12 +1,16 @@
import { useEffect, useMemo, useState } from "react";
import { fetchMeters, type Meter } from "../../api/meters";
import { fetchProjects } from "../../api/projects";
+import { getCurrentUserRole, getCurrentUserProjectId } from "../../api/auth";
type UseMetersArgs = {
initialProject?: string;
};
export function useMeters({ initialProject }: UseMetersArgs) {
+ const userRole = getCurrentUserRole();
+ const userProjectId = getCurrentUserProjectId();
+ const isAdmin = userRole?.toUpperCase() === 'ADMIN';
const [allProjects, setAllProjects] = useState([]);
const [loadingProjects, setLoadingProjects] = useState(true);
@@ -26,6 +30,12 @@ export function useMeters({ initialProject }: UseMetersArgs) {
setSelectedProject((prev) => {
if (prev) return prev;
if (initialProject) return initialProject;
+
+ if (!isAdmin && userProjectId) {
+ const userProject = projects.find(p => p.id === userProjectId);
+ if (userProject) return userProject.name;
+ }
+
return projectNames[0] ?? "";
});
} catch (error) {
@@ -62,7 +72,6 @@ export function useMeters({ initialProject }: UseMetersArgs) {
if (initialProject) setSelectedProject(initialProject);
}, [initialProject]);
- // filter by project
useEffect(() => {
if (!selectedProject) {
setFilteredMeters([]);