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
76 lines
3.0 KiB
TypeScript
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>
|
|
);
|
|
}
|