## 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
126 lines
5.7 KiB
TypeScript
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>
|
|
);
|
|
}
|