Files
moraworld/apps/web/src/app/portal/layout.tsx
T
Lizandro Guarnizo 4872053fd1 feat: Fase 1 — Auth JWT+MFA, portales CRUD, UI completa
API:
- AuthModule: register, login, refresh, logout, MFA/TOTP setup+verify
- JwtStrategy + JwtAuthGuard + RolesGuard + CurrentUser decorator
- PackagesModule: CRUD paquetes + historial de estados
- PreAlertsModule: pre-alertas por usuario
- UsersModule: gestión de usuarios + roles + activación
- B2BModule: solicitudes de carga pesada/cotización
- ValidationPipe global + CORS configurado

Web (Next.js 15):
- globals.css completo (design system + utility classes)
- Layout raíz con WhatsApp flotante
- /login + /registro funcionales con JWT y redirección por rol
- /portal: dashboard, mi-casillero, mis-paquetes, pre-alerta, calculadora, perfil
- /admin: dashboard, usuarios (gestión roles/activación), tarifas, reportes, auditoría
- /bodega: dashboard, paquetes (crear+actualizar estado), verificación, despacho
- /tracking: tracking real con progreso visual + historial
- /calculadora: calculadora interactiva real (API SENAE §15)
- /como-funciona, /tarifas, /quienes-somos, /casillero
- /carga-pesada + /carga-pesada/cotizacion (formulario B2B)
- lib/api.ts: cliente HTTP con auto-refresh de token

Roles sincronizados con schema: SUPER_ADMIN, ADMIN_EMPRESA,
OPERADOR_BODEGA, AGENTE_ADUANERO, CLIENTE, SOPORTE
2026-06-01 09:42:38 -05:00

76 lines
3.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { getUser, clearAuth, getRefresh } from "@/lib/api";
import { api } from "@/lib/api";
const NAV = [
{ href: "/portal", icon: "◈", label: "Dashboard" },
{ href: "/portal/mi-casillero", icon: "📦", label: "Mi Casillero" },
{ href: "/portal/mis-paquetes", icon: "🚚", label: "Mis Paquetes" },
{ href: "/portal/pre-alerta", icon: "🔔", label: "Pre-Alerta" },
{ href: "/portal/calculadora", icon: "🧮", label: "Calculadora" },
{ href: "/portal/perfil", icon: "👤", label: "Mi Perfil" },
];
export default function PortalLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const [user, setUser] = useState<any>(null);
const [unread, setUnread] = useState(0);
useEffect(() => {
const u = getUser();
if (!u) { router.replace("/login"); return; }
setUser(u);
}, [router]);
const handleLogout = async () => {
try { await api.auth.logout(localStorage.getItem("mw_refresh") ?? ""); } catch {}
clearAuth();
router.push("/login");
};
if (!user) return <div className="loading-overlay"><div className="spinner" /></div>;
return (
<div className="dash-layout">
{/* Sidebar */}
<aside className="dash-sidebar">
<div className="dash-logo">Mora<span>world</span></div>
<nav className="dash-nav">
{NAV.map(item => (
<Link key={item.href} href={item.href}
className={`dash-nav-item ${pathname === item.href ? "active" : ""}`}>
<span>{item.icon}</span>
<span>{item.label}</span>
</Link>
))}
</nav>
<div className="dash-user">
<div className="dash-user-name">{user.firstName} {user.lastName}</div>
<div className="dash-user-role">Cliente</div>
<button className="btn btn-ghost btn-sm" style={{ marginTop: ".5rem", color: "rgba(255,255,255,.5)", fontSize: ".8rem" }} onClick={handleLogout}>
Cerrar sesión
</button>
</div>
</aside>
{/* Main */}
<div className="dash-main">
<header className="dash-topbar">
<span style={{ fontSize: "1rem", fontWeight: 600, color: "var(--gray-900)" }}>Portal del Cliente</span>
<div className="flex items-center gap-4">
<Link href="/portal/notificaciones" style={{ position: "relative", fontSize: "1.2rem" }}>
🔔 {unread > 0 && <span style={{ position: "absolute", top: -6, right: -8, background: "var(--red)", color: "white", borderRadius: "9999px", fontSize: ".65rem", fontWeight: 700, padding: "0 4px", minWidth: 16, textAlign: "center" }}>{unread}</span>}
</Link>
<span style={{ fontSize: ".875rem", color: "var(--gray-500)" }}>{user.email}</span>
</div>
</header>
<main className="dash-content">{children}</main>
</div>
</div>
);
}