Replace the JC text mark in AppShell and login with the real company_logo.png. Restyle .brand-mark to host the image (white rounded bg, object-fit contain). Appbar 38px, login panel 46px.
132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, type ReactNode } from "react";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { logout, me } from "@/lib/api";
|
|
import { AuthContext, can } from "@/lib/abilities";
|
|
import { ROLE_LABEL } from "@/lib/labels";
|
|
import type { AuthUser, Ability } from "@/lib/types";
|
|
|
|
/**
|
|
* Authenticated shell: gates on /auth/me, redirects to /login when the
|
|
* session is missing, renders the brand header + logout, and wraps page
|
|
* content. Provides the AuthContext so any page can read the user's
|
|
* abilities. Used by every authenticated page.
|
|
*/
|
|
const NAV: { href: string; label: string; ability?: Ability }[] = [
|
|
{ href: "/clientes", label: "Clientes" },
|
|
{ href: "/servicios", label: "Propiedades" },
|
|
{ href: "/polizas", label: "Pólizas" },
|
|
{ href: "/estado-cuenta", label: "Estado de cuenta" },
|
|
{ href: "/banco", label: "Chequera" },
|
|
{ href: "/catalogos", label: "Catálogos", ability: "lookup:manage" },
|
|
{ href: "/usuarios", label: "Usuarios", ability: "user:manage" },
|
|
{ href: "/operaciones", label: "Operaciones", ability: "db:manage" },
|
|
];
|
|
|
|
export function AppShell({ children }: { children: ReactNode }) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
const [checking, setChecking] = useState(true);
|
|
const [loggingOut, setLoggingOut] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let alive = true;
|
|
me()
|
|
.then((u) => {
|
|
if (alive) {
|
|
setUser(u);
|
|
setChecking(false);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
router.replace("/login");
|
|
});
|
|
return () => {
|
|
alive = false;
|
|
};
|
|
}, [router]);
|
|
|
|
async function handleLogout() {
|
|
setLoggingOut(true);
|
|
try {
|
|
await logout();
|
|
} catch {
|
|
/* ignore — we redirect regardless */
|
|
}
|
|
router.replace("/login");
|
|
}
|
|
|
|
if (checking) {
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: "100vh",
|
|
display: "grid",
|
|
placeItems: "center",
|
|
color: "var(--brand-700)",
|
|
}}
|
|
>
|
|
<span className="spinner" aria-label="Cargando" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AuthContext.Provider value={user}>
|
|
<header className="appbar">
|
|
<div className="appbar-inner">
|
|
<Link href="/clientes" className="brand">
|
|
<img
|
|
src="/images/company_logo.png"
|
|
alt=""
|
|
className="brand-mark"
|
|
/>
|
|
<span className="brand-text">
|
|
<span className="brand-name">Jorge Cuadros</span>
|
|
<span className="brand-sub">& Asociados</span>
|
|
</span>
|
|
</Link>
|
|
<nav className="appbar-nav" aria-label="Principal">
|
|
{NAV.filter((item) => !item.ability || can(user, item.ability)).map(
|
|
(item) => {
|
|
const active = pathname?.startsWith(item.href) ?? false;
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`appbar-link${active ? " active" : ""}`}
|
|
aria-current={active ? "page" : undefined}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
},
|
|
)}
|
|
</nav>
|
|
<span className="appbar-spacer" />
|
|
<div className="appbar-user">
|
|
{user && (
|
|
<span className="appbar-user-name">
|
|
{user.name}
|
|
<span className="appbar-user-role">{ROLE_LABEL[user.role]}</span>
|
|
</span>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="btn btn-ghost"
|
|
onClick={handleLogout}
|
|
disabled={loggingOut}
|
|
>
|
|
{loggingOut ? "Saliendo…" : "Cerrar sesión"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main className="shell-main">{children}</main>
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|