API: - AuthModule: register, login, refresh, logout, MFA/TOTP setup+verify - JwtStrategy + JwtAuthGuard + RolesGuard + CurrentUser decorator - PackagesModule: CRUD paquetes + historial de estados - PreAlertsModule: pre-alertas por usuario - UsersModule: gestión de usuarios + roles + activación - B2BModule: solicitudes de carga pesada/cotización - ValidationPipe global + CORS configurado Web (Next.js 15): - globals.css completo (design system + utility classes) - Layout raíz con WhatsApp flotante - /login + /registro funcionales con JWT y redirección por rol - /portal: dashboard, mi-casillero, mis-paquetes, pre-alerta, calculadora, perfil - /admin: dashboard, usuarios (gestión roles/activación), tarifas, reportes, auditoría - /bodega: dashboard, paquetes (crear+actualizar estado), verificación, despacho - /tracking: tracking real con progreso visual + historial - /calculadora: calculadora interactiva real (API SENAE §15) - /como-funciona, /tarifas, /quienes-somos, /casillero - /carga-pesada + /carga-pesada/cotizacion (formulario B2B) - lib/api.ts: cliente HTTP con auto-refresh de token Roles sincronizados con schema: SUPER_ADMIN, ADMIN_EMPRESA, OPERADOR_BODEGA, AGENTE_ADUANERO, CLIENTE, SOPORTE
110 lines
5.0 KiB
TypeScript
110 lines
5.0 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>
|
|
|
|
<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>
|
|
);
|
|
}
|