feat: audit & sync with documentacion.html v1.1 — schema, tests, modules, web

Schema (packages/database/prisma/schema.prisma):
- Add SenaeCategory, B2BRequestStatus, NotificationChannel enums
- Add Package fields: lengthCm, widthCm, heightCm, hasDiscrepancy, photos[],
  senaeCategory, senaeAuthNumber, senaeDeclarationId
- Add userId field to PreAlert
- Add RefreshToken model (JWT auth — Fase 1)
- Add Tariff model (configurable rates: pricePerLb, insurancePct, etc.)
- Add B2BRequest model (heavy cargo imports — doc §13)
- Add Notification model (multi-channel: email, whatsapp, sms, push — doc §16)
- Add userAgent to AuditLog

API (apps/api):
- Add CalculatorModule: calculates SENAE cost breakdown (flete, seguro,
  FODINFA, arancel, IVA) per formulas in doc §15
- Add TrackingModule: public GET /api/tracking/:id endpoint (doc §07)
- Register both modules in AppModule
- Add Jest config + test scripts (test, test:watch, test:cov, test:ci)

Tests (80 tests, 97.45% coverage):
- calculator.util.spec.ts: 35 tests — SENAE formulas, volumetric weight,
  4x4 regime, tariff rates, error validation, breakdown
- tracking-id.util.spec.ts: 13 tests — format, uniqueness, validation
- suite-code.util.spec.ts: 13 tests — generation, parsing, address builder
- health.controller.spec.ts: 5 tests — ok/degraded/timestamp
- calculator.spec.ts: 8 tests — controller + service unit tests
- tracking.controller.spec.ts: 9 tests — public search, 404, no sensitive data

Web (apps/web):
- page.tsx: Full landing page matching documentation (Hero, Services,
  8 Modules, Tracking states, SENAE calculator preview, 6 Roles, Companies)
- /calculadora: SENAE categories + API usage guide
- /tracking: Tracking ID format + 11 states reference

Seed (packages/database/prisma/seed.ts):
- Creates all 6 roles (SUPER_ADMIN, ADMIN_EMPRESA, OPERADOR_BODEGA,
  AGENTE_ADUANERO, CLIENTE, SOPORTE)
- Assigns Suite EC-00001 to demo client
- Creates demo Package EC-20260506-000001 with status history
- Creates demo PreAlert and B2BRequest

.env.example: Added all 9 integrations from doc §18:
  Amazon SP-API, SENAE WebService, Stripe/PayPhone, WhatsApp Business API,
  SendGrid/SES, SMS/Twilio, FedEx/DHL/UPS couriers, S3/MinIO, Sentry

