Files
moraworld/apps/web/src/app/registro/page.tsx
T
Lizandro Guarnizo 7682b11dff feat: consolidations, aduanero portal, payments admin, bug fixes, tests
## Bugs corregidos
- bodega/page.tsx: estados correctos §08 (RECIBIDO_BODEGA, EN_VERIFICACION, EN_TRANSITO_ECUADOR)
- admin/page.tsx: carga paquetes reales, alertas de incidencias/B2B/cobros pendientes, barras de estado

## Nuevos portales y páginas
- /aduanero/ — portal propio para AGENTE_ADUANERO (layout + dashboard §11 + declaraciones DSI)
- /aduanero/declaraciones — cola de declaraciones con formulario DSI y detección DAI automática
- /admin/pagos — gestión de cobros: KPIs, filtros por estado, tabla con breakdown
- /portal/consolidacion — cliente crea/gestiona consolidaciones §21
- /bodega/consolidacion — operador cierra y despacha consolidaciones (→ EN_TRANSITO_ECUADOR)

## API nueva (ConsolidationsModule)
- GET/POST /consolidations
- GET /consolidations/:id
- POST /consolidations/:id/packages
- DELETE /consolidations/:id/packages/:packageId
- POST /consolidations/:id/close
- POST /consolidations/:id/dispatch (→ actualiza paquetes a EN_TRANSITO_ECUADOR)

## Prisma schema v0.5
- ConsolidationStatus enum (ABIERTA, CERRADA, DESPACHADA, ENTREGADA, CANCELADA)
- Consolidation model con totales calculados (totalWeightLb, totalValue)
- ConsolidationPackage (tabla intermedia, un paquete = una consolidación)
- db push aplicado a remote DB (46.202.93.92)

## Navegación
- Login: AGENTE_ADUANERO → /aduanero (ya no /bodega)
- Bodega nav: añadido Consolidaciones
- Portal nav: añadido Consolidar
- Admin nav: añadido Cobros

## Tests (104 total, 8 suites)
- payments.service.spec.ts: 16 tests (createIntent, confirm, list, findByPackageForUser)
- notifications.service.spec.ts: 12 tests (getTemplates, updateTemplate, seedDefaultTemplates, notifyStatusChange, findByUser)

## Legal §21
- Registro: aviso LOPDP Ecuador + normativa NJ en footer del formulario
- Landing footer: aviso detallado de protección de datos LOPDP/NJ

## api.ts: consolidations.* client methods
2026-06-01 18:29:26 -05:00

126 lines
5.7 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { api, setToken, setRefresh, setUser } from "@/lib/api";
export default function RegistroPage() {
const router = useRouter();
const [form, setForm] = useState({ email: "", password: "", confirmPassword: "", firstName: "", lastName: "", phone: "" });
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (form.password !== form.confirmPassword) { setError("Las contraseñas no coinciden."); return; }
if (form.password.length < 8) { setError("La contraseña debe tener al menos 8 caracteres."); return; }
setLoading(true);
try {
const { confirmPassword, ...body } = form;
const data = await api.auth.register(body);
setToken(data.accessToken);
setRefresh(data.refreshToken);
setUser(data.user);
router.push("/portal/mi-casillero");
} catch (err: any) {
setError(err.message ?? "Error al registrarse");
} finally {
setLoading(false);
}
};
const set = (k: string) => (e: React.ChangeEvent<HTMLInputElement>) =>
setForm(f => ({ ...f, [k]: e.target.value }));
return (
<div style={{ minHeight: "100vh", background: "linear-gradient(135deg, #0d1117 0%, #0f2050 100%)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1.5rem" }}>
<div style={{ width: "100%", maxWidth: 480 }}>
<div className="text-center mb-8">
<Link href="/" style={{ fontSize: "1.5rem", fontWeight: 800, color: "white" }}>
Moraworld<span style={{ color: "var(--accent)" }}>.</span>
</Link>
<p style={{ color: "rgba(255,255,255,.6)", marginTop: ".5rem", fontSize: ".9rem" }}>
Crea tu casillero gratis en 1 minuto
</p>
</div>
<div className="card" style={{ padding: "2rem" }}>
{error && <div className="alert alert-error mb-6">{error}</div>}
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "1.125rem" }}>
<div className="grid-2" style={{ gap: "1rem" }}>
<div className="form-group">
<label className="label">Nombre</label>
<input type="text" className="input" placeholder="Juan" value={form.firstName} onChange={set("firstName")} required minLength={2} />
</div>
<div className="form-group">
<label className="label">Apellido</label>
<input type="text" className="input" placeholder="Pérez" value={form.lastName} onChange={set("lastName")} required minLength={2} />
</div>
</div>
<div className="form-group">
<label className="label">Correo electrónico</label>
<input type="email" className="input" placeholder="juan@email.com" value={form.email} onChange={set("email")} required />
</div>
<div className="form-group">
<label className="label">Teléfono (opcional)</label>
<input type="tel" className="input" placeholder="+593 99 123 4567" value={form.phone} onChange={set("phone")} />
</div>
<div className="form-group">
<label className="label">Contraseña</label>
<input type="password" className="input" placeholder="Mínimo 8 caracteres" value={form.password} onChange={set("password")} required minLength={8} />
</div>
<div className="form-group">
<label className="label">Confirmar contraseña</label>
<input type="password" className="input" placeholder="Repite tu contraseña" value={form.confirmPassword} onChange={set("confirmPassword")} required />
</div>
<button type="submit" className="btn btn-primary" style={{ width: "100%", padding: ".875rem", marginTop: ".25rem" }} disabled={loading}>
{loading ? "Creando cuenta..." : "Crear mi casillero gratis"}
</button>
</form>
<p style={{ fontSize: ".75rem", color: "var(--gray-500)", textAlign: "center", marginTop: "1rem", lineHeight: 1.5 }}>
Al registrarte aceptas nuestros{" "}
<Link href="/terminos" style={{ color: "var(--primary)" }}>Términos de Servicio</Link>{" "}
y{" "}
<Link href="/privacidad" style={{ color: "var(--primary)" }}>Política de Privacidad</Link>.
</p>
{/* LOPDP §21 */}
<div style={{
marginTop: ".875rem",
padding: ".625rem .875rem",
background: "rgba(255,255,255,.04)",
border: "1px solid rgba(255,255,255,.08)",
borderRadius: 8,
fontSize: ".72rem",
color: "rgba(255,255,255,.4)",
lineHeight: 1.6,
textAlign: "center",
}}>
🛡️ Tus datos están protegidos por la <strong style={{ color: "rgba(255,255,255,.6)" }}>Ley Orgánica de Protección de Datos Personales (LOPDP)</strong> de Ecuador
y la normativa del estado de New Jersey (EE.UU.). Moraworld Imports S.A.S. · Mora Global Import LLC.
</div>
<div className="text-center mt-4">
<p style={{ fontSize: ".875rem", color: "var(--gray-500)" }}>
¿Ya tienes cuenta?{" "}
<Link href="/login" style={{ color: "var(--primary)", fontWeight: 600 }}>Inicia sesión</Link>
</p>
</div>
</div>
<div className="text-center mt-6">
<Link href="/" style={{ fontSize: ".8rem", color: "rgba(255,255,255,.4)" }}> Volver al sitio</Link>
</div>
</div>
</div>
);
}