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 && ( + )} - - - {isExpanded && usersOpen && ( - - )} -
  • + + )} 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

    - -
    + {!isOperator && ( +
    +

    Historial Reciente de Auditoría

    + {loadingAuditLogs ? ( +
    +
    +
    + ) : history.length === 0 ? ( +

    + No hay registros de auditoría disponibles +

    + ) : ( + + )} +
    + )} {/* Ú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([]);