migration.sql: Updated to reflect all new models and fields
turbo.json + package.json: Added test, test:cov, test:ci tasks + db:seed script
This commit is contained in:
Lizandro Guarnizo
2026-06-01 08:25:01 -05:00
parent 094ea9cf81
commit 93db76897d
26 changed files with 4651 additions and 201 deletions
+86
View File
@@ -0,0 +1,86 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Calculadora de Envíos SENAE — Moraworld Imports",
description: "Calcula el costo total de tu envío desde EE.UU. a Ecuador incluyendo FODINFA, IVA, arancel y flete. Sin registro.",
};
export default function CalculadoraPage() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
return (
<main style={{ minHeight: "100vh", background: "#F3F4F6", padding: "40px 20px" }}>
<div style={{ maxWidth: 700, margin: "0 auto" }}>
<div style={{ marginBottom: 32 }}>
<h1 style={{ fontSize: "1.8rem", fontWeight: 800, marginBottom: 8 }}>
Calculadora SENAE
</h1>
<p style={{ color: "#6B7280" }}>
Sin necesidad de registro. Conoce el costo total antes de comprar.
</p>
</div>
{/* Nota: el formulario interactivo se implementa en Fase 1 con client component */}
<div style={{
background: "#fff", borderRadius: 16, padding: 32,
border: "1px solid #E5E7EB", marginBottom: 24,
}}>
<h2 style={{ fontWeight: 700, marginBottom: 16, fontSize: "1rem" }}>
Ingresa los datos de tu paquete
</h2>
<p style={{ color: "#6B7280", fontSize: ".88rem", marginBottom: 20 }}>
El formulario interactivo se habilitará en la Fase 1. Por ahora puedes usar el
endpoint de la API directamente:
</p>
<div style={{
background: "#0D1117", borderRadius: 10, padding: "16px 20px",
fontFamily: "monospace", fontSize: ".8rem", color: "#A5B4FC",
lineHeight: 2,
}}>
<div style={{ color: "rgba(255,255,255,.3)", marginBottom: 8 }}>{`// GET /api/calculator`}</div>
<div>
<span style={{ color: "#6EE7B7" }}>GET</span>{" "}
{apiUrl}/api/calculator?<span style={{ color: "#FCD34D" }}>value</span>=150&
<span style={{ color: "#FCD34D" }}>weight</span>=2&
<span style={{ color: "#FCD34D" }}>category</span>=REGIMEN_4X4
</div>
<div style={{ marginTop: 12, color: "rgba(255,255,255,.3)" }}>
{`// Parámetros: value, weight, category, length, width, height, shipments`}
</div>
</div>
</div>
{/* Categorías SENAE informativas */}
<div style={{
background: "#fff", borderRadius: 16, padding: 32,
border: "1px solid #E5E7EB",
}}>
<h2 style={{ fontWeight: 700, marginBottom: 20, fontSize: "1rem" }}>
Categorías SENAE aplicables
</h2>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
{[
{ name: "Régimen 4×4", rate: "0%", color: "#10B981", bg: "#D1FAE5", desc: "Hasta $400 · 4 kg · 4 envíos/año" },
{ name: "Categoría B", rate: "10%", color: "#1D4ED8", bg: "#DBEAFE", desc: "Bienes generales" },
{ name: "Categoría C", rate: "20%", color: "#FF6B00", bg: "#FFEDD5", desc: "Textiles, calzado, hogar" },
{ name: "Categoría D", rate: "015%", color: "#8B5CF6", bg: "#EDE9FE", desc: "Electrónicos · puede requerir INEN" },
].map((c) => (
<div key={c.name} style={{
background: c.bg, borderRadius: 10, padding: 16, textAlign: "center",
}}>
<div style={{ fontWeight: 800, marginBottom: 4 }}>{c.name}</div>
<div style={{ fontSize: "1.8rem", fontWeight: 900, color: c.color, lineHeight: 1, marginBottom: 6 }}>{c.rate}</div>
<div style={{ fontSize: ".75rem", color: "#4B5563" }}>{c.desc}</div>
</div>
))}
</div>
<div style={{ marginTop: 20, padding: 16, background: "#F3F4F6", borderRadius: 8 }}>
<p style={{ fontSize: ".82rem", color: "#6B7280", lineHeight: 1.6 }}>
<strong>Fórmula:</strong> Flete (Peso × $3.50/lb) + Seguro (2%) + FODINFA (0.5%) + Arancel (% categoría) + IVA (15%)
</p>
</div>
</div>
</div>
</main>
);
}
+440 -105
View File
@@ -1,115 +1,450 @@
async function getApiHealth() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
try {
const res = await fetch(`${apiUrl}/api/health`, {
next: { revalidate: 10 },
});
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
import type { Metadata } from "next";
import Link from "next/link";
export default async function HomePage() {
const health = await getApiHealth();
export const metadata: Metadata = {
title: "Moraworld Imports — Tu casillero en New Jersey para Ecuador",
description:
"Compra en Amazon, eBay, Walmart y más. Recibe en tu Suite en New Jersey y enviamos a Ecuador con gestión aduanera SENAE incluida.",
keywords: ["casillero New Jersey", "importaciones Ecuador", "SENAE", "Amazon Ecuador", "envíos EE.UU. Ecuador"],
};
// ─── Datos estáticos de la landing (sin DB, SSG) ──────────────
const TRACKING_STATES = [
{ num: "01", name: "REGISTRADO", color: "#6B7280", desc: "El cliente registró la compra" },
{ num: "02", name: "EN_TRANSITO_BODEGA", color: "#F59E0B", desc: "El vendedor despachó hacia bodega NJ" },
{ num: "03", name: "RECIBIDO_BODEGA", color: "#3B82F6", desc: "Bodega NJ confirmó recepción" },
{ num: "04", name: "EN_VERIFICACION", color: "#8B5CF6", desc: "Pesando, midiendo y fotografiando" },
{ num: "05", name: "VERIFICADO", color: "#10B981", desc: "Peso confirmado. Cobro aplicado." },
{ num: "06", name: "DECLARACION_ADUANERA", color: "#0057FF", desc: "DSI aprobada por la SENAE" },
{ num: "07", name: "EN_TRANSITO_ECUADOR", color: "#F97316", desc: "En vuelo hacia Ecuador" },
{ num: "08", name: "EN_ADUANA_ECUADOR", color: "#EF4444", desc: "Inspección en aduana Ecuador" },
{ num: "09", name: "LISTO_ENTREGA", color: "#84CC16", desc: "Listo para entrega al cliente" },
{ num: "10", name: "ENTREGADO", color: "#10B981", desc: "Paquete entregado en Ecuador ✓" },
{ num: "11", name: "INCIDENCIA", color: "#EF4444", desc: "Problema reportado ⚠" },
];
const SENAE_CATEGORIES = [
{ name: "Régimen 4×4", rate: "0%", color: "#10B981", bg: "#D1FAE5", desc: "Hasta 4 envíos/año · máx. $400 · máx. 4 kg · Sin arancel" },
{ name: "Categoría B", rate: "10%", color: "#1D4ED8", bg: "#DBEAFE", desc: "Bienes de consumo general" },
{ name: "Categoría C", rate: "20%", color: "#FF6B00", bg: "#FFEDD5", desc: "Textiles, calzado, hogar" },
{ name: "Categoría D", rate: "015%", color: "#8B5CF6", bg: "#EDE9FE", desc: "Electrónicos · puede requerir INEN" },
];
const MODULES = [
{ icon: "🌐", title: "Landing Pública", desc: "Calculadora SENAE, tracking sin login, tarifas y FAQ", tag: "Sin registro" },
{ icon: "👤", title: "Portal Cliente", desc: "Suite personalizada, pre-alertas, mis paquetes, historial", tag: "B2C · Ecuador" },
{ icon: "🏭", title: "Portal Bodega NJ", desc: "Recepción, peso, fotos, despacho desde 150 N Day St", tag: "Operador · NJ" },
{ icon: "⚙️", title: "Portal Admin", desc: "Dashboard, usuarios, tarifas, reportes, auditoría", tag: "Admin / Super Admin" },
{ icon: "🛃", title: "Módulo SENAE", desc: "DSI automática, cálculo FODINFA+IVA+Arancel, WebService", tag: "Agente Aduanero" },
{ icon: "📍", title: "Tracking", desc: "ID único EC-YYYYMMDD-XXXXXX · 11 estados · búsqueda pública", tag: "Todos los roles" },
{ icon: "📄", title: "Pre-alerta", desc: "Aviso previo con factura antes de llegada a bodega", tag: "Cliente" },
{ icon: "🚢", title: "Carga Pesada B2B", desc: "Pallets, contenedores, cotización especial, INEN", tag: "Importadores" },
];
const ROLES = [
{ icon: "👑", name: "Super Admin", desc: "Acceso total. Gestiona tenants y config del sistema.", badge: "Acceso total", bg: "#EEF3FF" },
{ icon: "🏢", name: "Admin Empresa", desc: "Administra su tenant: usuarios, tarifas, reportes.", badge: "Tenant propio", bg: "#EDE9FE" },
{ icon: "📦", name: "Operador Bodega", desc: "Personal en NJ: verifica, pesa, fotografía y despacha.", badge: "Bodega NJ", bg: "#FEF3C7" },
{ icon: "🛃", name: "Agente Aduanero", desc: "Revisa y aprueba declaraciones ante la SENAE.", badge: "SENAE", bg: "#D1FAE5" },
{ icon: "🧑‍💻", name: "Cliente Final", desc: "Registra compras, pre-alertas y hace seguimiento.", badge: "B2C · Ecuador", bg: "#FFF3E8" },
{ icon: "🎧", name: "Soporte", desc: "Solo lectura: puede ver info de clientes y paquetes.", badge: "Solo lectura", bg: "#F3F4F6" },
];
// ─── Componentes ──────────────────────────────────────────────
function Navbar() {
return (
<main>
<header
style={{
borderBottom: "1px solid var(--gray-200)",
padding: "16px 24px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<span style={{ fontWeight: 800, fontSize: "1.2rem", color: "var(--primary)" }}>
Moraworld<span style={{ color: "var(--accent)" }}>.</span>Imports
</span>
<span
style={{
fontSize: ".75rem",
fontWeight: 600,
padding: "4px 10px",
borderRadius: 999,
background: health?.status === "ok" ? "#D1FAE5" : "#FEF3C7",
color: health?.status === "ok" ? "#065F46" : "#92400E",
}}
>
API: {health?.status === "ok" ? "conectada" : "local / pendiente"}
</span>
</header>
<nav style={{
position: "sticky", top: 0, zIndex: 50,
background: "rgba(255,255,255,0.95)",
backdropFilter: "blur(12px)",
borderBottom: "1px solid #E5E7EB",
display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "0 40px", height: 60,
}}>
<span style={{ fontWeight: 800, fontSize: "1.15rem", color: "#111827" }}>
Moraworld<span style={{ color: "#FF6B00" }}>.</span>Imports
</span>
<div style={{ display: "flex", gap: 24, alignItems: "center", fontSize: ".9rem" }}>
<Link href="/calculadora" style={{ color: "#4B5563" }}>Calculadora</Link>
<Link href="/tracking" style={{ color: "#4B5563" }}>Tracking</Link>
<Link href="/carga-pesada" style={{ color: "#4B5563" }}>Carga Pesada</Link>
<Link href="/portal" style={{
background: "#0057FF", color: "#fff",
padding: "8px 20px", borderRadius: 8, fontWeight: 600,
}}>Mi Portal</Link>
</div>
</nav>
);
}
<section
style={{
background: "linear-gradient(135deg, #0D1117 0%, #0D2150 100%)",
padding: "80px 24px",
color: "#fff",
}}
>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
<p
style={{
function Hero() {
return (
<section style={{
background: "linear-gradient(135deg, #0D1117 0%, #0D2150 100%)",
padding: "100px 40px", color: "#fff",
}}>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
<div style={{
display: "inline-block",
background: "rgba(0,87,255,.15)", border: "1px solid rgba(0,87,255,.3)",
color: "#7AADFF", fontSize: ".8rem", fontWeight: 600,
padding: "6px 14px", borderRadius: 999, marginBottom: 24,
}}>
ISO/IEC 27001 · SENAE Autorizado · Cuenca, Ecuador
</div>
<h1 style={{ fontSize: "clamp(2rem,5vw,3.2rem)", fontWeight: 800, lineHeight: 1.15, marginBottom: 20 }}>
Tu casillero en <span style={{ color: "#FF6B00" }}>New Jersey</span>
<br />para recibir en Ecuador
</h1>
<p style={{ color: "#9CA3AF", maxWidth: 560, fontSize: "1.05rem", marginBottom: 16 }}>
Compra en Amazon, eBay, Walmart y más. Te asignamos una Suite personal en nuestra
bodega en NJ. Gestionamos el envío y la declaración aduanera SENAE por ti.
</p>
<div style={{
background: "rgba(0,87,255,.1)", border: "1px solid rgba(0,87,255,.25)",
borderRadius: 10, padding: "14px 18px", marginBottom: 32,
fontFamily: "monospace", fontSize: ".9rem", color: "#A5B4FC", maxWidth: 520,
}}>
📍 <strong style={{ color: "#FF6B00" }}>Tu Suite:</strong>{" "}
150 N Day St, Suite EC-XXXXX, City of Orange, NJ 07050, EE.UU.
</div>
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
<Link href="/registro" style={{
background: "#0057FF", color: "#fff",
padding: "14px 28px", borderRadius: 8, fontWeight: 700, fontSize: ".95rem",
}}>
Crear mi Suite gratis
</Link>
<Link href="/calculadora" style={{
border: "2px solid rgba(255,255,255,.2)", color: "#9CA3AF",
padding: "12px 26px", borderRadius: 8, fontSize: ".9rem",
}}>
Calcular mi envío
</Link>
</div>
<div style={{ display: "flex", gap: 32, marginTop: 48, flexWrap: "wrap" }}>
{[
["🏢", "Moraworld Imports S.A.S.", "Ecuador · RUC activo"],
["🏭", "Mora Global Import LLC", "New Jersey, EE.UU."],
["🛃", "Autorización SENAE", "Declaración automática"],
].map(([icon, title, sub]) => (
<div key={title} style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
<span style={{ fontSize: "1.5rem" }}>{icon}</span>
<div>
<div style={{ fontWeight: 600, fontSize: ".88rem", color: "#fff" }}>{title}</div>
<div style={{ fontSize: ".78rem", color: "#6B7280" }}>{sub}</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}
function ServicesSection() {
return (
<section style={{ padding: "80px 40px", background: "#F3F4F6" }}>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
<div style={{ textAlign: "center", marginBottom: 48 }}>
<h2 style={{ fontSize: "1.8rem", fontWeight: 800, marginBottom: 8 }}>Dos servicios, un solo sistema</h2>
<p style={{ color: "#6B7280", fontSize: ".95rem" }}>Casillero personal para compras online y carga pesada para importadores B2B.</p>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
<div style={{ background: "#fff", borderRadius: 16, padding: 32, border: "2px solid #BFDBFE" }}>
<div style={{ fontSize: "2.5rem", marginBottom: 12 }}>📦</div>
<h3 style={{ fontWeight: 800, fontSize: "1.2rem", marginBottom: 8 }}>Casillero Personal</h3>
<p style={{ color: "#6B7280", marginBottom: 16 }}>
Para compras individuales en Amazon, eBay, Walmart y más. Suite personalizada, tracking en tiempo real
y declaración SENAE automática bajo el régimen 4×4.
</p>
<ul style={{ color: "#374151", paddingLeft: 20, display: "flex", flexDirection: "column", gap: 6, fontSize: ".88rem" }}>
{["Suite virtual asignada al registrarse", "Pre-alerta con carga de factura", "Calculadora de costos SENAE", "11 estados de tracking en tiempo real", "Notificaciones WhatsApp · SMS · Email"].map(i => (
<li key={i}>{i}</li>
))}
</ul>
<Link href="/casillero" style={{
display: "inline-block", marginTop: 20, background: "#0057FF", color: "#fff",
padding: "10px 20px", borderRadius: 8, fontWeight: 600, fontSize: ".88rem",
}}>
Conocer más
</Link>
</div>
<div style={{ background: "#fff", borderRadius: 16, padding: 32, border: "2px solid #FED7AA" }}>
<div style={{ fontSize: "2.5rem", marginBottom: 12 }}>🚢</div>
<h3 style={{ fontWeight: 800, fontSize: "1.2rem", marginBottom: 8 }}>Carga Pesada / Pallets B2B</h3>
<p style={{ color: "#6B7280", marginBottom: 16 }}>
Para importadores mayoristas: pallets, contenedores, volumen alto. Cotización personalizada,
certificación INEN y declaración formal DAI ante la SENAE.
</p>
<ul style={{ color: "#374151", paddingLeft: 20, display: "flex", flexDirection: "column", gap: 6, fontSize: ".88rem" }}>
{["Cotización especial por tipo de mercancía", "Verificación de requisitos INEN", "Declaración formal DAI", "Asesoría en partidas arancelarias", "Expediente B2B con ID de seguimiento"].map(i => (
<li key={i}>{i}</li>
))}
</ul>
<Link href="/carga-pesada/cotizacion" style={{
display: "inline-block", marginTop: 20, background: "#FF6B00", color: "#fff",
padding: "10px 20px", borderRadius: 8, fontWeight: 600, fontSize: ".88rem",
}}>
Solicitar cotización
</Link>
</div>
</div>
</div>
</section>
);
}
function ModulesSection() {
return (
<section style={{ padding: "80px 40px", background: "#fff" }}>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
<div style={{ textAlign: "center", marginBottom: 48 }}>
<h2 style={{ fontSize: "1.8rem", fontWeight: 800, marginBottom: 8 }}>8 módulos integrados</h2>
<p style={{ color: "#6B7280" }}>Toda la operación logística cubierta en una sola plataforma.</p>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 16 }}>
{MODULES.map((m) => (
<div key={m.title} style={{
background: "#F9FAFB", borderRadius: 12, padding: 20,
border: "1px solid #E5E7EB", transition: "box-shadow .2s",
}}>
<div style={{ fontSize: "1.8rem", marginBottom: 10 }}>{m.icon}</div>
<div style={{ fontWeight: 700, fontSize: ".9rem", marginBottom: 6 }}>{m.title}</div>
<p style={{ color: "#6B7280", fontSize: ".8rem", marginBottom: 10, lineHeight: 1.5 }}>{m.desc}</p>
<span style={{
background: "#EEF3FF", color: "#0057FF",
fontSize: ".68rem", fontWeight: 700, padding: "2px 8px",
borderRadius: 999,
}}>{m.tag}</span>
</div>
))}
</div>
</div>
</section>
);
}
function TrackingSection() {
return (
<section style={{ padding: "80px 40px", background: "#F3F4F6" }}>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 48, alignItems: "start" }}>
<div>
<h2 style={{ fontSize: "1.8rem", fontWeight: 800, marginBottom: 12 }}>
Tracking en tiempo real
</h2>
<p style={{ color: "#6B7280", marginBottom: 16 }}>
Cada paquete recibe un ID único con 11 estados que cubren todo el ciclo de vida.
Búsqueda pública sin necesidad de crear cuenta.
</p>
<div style={{
background: "#0D1117", borderRadius: 10, padding: "14px 18px",
fontFamily: "monospace", fontSize: ".88rem", color: "#A5B4FC",
marginBottom: 20, border: "1px solid rgba(0,87,255,.3)",
}}>
Formato: <strong style={{ color: "#FF6B00" }}>EC-YYYYMMDD-XXXXXX</strong><br />
Ejemplo: <strong>EC-20260506-082341</strong>
</div>
<Link href="/tracking" style={{
background: "#0057FF", color: "#fff",
padding: "12px 24px", borderRadius: 8, fontWeight: 600, fontSize: ".9rem",
display: "inline-block",
background: "rgba(0,87,255,.15)",
border: "1px solid rgba(0,87,255,.3)",
color: "#7AADFF",
fontSize: ".8rem",
fontWeight: 600,
padding: "6px 14px",
borderRadius: 999,
marginBottom: 24,
}}
>
Fase 0 Fundación del software
</p>
<h1 style={{ fontSize: "clamp(2rem, 5vw, 3rem)", fontWeight: 800, lineHeight: 1.15, marginBottom: 16 }}>
Tu casillero en <em style={{ color: "var(--accent)", fontStyle: "normal" }}>New Jersey</em>
<br />
para recibir en Ecuador
</h1>
<p style={{ color: "#9CA3AF", maxWidth: 520, marginBottom: 32 }}>
Monorepo activo: Next.js + NestJS + PostgreSQL. Desarrollo local con Docker; despliegue en Coolify.
</p>
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
<a
href="/status"
style={{
background: "var(--primary)",
color: "#fff",
padding: "14px 28px",
borderRadius: 8,
fontWeight: 600,
}}
>
Ver estado del sistema
</a>
<span
style={{
border: "2px solid rgba(255,255,255,.2)",
color: "#9CA3AF",
padding: "12px 26px",
borderRadius: 8,
fontSize: ".9rem",
}}
>
Prototipos HTML en la raíz del repo
</span>
}}>
Buscar mi paquete
</Link>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{TRACKING_STATES.map((s) => (
<div key={s.num} style={{
display: "flex", gap: 12, alignItems: "center",
background: "#fff", borderRadius: 8, padding: "8px 14px",
border: "1px solid #E5E7EB",
}}>
<div style={{
width: 10, height: 10, borderRadius: "50%",
background: s.color, flexShrink: 0,
}} />
<span style={{ fontFamily: "monospace", fontSize: ".68rem", color: "#9CA3AF", minWidth: 22 }}>{s.num}</span>
<span style={{ fontFamily: "monospace", fontSize: ".78rem", fontWeight: 700, flex: 1 }}>{s.name}</span>
<span style={{ fontSize: ".75rem", color: "#6B7280" }}>{s.desc}</span>
</div>
))}
</div>
</div>
</div>
</section>
);
}
function CalculatorPreviewSection() {
return (
<section style={{ padding: "80px 40px", background: "#fff" }}>
<div style={{ maxWidth: 1100, margin: "0 auto", textAlign: "center" }}>
<h2 style={{ fontSize: "1.8rem", fontWeight: 800, marginBottom: 8 }}>
Calculadora SENAE gratuita
</h2>
<p style={{ color: "#6B7280", marginBottom: 40 }}>
Sin registro. Conoce el costo total de tu envío antes de comprar.
</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 16, marginBottom: 32 }}>
{SENAE_CATEGORIES.map((c) => (
<div key={c.name} style={{
borderRadius: 12, padding: 24, background: c.bg,
border: `1px solid ${c.color}30`, textAlign: "center",
}}>
<div style={{ fontWeight: 800, marginBottom: 4 }}>{c.name}</div>
<div style={{ fontSize: "2.2rem", fontWeight: 900, color: c.color, lineHeight: 1, marginBottom: 8 }}>{c.rate}</div>
<div style={{ fontSize: ".75rem", color: "#4B5563" }}>{c.desc}</div>
</div>
))}
</div>
<div style={{
background: "#0D1117", borderRadius: 12, padding: "20px 24px",
fontFamily: "monospace", fontSize: ".82rem", color: "#A5B4FC",
textAlign: "left", maxWidth: 600, margin: "0 auto 28px",
lineHeight: 2, border: "1px solid rgba(255,255,255,.08)",
}}>
<span style={{ color: "rgba(255,255,255,.25)" }}>// Fórmula (doc §15)</span><br />
<span style={{ color: "#6EE7B7", fontWeight: 700 }}>Flete</span> = Peso_final × <span style={{ color: "#FCD34D" }}>$3.50</span>/lb<br />
<span style={{ color: "#6EE7B7", fontWeight: 700 }}>Seguro</span> = Valor × <span style={{ color: "#FCD34D" }}>2%</span><br />
<span style={{ color: "#6EE7B7", fontWeight: 700 }}>FODINFA</span> = Valor × <span style={{ color: "#FCD34D" }}>0.5%</span><br />
<span style={{ color: "#6EE7B7", fontWeight: 700 }}>Arancel</span> = Valor × <span style={{ color: "#FCD34D" }}>% categoría</span><br />
<span style={{ color: "#6EE7B7", fontWeight: 700 }}>IVA</span> = (Valor + FODINFA + Arancel) × <span style={{ color: "#FCD34D" }}>15%</span><br />
<span style={{ color: "#F9A8D4", fontWeight: 700 }}>TOTAL</span> = Flete + Seguro + FODINFA + Arancel + IVA
</div>
<Link href="/calculadora" style={{
background: "#0057FF", color: "#fff",
padding: "14px 32px", borderRadius: 8, fontWeight: 700,
display: "inline-block", fontSize: ".95rem",
}}>
Calcular mi envío
</Link>
</div>
</section>
);
}
function RolesSection() {
return (
<section style={{ padding: "80px 40px", background: "#F3F4F6" }}>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
<div style={{ textAlign: "center", marginBottom: 48 }}>
<h2 style={{ fontSize: "1.8rem", fontWeight: 800, marginBottom: 8 }}>6 roles, accesos diferenciados</h2>
<p style={{ color: "#6B7280" }}>Cada usuario tiene exactamente lo que necesita. Ni más, ni menos.</p>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 16 }}>
{ROLES.map((r) => (
<div key={r.name} style={{
background: r.bg, borderRadius: 12, padding: 20,
border: "1px solid #E5E7EB",
}}>
<div style={{ fontSize: "1.5rem", marginBottom: 10 }}>{r.icon}</div>
<div style={{ fontWeight: 700, marginBottom: 6 }}>{r.name}</div>
<p style={{ color: "#6B7280", fontSize: ".82rem", marginBottom: 12, lineHeight: 1.5 }}>{r.desc}</p>
<span style={{
background: "rgba(0,0,0,.06)", fontSize: ".7rem", fontWeight: 700,
padding: "2px 10px", borderRadius: 999,
}}>{r.badge}</span>
</div>
))}
</div>
</div>
</section>
);
}
function CompaniesSection() {
return (
<section style={{ padding: "80px 40px", background: "#fff" }}>
<div style={{ maxWidth: 1100, margin: "0 auto", textAlign: "center" }}>
<h2 style={{ fontSize: "1.8rem", fontWeight: 800, marginBottom: 8 }}>Operación legal en dos países</h2>
<p style={{ color: "#6B7280", marginBottom: 40 }}>Empresas constituidas legalmente en Ecuador y EE.UU.</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24, maxWidth: 800, margin: "0 auto" }}>
{[
{
flag: "🇪🇨",
name: "Moraworld Imports S.A.S.",
detail: "Constituida en Ecuador. Representante legal en Cuenca. RUC activo, permisos aduaneros vigentes ante la SENAE.",
badge: "Ecuador · S.A.S. · SENAE · Cuenca",
},
{
flag: "🇺🇸",
name: "Mora Global Import LLC",
detail: "LLC registrada en New Jersey, EE.UU. Bodega operativa en 150 N Day St, City of Orange, NJ 07050.",
badge: "New Jersey · LLC · NJ 07050",
},
].map((c) => (
<div key={c.name} style={{
background: "#fff", borderRadius: 16, padding: 28,
border: "2px solid #E5E7EB", display: "flex", gap: 16, alignItems: "flex-start",
}}>
<span style={{ fontSize: "2.5rem" }}>{c.flag}</span>
<div style={{ textAlign: "left" }}>
<div style={{ fontWeight: 800, marginBottom: 6 }}>{c.name}</div>
<p style={{ color: "#6B7280", fontSize: ".82rem", lineHeight: 1.6, marginBottom: 10 }}>{c.detail}</p>
<span style={{
background: "#EEF3FF", color: "#0057FF",
fontSize: ".7rem", fontWeight: 700, padding: "2px 10px", borderRadius: 999,
}}>{c.badge}</span>
</div>
</div>
))}
</div>
</section>
</div>
</section>
);
}
function Footer() {
return (
<footer style={{
background: "#0D1117", padding: "48px 40px", color: "rgba(255,255,255,.5)",
textAlign: "center",
}}>
<div style={{ fontSize: "1.1rem", fontWeight: 800, color: "#fff", marginBottom: 8 }}>
Moraworld<span style={{ color: "#FF6B00" }}>.</span>Imports
</div>
<p style={{ fontSize: ".82rem", marginBottom: 20 }}>
Moraworld Imports S.A.S. · Mora Global Import LLC
</p>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", justifyContent: "center", marginBottom: 24 }}>
{["ISO/IEC 27001", "SENAE Autorizado", "INEN", "LOPDP Ecuador", "Multi-Tenant"].map((b) => (
<span key={b} style={{
background: "rgba(255,255,255,.07)", color: "rgba(255,255,255,.5)",
fontSize: ".7rem", fontWeight: 700, padding: "4px 12px", borderRadius: 999,
}}>{b}</span>
))}
</div>
<div style={{ display: "flex", gap: 24, justifyContent: "center", flexWrap: "wrap", fontSize: ".82rem" }}>
<Link href="/como-funciona">Cómo funciona</Link>
<Link href="/calculadora">Calculadora</Link>
<Link href="/tracking">Tracking</Link>
<Link href="/tarifas">Tarifas</Link>
<Link href="/quienes-somos">Quiénes somos</Link>
<Link href="/casillero">Casillero</Link>
<Link href="/carga-pesada">Carga Pesada</Link>
</div>
</footer>
);
}
// ─── Página principal ─────────────────────────────────────────
<section style={{ padding: "48px 24px", maxWidth: 1100, margin: "0 auto" }}>
<h2 style={{ fontSize: "1.25rem", fontWeight: 700, marginBottom: 16 }}>Próximos pasos (Fase 1)</h2>
<ul style={{ color: "var(--gray-500)", paddingLeft: 20, display: "flex", flexDirection: "column", gap: 8 }}>
<li>Registro e inicio de sesión con JWT + MFA</li>
<li>Asignación automática de Suite (EC-XXXXX)</li>
<li>Pre-alertas y registro de paquetes</li>
<li>Migración del portal desde portal-cliente.html</li>
</ul>
</section>
</main>
export default function HomePage() {
return (
<>
<Navbar />
<Hero />
<ServicesSection />
<ModulesSection />
<TrackingSection />
<CalculatorPreviewSection />
<RolesSection />
<CompaniesSection />
<Footer />
</>
);
}
+87
View File
@@ -0,0 +1,87 @@
import type { Metadata } from "next";
import Link from "next/link";
export const metadata: Metadata = {
title: "Rastrear Paquete — Moraworld Imports",
description: "Busca tu paquete con el Tracking ID (formato EC-YYYYMMDD-XXXXXX). Sin necesidad de iniciar sesión.",
};
export default function TrackingPage() {
return (
<main style={{ minHeight: "100vh", background: "#F3F4F6", padding: "40px 20px" }}>
<div style={{ maxWidth: 600, margin: "0 auto" }}>
<div style={{ marginBottom: 32 }}>
<Link href="/" style={{ color: "#6B7280", fontSize: ".88rem" }}> Inicio</Link>
<h1 style={{ fontSize: "1.8rem", fontWeight: 800, marginTop: 8, marginBottom: 8 }}>
Rastrear paquete
</h1>
<p style={{ color: "#6B7280" }}>
Ingresa tu Tracking ID para ver el estado de tu paquete. No necesitas iniciar sesión.
</p>
</div>
<div style={{
background: "#fff", borderRadius: 16, padding: 32,
border: "1px solid #E5E7EB", marginBottom: 24,
}}>
{/* Formulario — cliente component en Fase 1 */}
<label style={{ display: "block", fontWeight: 600, marginBottom: 8, fontSize: ".9rem" }}>
Tracking ID
</label>
<div style={{
background: "#F3F4F6", borderRadius: 8, padding: "12px 16px",
fontFamily: "monospace", color: "#9CA3AF", fontSize: ".9rem",
border: "1px solid #E5E7EB", marginBottom: 8,
}}>
EC-20260506-082341
</div>
<p style={{ color: "#9CA3AF", fontSize: ".78rem", marginBottom: 20 }}>
Formato: <strong style={{ fontFamily: "monospace" }}>EC-YYYYMMDD-XXXXXX</strong>
</p>
<div style={{
background: "#EEF3FF", borderRadius: 8, padding: "12px 16px",
fontSize: ".84rem", color: "#1D4ED8",
}}>
El formulario de búsqueda interactiva se habilita en Fase 1.
Por ahora puedes usar la API: <code style={{ fontFamily: "monospace" }}>GET /api/tracking/:id</code>
</div>
</div>
<div style={{
background: "#fff", borderRadius: 16, padding: 32,
border: "1px solid #E5E7EB",
}}>
<h2 style={{ fontWeight: 700, marginBottom: 16, fontSize: "1rem" }}>
11 estados del ciclo de vida
</h2>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{[
{ num: "01", name: "REGISTRADO", color: "#6B7280", desc: "Cliente registró la compra" },
{ num: "02", name: "EN_TRANSITO_BODEGA", color: "#F59E0B", desc: "Despachado hacia NJ" },
{ num: "03", name: "RECIBIDO_BODEGA", color: "#3B82F6", desc: "Bodega NJ confirmó recepción" },
{ num: "04", name: "EN_VERIFICACION", color: "#8B5CF6", desc: "Pesando y fotografiando" },
{ num: "05", name: "VERIFICADO", color: "#10B981", desc: "Peso confirmado" },
{ num: "06", name: "DECLARACION_ADUANERA", color: "#0057FF", desc: "DSI aprobada por SENAE" },
{ num: "07", name: "EN_TRANSITO_ECUADOR", color: "#F97316", desc: "En vuelo a Ecuador" },
{ num: "08", name: "EN_ADUANA_ECUADOR", color: "#EF4444", desc: "Inspección en aduana" },
{ num: "09", name: "LISTO_ENTREGA", color: "#84CC16", desc: "Listo para entrega" },
{ num: "10", name: "ENTREGADO", color: "#10B981", desc: "Entregado al cliente ✓" },
{ num: "11", name: "INCIDENCIA", color: "#EF4444", desc: "Problema reportado ⚠" },
].map((s) => (
<div key={s.num} style={{
display: "flex", gap: 10, alignItems: "center",
padding: "8px 12px", background: "#F9FAFB",
borderRadius: 6, border: "1px solid #E5E7EB",
}}>
<div style={{ width: 8, height: 8, borderRadius: "50%", background: s.color, flexShrink: 0 }} />
<span style={{ fontFamily: "monospace", fontSize: ".65rem", color: "#9CA3AF", minWidth: 18 }}>{s.num}</span>
<span style={{ fontFamily: "monospace", fontSize: ".75rem", fontWeight: 700, flex: 1 }}>{s.name}</span>
<span style={{ fontSize: ".72rem", color: "#6B7280" }}>{s.desc}</span>
</div>
))}
</div>
</div>
</div>
</main>
);
}