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
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user