Files
jorgecuadros-platform/apps/web/src/app/login/page.tsx
T
rmancinas 8802f08d4f
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m35s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m11s
feat(reports): reports module + /inicio + edo-cuenta-datos prefill
- New reports backend (registry, service, controller, outputs, types)
  with catalog endpoint + slug/CSV/XLSX/PDF/print outputs.
- /reportes catalog + /reportes/[slug] runner; ReportRunner + ContextReports
  components wire pre-filtered links from domain pages.
- Fix: /reportes/[slug] now reads searchParams and forwards initialParams to
  ReportRunner so /reportes/edo-cuenta-datos?customerId=... auto-runs
  instead of dropping the id and forcing a manual customer search.
- /inicio landing page; root + login redirect to /inicio.
- Company header env vars + logo asset for PDF/print rendering.
- exceljs + pdfkit deps.
2026-07-23 23:20:41 -07:00

173 lines
5.1 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { ApiError, login, me } from "@/lib/api";
export default function LoginPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [bootChecking, setBootChecking] = useState(true);
// If already signed in, skip straight to the customer browser.
useEffect(() => {
let alive = true;
me()
.then(() => router.replace("/inicio"))
.catch(() => {
if (alive) setBootChecking(false);
});
return () => {
alive = false;
};
}, [router]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
setSubmitting(true);
try {
await login(email.trim(), password);
router.replace("/inicio");
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
setError("Correo o contraseña incorrectos");
} else if (err instanceof ApiError && err.status === 0) {
setError(err.message);
} else {
setError("No se pudo iniciar sesión. Inténtalo de nuevo.");
}
setSubmitting(false);
}
}
if (bootChecking) {
return (
<div
style={{
minHeight: "100vh",
display: "grid",
placeItems: "center",
color: "var(--brand-700)",
}}
>
<span className="spinner" aria-label="Cargando" />
</div>
);
}
return (
<div className="login-wrap">
{/* Brand / narrative panel */}
<aside className="login-aside" aria-hidden="false">
<div className="login-aside-top">
<div className="login-brand">
<img
src="/images/company_logo.png"
alt=""
className="brand-mark"
/>
<div className="brand-text">
<span className="brand-name" style={{ color: "#f6f3ec" }}>
Jorge Cuadros
</span>
<span className="brand-sub">& Asociados</span>
</div>
</div>
</div>
<div className="login-aside-mid">
<p className="eyebrow" style={{ color: "rgba(242,239,231,0.6)" }}>
Plataforma interna
</p>
<h1 className="login-headline">
Un solo expediente para <em>Servicios</em> y <em>Seguros</em>.
</h1>
<p className="login-lede">
Consulta en un mismo lugar las propiedades, pólizas y el estado de
cuenta de cada cliente en Baja California.
</p>
</div>
<div className="login-aside-foot">
<div className="login-lob">
<span className="badge badge-servicios">
<span className="dot" /> Servicios
</span>
<span className="badge badge-seguros">
<span className="dot" /> Seguros
</span>
</div>
<span className="login-foot-note">
Gestión de propiedades y correduría de seguros
</span>
</div>
</aside>
{/* Form panel */}
<section className="login-form-panel">
<div className="login-form-inner rise">
<p className="eyebrow">Acceso del personal</p>
<h2 className="login-form-title">Iniciar sesión</h2>
<p className="muted" style={{ marginTop: 6, marginBottom: 28 }}>
Ingresa con tu cuenta para continuar.
</p>
<form onSubmit={handleSubmit} noValidate>
<label className="field">
<span className="field-label">Correo electrónico</span>
<input
type="email"
autoComplete="username"
className="input"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="nombre@jorgecuadros.local"
required
autoFocus
/>
</label>
<label className="field">
<span className="field-label">Contraseña</span>
<input
type="password"
autoComplete="current-password"
className="input"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</label>
{error && (
<div className="login-error" role="alert">
{error}
</div>
)}
<button
type="submit"
className="btn btn-primary login-submit"
disabled={submitting}
>
{submitting ? (
<>
<span className="spinner" style={{ width: 15, height: 15 }} />
Entrando
</>
) : (
"Entrar"
)}
</button>
</form>
</div>
</section>
</div>
);
}