Web: Spanish-first staff UI — login + unified customer browser
First real frontend feature against the live Customer module API.
- login/ — session login form posting to /auth/login with credentials
included; the session cookie is what every subsequent request rides on.
- clientes/ — customer list with search and the cross-line stats header
(customers, utilities/insurance split, both-lines count).
- clientes/[id]/ — unified detail view: identity, properties + services,
policies, and transaction history for one customer, which is the whole
point of the migration (one record spanning both business lines).
- components/AppShell.tsx, lib/{api,labels,types}.ts — shared fetch wrapper
(always credentials: "include"), Spanish label maps for the enum values
the API returns, and the API response types.
- globals.css + layout.tsx — Spanish-first document (lang="es"), the type
scale, and the design tokens the pages share. Fonts load via <link> so an
offline build still renders on the system fallback stacks.
- page.tsx now redirects / to /clientes.
Also fixes pnpm-workspace.yaml: the allowBuilds map held pnpm's literal
placeholder text ("set this to true or false"), which made every install
fail with ERR_PNPM_IGNORED_BUILDS. Since pnpm 11 auto-installs before
running a script, that broke `pnpm start:dev` outright. Set the values to
true and dropped the superseded onlyBuiltDependencies list.
Verified: both apps build clean, and login -> /auth/me -> /customers/stats
round-trips against the dev database (1682 customers, 526 on both lines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { logout, me } from "@/lib/api";
|
||||
import type { AuthUser } 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. Used by /clientes and /clientes/[id].
|
||||
*/
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
const router = useRouter();
|
||||
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 (
|
||||
<>
|
||||
<header className="appbar">
|
||||
<div className="appbar-inner">
|
||||
<Link href="/clientes" className="brand">
|
||||
<span className="brand-mark" aria-hidden>
|
||||
JC
|
||||
</span>
|
||||
<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">
|
||||
<Link href="/clientes" className="appbar-link active">
|
||||
Clientes
|
||||
</Link>
|
||||
</nav>
|
||||
<span className="appbar-spacer" />
|
||||
<div className="appbar-user">
|
||||
{user && (
|
||||
<span className="appbar-user-name">{user.name}</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user