feat: initial commit - Moraworld Imports project
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# ─── Base de datos ───
|
||||
# Opción A — Coolify (recomendado si ya tienes Postgres ahí):
|
||||
# DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/NOMBRE_BD?schema=moraworld"
|
||||
# Nota: en Postgres 15+ el schema "public" suele no permitir CREATE; usamos "moraworld".
|
||||
#
|
||||
# Opción B — Local con docker compose:
|
||||
DATABASE_URL="postgresql://moraworld:moraworld_dev@localhost:5432/moraworld?schema=public"
|
||||
|
||||
# ─── Redis ───
|
||||
# Opción A — Coolify:
|
||||
# REDIS_URL="redis://:PASSWORD@HOST:6379"
|
||||
# REDIS_URL="rediss://:PASSWORD@HOST:6380" # si TLS
|
||||
#
|
||||
# Opción B — Local:
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
|
||||
# ─── API (NestJS) ───
|
||||
API_PORT=3001
|
||||
API_URL="http://localhost:3001"
|
||||
JWT_SECRET="change-me-in-production-min-32-chars-long"
|
||||
JWT_EXPIRES_IN="15m"
|
||||
JWT_REFRESH_EXPIRES_IN="7d"
|
||||
|
||||
# ─── Web (Next.js) ───
|
||||
WEB_PORT=3000
|
||||
NEXT_PUBLIC_API_URL="http://localhost:3001"
|
||||
|
||||
# ─── CORS (coma separada; en Coolify usar dominio real) ───
|
||||
CORS_ORIGINS="http://localhost:3000"
|
||||
|
||||
# ─── Moraworld (negocio) ───
|
||||
WAREHOUSE_ADDRESS_STREET="150 N Day St"
|
||||
WAREHOUSE_ADDRESS_CITY="City of Orange"
|
||||
WAREHOUSE_ADDRESS_STATE="NJ"
|
||||
WAREHOUSE_ADDRESS_ZIP="07050"
|
||||
WAREHOUSE_ADDRESS_COUNTRY="US"
|
||||
|
||||
# ─── S3 / archivos (opcional Fase 1; MinIO local en docker-compose) ───
|
||||
# S3_ENDPOINT="http://localhost:9000"
|
||||
# S3_BUCKET="moraworld"
|
||||
# S3_ACCESS_KEY="minioadmin"
|
||||
# S3_SECRET_KEY="minioadmin"
|
||||
# S3_REGION="us-east-1"
|
||||
# S3_FORCE_PATH_STYLE="true"
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Build
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
.turbo/
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
!.env.example
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Prisma
|
||||
packages/database/prisma/migrations/**/migration_lock.toml
|
||||
|
||||
# Test / coverage
|
||||
coverage/
|
||||
|
||||
# Coolify / Docker local overrides
|
||||
docker-compose.override.yml
|
||||
@@ -0,0 +1,96 @@
|
||||
# Moraworld Imports — Software
|
||||
|
||||
Casillero internacional (EE.UU. → Ecuador). Monorepo con **Next.js**, **NestJS**, **PostgreSQL** y **Redis**.
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm 9+ (ver instalación abajo si sale `command not found`)
|
||||
- **Postgres + Redis** — en Coolify (recomendado) **o** Docker local
|
||||
|
||||
### Instalar pnpm (si no lo tienes)
|
||||
|
||||
```bash
|
||||
npm install -g pnpm@9.15.0
|
||||
```
|
||||
|
||||
Cierra y abre la terminal, luego:
|
||||
|
||||
```bash
|
||||
pnpm -v # debe mostrar 9.15.x
|
||||
```
|
||||
|
||||
**Alternativa sin instalar global** (usa npx):
|
||||
|
||||
```bash
|
||||
npx pnpm@9.15.0 install
|
||||
npx pnpm@9.15.0 dev
|
||||
```
|
||||
|
||||
## Desarrollo local (paso a paso)
|
||||
|
||||
### 1. Variables de entorno
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Pega `DATABASE_URL` y `REDIS_URL` de Coolify. Guía: **[docs/VARIABLES-COOLIFY.md](./docs/VARIABLES-COOLIFY.md)**
|
||||
|
||||
### 2. Infraestructura (solo si NO usas Coolify para BD)
|
||||
|
||||
```bash
|
||||
pnpm docker:up
|
||||
```
|
||||
|
||||
Levanta **PostgreSQL** (`localhost:5432`) y **Redis** (`localhost:6379`).
|
||||
Si Postgres/Redis ya están en Coolify, **omite este paso**.
|
||||
|
||||
### 3. Instalar dependencias
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 4. Base de datos
|
||||
|
||||
```bash
|
||||
pnpm db:generate
|
||||
pnpm db:migrate
|
||||
```
|
||||
|
||||
Nombre sugerido para la primera migración: `init`.
|
||||
|
||||
### 5. Arrancar apps
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
| Servicio | URL |
|
||||
|----------|-----|
|
||||
| Web (Next.js) | http://localhost:3000 |
|
||||
| API (NestJS) | http://localhost:3001/api/health |
|
||||
| Estado | http://localhost:3000/status |
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
apps/
|
||||
api/ # NestJS REST API
|
||||
web/ # Next.js (landing + futuro portal)
|
||||
packages/
|
||||
database/ # Prisma + PostgreSQL
|
||||
```
|
||||
|
||||
## Coolify
|
||||
|
||||
Ver **[docs/COOLIFY.md](./docs/COOLIFY.md)** para desplegar API y Web en Coolify (Postgres/Redis como servicios del panel o externos).
|
||||
|
||||
## Roadmap
|
||||
|
||||
Ver **[ROADMAP.md](./ROADMAP.md)** — Fase 0 en curso.
|
||||
|
||||
## Prototipos HTML (legacy)
|
||||
|
||||
Los archivos `index.html`, `portal-cliente.html`, etc. en la raíz del repo son maquetas de referencia hasta completar la migración a Next.js (Fase 1).
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
# Moraworld Imports — Roadmap de Desarrollo
|
||||
|
||||
**Versión:** 1.0
|
||||
**Fecha inicio:** Mayo 2026
|
||||
**Estado:** En planificación → Fase 0
|
||||
|
||||
---
|
||||
|
||||
## Visión del producto
|
||||
|
||||
Plataforma logística transfronteriza (EE.UU. → Ecuador) con:
|
||||
|
||||
- **Casillero B2C** — automatizado (Amazon, eBay, etc.)
|
||||
- **Carga pesada B2B** — cotización manual
|
||||
- **Bodega NJ** — operaciones físicas sincronizadas por API
|
||||
- **Aduanas** — declaraciones SENAE
|
||||
- **Multi-tenant** — una instalación, datos aislados por empresa
|
||||
|
||||
**Bodega:** `150 N Day St, Suite EC-XXXXX, City of Orange, NJ 07050`
|
||||
|
||||
---
|
||||
|
||||
## Stack acordado (base del roadmap)
|
||||
|
||||
| Capa | Tecnología |
|
||||
|------|------------|
|
||||
| Monorepo | Turborepo + pnpm |
|
||||
| Frontend | Next.js 15 (App Router) + TypeScript + Tailwind CSS |
|
||||
| API | NestJS + TypeScript |
|
||||
| ORM / migraciones | Prisma |
|
||||
| Base de datos | PostgreSQL 16 |
|
||||
| Cache / colas | Redis + BullMQ |
|
||||
| Archivos | AWS S3 (facturas, fotos) |
|
||||
| Auth | JWT + refresh tokens + MFA TOTP |
|
||||
| Contenedores | Docker Compose (local) |
|
||||
| CI | GitHub Actions |
|
||||
| Docs API | Swagger (OpenAPI) |
|
||||
|
||||
> El documento de requerimientos lista esto como **sugerido**; este roadmap lo adopta como decisión de implementación salvo cambio explícito.
|
||||
|
||||
---
|
||||
|
||||
## Estructura del repositorio (objetivo final)
|
||||
|
||||
```
|
||||
moraworld/
|
||||
├── apps/
|
||||
│ ├── web/ # Next.js — landing + portal cliente + tracking público
|
||||
│ ├── admin/ # Next.js — portal admin + agente aduanero (Fase 3+)
|
||||
│ ├── warehouse/ # Next.js — portal bodega NJ (Fase 2)
|
||||
│ └── api/ # NestJS — REST API + workers
|
||||
├── packages/
|
||||
│ ├── database/ # Prisma schema + client
|
||||
│ ├── shared/ # Tipos, constantes, validaciones Zod
|
||||
│ └── ui/ # Componentes compartidos (opcional Fase 2)
|
||||
├── docker/
|
||||
│ └── docker-compose.yml
|
||||
├── docs/
|
||||
│ └── api/ # OpenAPI export
|
||||
└── ROADMAP.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fases y cronograma estimado
|
||||
|
||||
| Fase | Nombre | Duración est. | Entregable principal |
|
||||
|:----:|--------|:-------------:|----------------------|
|
||||
| 0 | Fundación | 1 semana | Repo, Docker, BD, auth básico |
|
||||
| 1 | MVP Casillero (cliente) | 3–4 semanas | Registro, suite, paquetes, pre-alerta |
|
||||
| 2 | Bodega + API externa | 3–4 semanas | Portal operador NJ + sync software bodega |
|
||||
| 3 | Admin + tarifas | 2–3 semanas | Dashboard, roles, configuración |
|
||||
| 4 | Calculadora + pagos | 2–3 semanas | SENAE en calculadora + cobro en línea |
|
||||
| 5 | Aduanas SENAE | 4–6 semanas | DSI, envío WebService, agente aduanero |
|
||||
| 6 | Notificaciones + integraciones | 2–3 semanas | Email, SMS, WhatsApp, Amazon SP-API |
|
||||
| 7 | B2B carga pesada | 2 semanas | Cotizaciones, expedientes INEN |
|
||||
| 8 | Producción + hardening | 2–3 semanas | Deploy, backups, auditoría, ISO prep |
|
||||
|
||||
**Total estimado:** 19–26 semanas (~5–6 meses) con 1–2 desarrolladores.
|
||||
|
||||
---
|
||||
|
||||
## Fase 0 — Fundación (Semana 1)
|
||||
|
||||
### Objetivo
|
||||
Entorno de desarrollo reproducible y esqueleto del monorepo.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] Inicializar git + `.gitignore` + ramas (`main`, `develop`)
|
||||
- [ ] Crear monorepo Turborepo (pnpm workspaces)
|
||||
- [ ] `docker-compose`: PostgreSQL, Redis, (MinIO opcional para S3 local)
|
||||
- [ ] App `api` NestJS: health check, CORS, validación global
|
||||
- [ ] Package `database`: Prisma + primera migración
|
||||
- [ ] App `web` Next.js: layout base, tipografía Inter, colores marca
|
||||
- [ ] Variables de entorno (`.env.example`)
|
||||
- [ ] README con instrucciones `pnpm install` + `docker compose up`
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- `pnpm dev` levanta API + web en local
|
||||
- API responde `GET /health` → 200
|
||||
- Prisma conecta a PostgreSQL y aplica migraciones
|
||||
|
||||
---
|
||||
|
||||
## Fase 1 — MVP Casillero cliente (Semanas 2–5)
|
||||
|
||||
### Objetivo
|
||||
Reemplazar el flujo mock de `login.html`, `registro.html` y `portal-cliente.html` con backend real.
|
||||
|
||||
### Modelo de datos (Prisma — núcleo)
|
||||
|
||||
```
|
||||
Tenant, User, Role, UserRole
|
||||
Suite (código EC-XXXXX por usuario)
|
||||
Package, PackageStatusHistory
|
||||
PreAlert, Document (S3 keys)
|
||||
TariffZone (básico)
|
||||
AuditLog
|
||||
```
|
||||
|
||||
### Tareas backend (NestJS)
|
||||
|
||||
- [ ] Módulo `auth`: registro, login, refresh, logout
|
||||
- [ ] MFA TOTP: activar / verificar en login
|
||||
- [ ] Módulo `users`: perfil, actualizar teléfono/WhatsApp
|
||||
- [ ] Módulo `suites`: asignación automática al registrar (`EC-{secuencia}`)
|
||||
- [ ] Módulo `packages`: CRUD, cambio de estado, historial inmutable
|
||||
- [ ] Módulo `pre-alerts`: crear, listar, adjuntar factura (upload S3)
|
||||
- [ ] Módulo `documents`: presigned URLs para subida
|
||||
- [ ] RBAC: guard `Cliente` vs roles internos (stub para fases posteriores)
|
||||
- [ ] Generador Tracking ID: `EC-YYYYMMDD-XXXXXX`
|
||||
- [ ] Seed: tenant Moraworld + usuario demo
|
||||
|
||||
### Tareas frontend (Next.js `web`)
|
||||
|
||||
- [ ] Migrar diseño de `index.html` → `/` (landing)
|
||||
- [ ] `/login`, `/registro` (multi-paso como HTML actual)
|
||||
- [ ] `/portal` — layout con sidebar (dashboard, paquetes, pre-alertas)
|
||||
- [ ] `/portal/mi-casillero` — dirección Suite copiable
|
||||
- [ ] `/portal/paquetes` — tabla con estados reales desde API
|
||||
- [ ] `/portal/pre-alertas` — formulario + upload factura
|
||||
- [ ] Modal registrar compra (manual; URL Amazon en Fase 6)
|
||||
- [ ] Cliente API (fetch/axios) + manejo de tokens
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Usuario se registra y recibe Suite única en BD
|
||||
- Crea pre-alerta con PDF/imagen en S3
|
||||
- Registra paquete manual y ve estado `REGISTRADO`
|
||||
- Historial de estados visible y no editable
|
||||
|
||||
---
|
||||
|
||||
## Fase 2 — Portal bodega + API software propio (Semanas 6–9)
|
||||
|
||||
### Objetivo
|
||||
Operadores en NJ gestionan paquetes físicos; tu software de bodega sincroniza por API.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] App `warehouse` (Next.js) o rutas `/warehouse` con rol `Operador Bodega`
|
||||
- [ ] Lista paquetes esperados (pre-alertas + registrados)
|
||||
- [ ] Acción: recibir paquete → `RECIBIDO_BODEGA`
|
||||
- [ ] Verificación: peso real, dimensiones, fotos (múltiples) → `EN_VERIFICACION` / `VERIFICADO`
|
||||
- [ ] Alerta discrepancia peso > 10%
|
||||
- [ ] Asignar paquete a Suite si llegó sin pre-alertar
|
||||
- [ ] **API externa v1** (API Key por tenant):
|
||||
- `POST /api/v1/webhooks/package-status`
|
||||
- `GET /api/v1/packages?status=...`
|
||||
- `PATCH /api/v1/packages/:id/verify`
|
||||
- [ ] Documentación OpenAPI para integración bodega
|
||||
- [ ] Idempotencia en webhooks (`X-Idempotency-Key`)
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Operador escanea/busca paquete y confirma recepción
|
||||
- Fotos visibles en portal cliente tras subirlas
|
||||
- Software externo puede actualizar estado vía API autenticada
|
||||
|
||||
---
|
||||
|
||||
## Fase 3 — Portal administrativo (Semanas 10–12)
|
||||
|
||||
### Objetivo
|
||||
Admin Empresa y Super Admin gestionan operación.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] App `admin` o rutas `/admin`
|
||||
- [ ] Dashboard: paquetes por estado, ingresos, incidencias
|
||||
- [ ] Gestión usuarios y roles (matriz del doc §3.2)
|
||||
- [ ] Configurar tarifas: $/lb por zona Ecuador
|
||||
- [ ] Configurar categorías SENAE (4x4, B, C, D) editables
|
||||
- [ ] Logs de auditoría (solo lectura, filtros)
|
||||
- [ ] Reportes básicos CSV: envíos del mes
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Admin cambia tarifa y afecta calculadora nueva
|
||||
- Toda acción sensible queda en `audit_logs`
|
||||
|
||||
---
|
||||
|
||||
## Fase 4 — Calculadora + pagos (Semanas 13–15)
|
||||
|
||||
### Objetivo
|
||||
Calculadora pública y de portal con reglas reales; cobro anticipado del envío.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] Servicio `calculator`: peso volumétrico, flete, seguro, FODINFA, arancel, IVA
|
||||
- [ ] Endpoint público `POST /api/v1/calculate-shipping` (sin auth)
|
||||
- [ ] Migrar sección calculadora de `index.html` → datos desde API
|
||||
- [ ] Tracking público `GET /api/v1/tracking/:id` (sin auth, datos limitados)
|
||||
- [ ] Integración pasarela: **PayPhone** (prioridad Ecuador) + webhook confirmación
|
||||
- [ ] Vincular pago a `Package` antes de despacho
|
||||
- [ ] Estados post-pago en historial
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Calculadora landing = misma lógica que portal (misma API)
|
||||
- Cliente paga envío y paquete queda marcado como pagado
|
||||
- Tracking público muestra timeline sin login
|
||||
|
||||
---
|
||||
|
||||
## Fase 5 — Módulo aduanas SENAE (Semanas 16–21)
|
||||
|
||||
### Objetivo
|
||||
Declaración simplificada (DSI) y envío al WebService SENAE.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] Modelo `CustomsDeclaration` ligado a `Package`
|
||||
- [ ] Validación umbral $400 / régimen 4x4
|
||||
- [ ] Generación automática DSI (borrador)
|
||||
- [ ] Portal agente aduanero: revisar, aprobar, rechazar
|
||||
- [ ] Cola BullMQ: `submit-to-senae` con reintentos
|
||||
- [ ] Integración SENAE sandbox → producción
|
||||
- [ ] Guardar número autorización SENAE + PDF
|
||||
- [ ] Estado `DECLARACION_ADUANERA` → `EN_TRANSITO_ECUADOR`
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Paquete `VERIFICADO` genera DSI revisable por agente
|
||||
- Envío exitoso a SENAE actualiza estado y notifica cliente
|
||||
- Paquetes sobre umbral generan alerta de importación formal
|
||||
|
||||
---
|
||||
|
||||
## Fase 6 — Notificaciones e integraciones (Semanas 22–24)
|
||||
|
||||
### Objetivo
|
||||
Comunicación automática en cada cambio de estado.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] Worker notificaciones: email (Resend/SES), SMS, WhatsApp Business API
|
||||
- [ ] Plantillas por estado (11 estados del doc)
|
||||
- [ ] Preferencias usuario: canal activo
|
||||
- [ ] Amazon SP-API: parsear URL → pre-llenar registro compra
|
||||
- [ ] Courier APIs (UPS/FedEx): tracking vendedor opcional
|
||||
- [ ] Botón WhatsApp landing (ya en HTML) — números desde config admin
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Cambio de estado dispara al menos email + un canal configurado
|
||||
- Pegar URL Amazon pre-llena formulario de compra
|
||||
|
||||
---
|
||||
|
||||
## Fase 7 — Carga pesada B2B (Semanas 25–26)
|
||||
|
||||
### Objetivo
|
||||
Flujo separado del casillero para importadores mayoristas.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] Modelo `B2BQuote`, `B2BQuoteStatus`
|
||||
- [ ] Formulario cotización en `/carga-pesada/cotizacion`
|
||||
- [ ] Panel admin: revisar solicitudes, responder cotización
|
||||
- [ ] Expediente con tracking B2B propio
|
||||
- [ ] Campos INEN / certificación en formulario
|
||||
- [ ] Notificación manual (email/WhatsApp) al equipo Moraworld
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Cotización B2B no mezcla flujo con paquetes B2C
|
||||
- Admin puede cerrar cotización como aceptada/rechazada
|
||||
|
||||
---
|
||||
|
||||
## Fase 8 — Producción y hardening (Semanas 27–29)
|
||||
|
||||
### Objetivo
|
||||
Sistema listo para usuarios reales con SLA y seguridad.
|
||||
|
||||
### Tareas
|
||||
|
||||
- [ ] Ambientes: `development`, `staging`, `production`
|
||||
- [ ] Deploy: API + web (Railway/Render o AWS ECS + RDS)
|
||||
- [ ] TLS, dominio, Cloudflare WAF
|
||||
- [ ] Backups PostgreSQL automáticos (retención 90 días)
|
||||
- [ ] Rate limiting, helmet, validación OWASP básica
|
||||
- [ ] Políticas: privacidad LOPDP, términos, cookies
|
||||
- [ ] Monitoreo: Sentry + logs estructurados
|
||||
- [ ] Pruebas E2E críticas (Playwright): registro → pre-alerta → bodega recibe
|
||||
- [ ] Runbook operativo para equipo Moraworld
|
||||
|
||||
### Criterios de aceptación
|
||||
|
||||
- Staging accesible con datos de prueba
|
||||
- Producción con backup restaurable probado
|
||||
- Checklist ISO 27001 controles implementados documentado
|
||||
|
||||
---
|
||||
|
||||
## Roles del sistema (implementación por fase)
|
||||
|
||||
| Rol | Fase disponible |
|
||||
|-----|-----------------|
|
||||
| Cliente Final | Fase 1 |
|
||||
| Operador Bodega | Fase 2 |
|
||||
| Soporte | Fase 3 (solo lectura) |
|
||||
| Admin Empresa | Fase 3 |
|
||||
| Agente Aduanero | Fase 5 |
|
||||
| Super Admin | Fase 3 |
|
||||
|
||||
---
|
||||
|
||||
## Estados de paquete (referencia única)
|
||||
|
||||
```
|
||||
REGISTRADO → EN_TRANSITO_BODEGA → RECIBIDO_BODEGA → EN_VERIFICACION
|
||||
→ VERIFICADO → DECLARACION_ADUANERA → EN_TRANSITO_ECUADOR
|
||||
→ EN_ADUANA_ECUADOR → LISTO_ENTREGA → ENTREGADO
|
||||
(INCIDENCIA en cualquier punto)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migración desde prototipos HTML actuales
|
||||
|
||||
| Archivo actual | Destino en producción |
|
||||
|----------------|----------------------|
|
||||
| `index.html` | `apps/web` — `/`, secciones anchor |
|
||||
| `login.html` | `apps/web` — `/login` |
|
||||
| `registro.html` | `apps/web` — `/registro` |
|
||||
| `portal-cliente.html` | `apps/web` — `/portal/*` |
|
||||
| `documentacion.html` | Mantener estático o mover a `/docs` interno |
|
||||
| `presentacion.html` | Mantener para ventas; no producción |
|
||||
| `requerimientos-sistema-casillero.md` | Fuente de verdad funcional |
|
||||
|
||||
Los HTML actuales **no se borran** hasta completar migración de cada sección (Fase 1–4).
|
||||
|
||||
---
|
||||
|
||||
## Riesgos y dependencias externas
|
||||
|
||||
| Riesgo | Mitigación |
|
||||
|--------|------------|
|
||||
| Acceso API SENAE (sandbox/prod) | Iniciar trámite en Fase 0; mock en Fase 5 |
|
||||
| Software bodega sin API aún | Definir contrato OpenAPI en Fase 2; mock |
|
||||
| PayPhone / credenciales pagos | Cuenta merchant en Fase 4 |
|
||||
| WhatsApp Business verificación | Proceso Meta en Fase 6 |
|
||||
| Amazon SP-API aprobación | Fallback scraping/manual Fase 6 |
|
||||
|
||||
---
|
||||
|
||||
## Próximo paso inmediato (esta semana)
|
||||
|
||||
**Iniciar Fase 0** en este orden:
|
||||
|
||||
1. Crear monorepo + Docker + PostgreSQL + Redis
|
||||
2. Prisma schema v0 (Tenant, User, Suite, Package)
|
||||
3. NestJS auth mínimo (registro + login JWT)
|
||||
4. Next.js shell con página de health
|
||||
|
||||
Cuando confirmes, ejecutamos la Fase 0 en el repositorio.
|
||||
|
||||
---
|
||||
|
||||
## Control de progreso
|
||||
|
||||
| Fase | Estado | Fecha inicio | Fecha fin |
|
||||
|:----:|:------:|:------------:|:---------:|
|
||||
| 0 | 🟡 En curso | May 2026 | |
|
||||
| 1 | ⬜ Pendiente | | |
|
||||
| 2 | ⬜ Pendiente | | |
|
||||
| 3 | ⬜ Pendiente | | |
|
||||
| 4 | ⬜ Pendiente | | |
|
||||
| 5 | ⬜ Pendiente | | |
|
||||
| 6 | ⬜ Pendiente | | |
|
||||
| 7 | ⬜ Pendiente | | |
|
||||
| 8 | ⬜ Pendiente | | |
|
||||
|
||||
---
|
||||
|
||||
*Documento vivo — actualizar al cerrar cada fase.*
|
||||
@@ -0,0 +1,27 @@
|
||||
# Coolify / producción — API NestJS
|
||||
FROM node:22-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
|
||||
COPY apps/api/package.json ./apps/api/
|
||||
COPY packages/database/package.json ./packages/database/
|
||||
RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||||
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN pnpm --filter @moraworld/database generate
|
||||
RUN pnpm --filter @moraworld/database build
|
||||
RUN pnpm --filter @moraworld/api build
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/apps/api/dist ./dist
|
||||
COPY --from=builder /app/apps/api/package.json ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/packages/database/node_modules/.prisma ./node_modules/.prisma
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/main.js"]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@moraworld/api",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "nest start --watch",
|
||||
"start": "node dist/main",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@moraworld/database": "workspace:*",
|
||||
"@nestjs/common": "^11.1.0",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.0",
|
||||
"@nestjs/platform-express": "^11.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.7",
|
||||
"@nestjs/schematics": "^11.0.5",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/node": "^22.15.21",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { HealthModule } from "./health/health.module";
|
||||
import { PrismaModule } from "./prisma/prisma.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: ["../../.env", ".env"],
|
||||
}),
|
||||
PrismaModule,
|
||||
HealthModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
|
||||
@Controller()
|
||||
export class HealthController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get("health")
|
||||
async health() {
|
||||
let database: "ok" | "error" = "ok";
|
||||
try {
|
||||
await this.prisma.client.$queryRaw`SELECT 1`;
|
||||
} catch {
|
||||
database = "error";
|
||||
}
|
||||
|
||||
return {
|
||||
status: database === "ok" ? "ok" : "degraded",
|
||||
service: "moraworld-api",
|
||||
version: "0.1.0",
|
||||
timestamp: new Date().toISOString(),
|
||||
checks: {
|
||||
database,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { HealthController } from "./health.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class HealthModule {}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const corsOrigins = (process.env.CORS_ORIGINS ?? "http://localhost:3000")
|
||||
.split(",")
|
||||
.map((o) => o.trim());
|
||||
|
||||
app.enableCors({
|
||||
origin: corsOrigins,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
app.setGlobalPrefix("api");
|
||||
|
||||
const port = process.env.API_PORT ?? process.env.PORT ?? 3001;
|
||||
await app.listen(port, "0.0.0.0");
|
||||
|
||||
console.log(`Moraworld API → http://localhost:${port}/api`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { PrismaService } from "./prisma.service";
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { prisma, PrismaClient } from "@moraworld/database";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService implements OnModuleInit, OnModuleDestroy {
|
||||
readonly client: PrismaClient = prisma;
|
||||
|
||||
async onModuleInit() {
|
||||
await prisma.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2022",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Coolify / producción — Next.js standalone
|
||||
FROM node:22-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
|
||||
COPY apps/web/package.json ./apps/web/
|
||||
RUN pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||||
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
ARG NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||
RUN pnpm --filter @moraworld/web build
|
||||
|
||||
FROM node:22-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder /app/apps/web/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
CMD ["node", "apps/web/server.js"]
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@moraworld/web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"start": "next start --port 3000",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^15.3.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
:root {
|
||||
--primary: #0057ff;
|
||||
--primary-dark: #003fc7;
|
||||
--accent: #ff6b00;
|
||||
--dark: #0d1117;
|
||||
--gray-900: #111827;
|
||||
--gray-500: #6b7280;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-100: #f3f4f6;
|
||||
--white: #ffffff;
|
||||
--green: #10b981;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-inter), system-ui, sans-serif;
|
||||
color: var(--gray-900);
|
||||
background: var(--white);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Moraworld Imports — Casillero en EE.UU. para Ecuador",
|
||||
description:
|
||||
"Compra en Amazon, eBay y más. Tu casillero en New Jersey con envío y gestión aduanera a Ecuador.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="es">
|
||||
<body className={inter.variable}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const health = await getApiHealth();
|
||||
|
||||
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>
|
||||
|
||||
<section
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #0D1117 0%, #0D2150 100%)",
|
||||
padding: "80px 24px",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
|
||||
<p
|
||||
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,
|
||||
}}
|
||||
>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
async function getHealth() {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
|
||||
const res = await fetch(`${apiUrl}/api/health`, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error("API no disponible");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default async function StatusPage() {
|
||||
let health: Record<string, unknown> | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
health = await getHealth();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error desconocido";
|
||||
}
|
||||
|
||||
return (
|
||||
<main style={{ padding: 48, maxWidth: 720, margin: "0 auto", fontFamily: "var(--font-inter), sans-serif" }}>
|
||||
<a href="/" style={{ color: "var(--primary)", fontWeight: 600, fontSize: ".9rem" }}>
|
||||
← Volver
|
||||
</a>
|
||||
<h1 style={{ fontSize: "1.75rem", fontWeight: 800, margin: "24px 0 16px" }}>Estado del sistema</h1>
|
||||
|
||||
{error ? (
|
||||
<pre
|
||||
style={{
|
||||
background: "#FEE2E2",
|
||||
color: "#991B1B",
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
{"\n\n"}
|
||||
Asegúrate de tener corriendo:{"\n"}
|
||||
1. pnpm docker:up{"\n"}
|
||||
2. pnpm dev (api + web)
|
||||
</pre>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
background: "var(--gray-100)",
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
overflow: "auto",
|
||||
fontSize: ".85rem",
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(health, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# OPCIONAL — solo si NO tienes Postgres/Redis en Coolify.
|
||||
# Si ya usas BD/Redis en Coolify, configura DATABASE_URL y REDIS_URL en .env y no levantes esto.
|
||||
#
|
||||
# Uso: pnpm docker:up
|
||||
# Apps api/web se despliegan en Coolify con docs/COOLIFY.md
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: moraworld-postgres
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: moraworld
|
||||
POSTGRES_PASSWORD: moraworld_dev
|
||||
POSTGRES_DB: moraworld
|
||||
volumes:
|
||||
- moraworld_pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U moraworld -d moraworld"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: moraworld-redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- moraworld_redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# Almacenamiento compatible S3 (facturas/fotos — Fase 1+)
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: moraworld-minio
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioadmin
|
||||
MINIO_ROOT_PASSWORD: minioadmin
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- moraworld_minio_data:/data
|
||||
profiles:
|
||||
- storage
|
||||
|
||||
volumes:
|
||||
moraworld_pg_data:
|
||||
moraworld_redis_data:
|
||||
moraworld_minio_data:
|
||||
@@ -0,0 +1,80 @@
|
||||
# Despliegue en Coolify
|
||||
|
||||
## Arquitectura recomendada
|
||||
|
||||
| Servicio Coolify | Repo / Dockerfile | Puerto |
|
||||
|------------------|-------------------|--------|
|
||||
| **moraworld-web** | `apps/web/Dockerfile` (build context: raíz del monorepo) | 3000 |
|
||||
| **moraworld-api** | `apps/api/Dockerfile` (build context: raíz del monorepo) | 3001 |
|
||||
| **PostgreSQL** | Servicio Coolify o externo | 5432 |
|
||||
| **Redis** | Servicio Coolify o externo | 6379 |
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
### API (`moraworld-api`)
|
||||
|
||||
```
|
||||
DATABASE_URL=postgresql://USER:PASS@HOST:5432/moraworld
|
||||
REDIS_URL=redis://HOST:6379
|
||||
API_PORT=3001
|
||||
JWT_SECRET=<generar-32-chars-minimo>
|
||||
CORS_ORIGINS=https://tu-dominio.com
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
### Web (`moraworld-web`)
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_API_URL=https://api.tu-dominio.com
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
> En build de Coolify para **web**, pasar `NEXT_PUBLIC_API_URL` como **build argument** si usas Dockerfile (ya declarado en `apps/web/Dockerfile`).
|
||||
|
||||
## Build en Coolify
|
||||
|
||||
1. Conectar repositorio Git.
|
||||
2. Crear **dos aplicaciones** desde el mismo repo:
|
||||
|
||||
### Aplicación API
|
||||
|
||||
- **Build Pack:** Dockerfile
|
||||
- **Dockerfile location:** `apps/api/Dockerfile`
|
||||
- **Base directory / Context:** `/` (raíz del monorepo)
|
||||
- **Health check path:** `/api/health`
|
||||
- **Puerto expuesto:** 3001
|
||||
|
||||
### Aplicación Web
|
||||
|
||||
- **Build Pack:** Dockerfile
|
||||
- **Dockerfile location:** `apps/web/Dockerfile`
|
||||
- **Base directory / Context:** `/`
|
||||
- **Health check path:** `/`
|
||||
- **Puerto expuesto:** 3000
|
||||
|
||||
## Migraciones en producción
|
||||
|
||||
Ejecutar una vez (o en CI) antes/después del deploy de API:
|
||||
|
||||
```bash
|
||||
pnpm db:generate
|
||||
pnpm exec prisma migrate deploy --schema=packages/database/prisma/schema.prisma
|
||||
```
|
||||
|
||||
En Coolify puedes usar un **one-off command** o job con imagen Node y `DATABASE_URL` del servicio Postgres.
|
||||
|
||||
## Desarrollo local vs Coolify
|
||||
|
||||
| | Local | Coolify |
|
||||
|---|--------|---------|
|
||||
| Postgres/Redis | `docker compose up` | Servicios gestionados |
|
||||
| API | `pnpm --filter @moraworld/api dev` | Contenedor NestJS |
|
||||
| Web | `pnpm --filter @moraworld/web dev` | Contenedor Next standalone |
|
||||
| Dominio | localhost | HTTPS + dominio real |
|
||||
|
||||
## Notas
|
||||
|
||||
- No subir `.env` al repositorio.
|
||||
- Ajustar `CORS_ORIGINS` con el dominio exacto del frontend.
|
||||
- Para archivos (facturas/fotos), configurar S3 o MinIO en Fase 1 (`S3_*` en `.env.example`).
|
||||
@@ -0,0 +1,120 @@
|
||||
# Variables desde Coolify (Postgres + Redis externos)
|
||||
|
||||
Si Postgres y Redis ya están en Coolify, **no necesitas** `docker compose` para la BD. Solo pega las URLs en `.env` (local) y en cada app de Coolify (producción).
|
||||
|
||||
## Qué necesitamos que nos pases
|
||||
|
||||
Copia desde el panel de Coolify (o pega aquí en el chat) estos valores:
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
| Variable | Ejemplo | Dónde está en Coolify |
|
||||
|----------|---------|------------------------|
|
||||
| Host | `postgres.tudominio.com` o IP interna | Servicio Postgres → Connection |
|
||||
| Puerto | `5432` | |
|
||||
| Usuario | `moraworld` | |
|
||||
| Contraseña | `***` | |
|
||||
| Base de datos | `moraworld` | Crear DB si no existe |
|
||||
|
||||
**Formato final en `.env`:**
|
||||
|
||||
```env
|
||||
DATABASE_URL="postgresql://USUARIO:CONTRASEÑA@HOST:5432/NOMBRE_BD?schema=moraworld"
|
||||
```
|
||||
|
||||
> En PostgreSQL 15+ el schema `public` a menudo no permite `CREATE` al usuario de la app. Usamos el schema **`moraworld`** (se crea automáticamente en el primer deploy si el usuario tiene permiso `CREATE SCHEMA`).
|
||||
|
||||
> Si la contraseña tiene caracteres especiales (`@`, `#`, `%`), hay que codificarlos en URL (encodeURIComponent).
|
||||
|
||||
### Redis
|
||||
|
||||
| Variable | Ejemplo |
|
||||
|----------|---------|
|
||||
| Host | `redis.tudominio.com` |
|
||||
| Puerto | `6379` (o `6380` con TLS) |
|
||||
| Contraseña | si aplica |
|
||||
|
||||
**Sin contraseña:**
|
||||
|
||||
```env
|
||||
REDIS_URL="redis://HOST:6379"
|
||||
```
|
||||
|
||||
**Con contraseña:**
|
||||
|
||||
```env
|
||||
REDIS_URL="redis://:CONTRASEÑA@HOST:6379"
|
||||
```
|
||||
|
||||
**Con TLS (rediss):**
|
||||
|
||||
```env
|
||||
REDIS_URL="rediss://:CONTRASEÑA@HOST:6380"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dónde configurarlas
|
||||
|
||||
### Desarrollo en tu Mac (`.env` en la raíz)
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Editar DATABASE_URL y REDIS_URL con los valores de Coolify
|
||||
```
|
||||
|
||||
Luego:
|
||||
|
||||
```bash
|
||||
pnpm db:generate
|
||||
npx pnpm exec prisma migrate deploy --schema=packages/database/prisma/schema.prisma
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
> Tu Mac debe poder **alcanzar** el host de Postgres/Redis (IP pública, túnel, o VPN). Si Coolify solo expone red interna, usa túnel SSH o el dominio público que Coolify asigne.
|
||||
|
||||
### Coolify — app API
|
||||
|
||||
```
|
||||
DATABASE_URL=postgresql://...
|
||||
REDIS_URL=redis://...
|
||||
API_PORT=3001
|
||||
JWT_SECRET=...
|
||||
CORS_ORIGINS=https://tu-web.com
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
### Coolify — app Web
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_API_URL=https://api.tu-dominio.com
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migraciones contra Postgres en Coolify
|
||||
|
||||
Una vez tengas `DATABASE_URL` en `.env`:
|
||||
|
||||
```bash
|
||||
pnpm db:generate
|
||||
npx pnpm exec prisma migrate deploy --schema=packages/database/prisma/schema.prisma
|
||||
```
|
||||
|
||||
Esto crea las tablas en tu Postgres remoto (migración `20260520120000_init`).
|
||||
|
||||
---
|
||||
|
||||
## Redis en Fase 0
|
||||
|
||||
La API aún **no usa** Redis (colas BullMQ en Fase 6). Puedes dejar `REDIS_URL` configurado desde ya para no cambiar env después.
|
||||
|
||||
---
|
||||
|
||||
## Seguridad
|
||||
|
||||
- No subir `.env` a Git.
|
||||
- En Coolify usar "Secrets" del panel.
|
||||
- Rotar contraseñas si se compartieron en chat; luego actualizar `.env` y Coolify.
|
||||
+1156
File diff suppressed because it is too large
Load Diff
+1391
File diff suppressed because it is too large
Load Diff
+1391
File diff suppressed because it is too large
Load Diff
+294
@@ -0,0 +1,294 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Iniciar sesión — Moraworld Imports</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--primary:#0057FF;--primary-d:#003FC7;--accent:#FF6B00;
|
||||
--dark:#0D1117;--gray-900:#111827;--gray-700:#374151;
|
||||
--gray-500:#6B7280;--gray-200:#E5E7EB;--gray-100:#F3F4F6;
|
||||
--white:#fff;--green:#10B981;--red:#EF4444;
|
||||
}
|
||||
body{font-family:'Inter',sans-serif;background:var(--gray-100);min-height:100vh;display:flex;flex-direction:column}
|
||||
a{text-decoration:none;color:inherit}
|
||||
|
||||
/* NAV */
|
||||
nav{background:rgba(255,255,255,.95);backdrop-filter:blur(12px);border-bottom:1px solid var(--gray-200);padding:0 24px;height:64px;display:flex;align-items:center;justify-content:space-between}
|
||||
.nav-logo{font-size:1.2rem;font-weight:800;color:var(--primary)}
|
||||
.nav-logo span{color:var(--accent)}
|
||||
.nav-back{font-size:.85rem;color:var(--gray-500);display:flex;align-items:center;gap:6px;transition:color .2s}
|
||||
.nav-back:hover{color:var(--primary)}
|
||||
|
||||
/* LAYOUT */
|
||||
.auth-wrapper{flex:1;display:grid;grid-template-columns:1fr 1fr;min-height:calc(100vh - 64px)}
|
||||
|
||||
/* LEFT PANEL */
|
||||
.auth-left{
|
||||
background:linear-gradient(135deg,#0D1117 0%,#0D2150 100%);
|
||||
display:flex;flex-direction:column;justify-content:center;align-items:flex-start;
|
||||
padding:60px 56px;position:relative;overflow:hidden;
|
||||
}
|
||||
.auth-left::before{
|
||||
content:'';position:absolute;inset:0;
|
||||
background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none'%3E%3Cg fill='%230057FF' fill-opacity='0.06'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
.auth-left-content{position:relative;z-index:1;max-width:420px}
|
||||
.al-badge{display:inline-flex;align-items:center;gap:8px;background:rgba(0,87,255,.15);border:1px solid rgba(0,87,255,.3);color:#7AADFF;font-size:.8rem;font-weight:600;padding:6px 14px;border-radius:999px;margin-bottom:32px}
|
||||
.al-badge span{width:8px;height:8px;border-radius:50%;background:#7AADFF;animation:pulse 2s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
.auth-left h2{font-size:2.2rem;font-weight:800;color:#fff;line-height:1.2;margin-bottom:16px}
|
||||
.auth-left h2 em{color:var(--accent);font-style:normal}
|
||||
.auth-left p{color:#9CA3AF;font-size:1rem;line-height:1.7;margin-bottom:40px}
|
||||
|
||||
.feature-list{display:flex;flex-direction:column;gap:16px}
|
||||
.feat-item{display:flex;align-items:flex-start;gap:14px}
|
||||
.feat-icon{width:40px;height:40px;border-radius:10px;background:rgba(0,87,255,.2);display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0}
|
||||
.feat-text strong{display:block;color:#fff;font-size:.9rem;margin-bottom:2px}
|
||||
.feat-text span{color:#6B7280;font-size:.82rem}
|
||||
|
||||
/* RIGHT PANEL */
|
||||
.auth-right{display:flex;align-items:center;justify-content:center;padding:40px 24px;background:var(--white)}
|
||||
.auth-box{width:100%;max-width:420px}
|
||||
.auth-box h1{font-size:1.8rem;font-weight:800;color:var(--gray-900);margin-bottom:8px}
|
||||
.auth-box .subtitle{color:var(--gray-500);font-size:.93rem;margin-bottom:36px}
|
||||
.auth-box .subtitle a{color:var(--primary);font-weight:600}
|
||||
|
||||
.form-group{margin-bottom:20px}
|
||||
.form-group label{display:block;font-size:.85rem;font-weight:600;color:var(--gray-700);margin-bottom:6px}
|
||||
.form-group input{
|
||||
width:100%;padding:13px 16px;border:2px solid var(--gray-200);
|
||||
border-radius:8px;font-size:.95rem;font-family:inherit;
|
||||
transition:border-color .2s;outline:none;background:var(--white);
|
||||
}
|
||||
.form-group input:focus{border-color:var(--primary)}
|
||||
.form-group input.error{border-color:var(--red)}
|
||||
.input-wrap{position:relative}
|
||||
.input-wrap input{padding-right:44px}
|
||||
.toggle-pass{position:absolute;right:14px;top:50%;transform:translateY(-50%);cursor:pointer;font-size:1rem;color:var(--gray-500);background:none;border:none;padding:0}
|
||||
|
||||
.form-options{display:flex;justify-content:space-between;align-items:center;margin-bottom:28px}
|
||||
.remember{display:flex;align-items:center;gap:8px;font-size:.85rem;color:var(--gray-700);cursor:pointer}
|
||||
.remember input[type=checkbox]{accent-color:var(--primary);width:16px;height:16px}
|
||||
.forgot-link{font-size:.85rem;color:var(--primary);font-weight:600}
|
||||
.forgot-link:hover{text-decoration:underline}
|
||||
|
||||
.btn-submit{
|
||||
width:100%;padding:14px;background:var(--primary);color:#fff;
|
||||
border:none;border-radius:8px;font-size:1rem;font-weight:700;
|
||||
cursor:pointer;font-family:inherit;transition:all .2s;
|
||||
}
|
||||
.btn-submit:hover{background:var(--primary-d);transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,87,255,.3)}
|
||||
|
||||
.divider{display:flex;align-items:center;gap:12px;margin:24px 0;color:var(--gray-500);font-size:.82rem}
|
||||
.divider::before,.divider::after{content:'';flex:1;height:1px;background:var(--gray-200)}
|
||||
|
||||
.mfa-notice{
|
||||
background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;
|
||||
padding:12px 16px;display:flex;gap:10px;align-items:flex-start;
|
||||
font-size:.82rem;color:#1D4ED8;margin-bottom:24px;
|
||||
}
|
||||
.mfa-notice strong{display:block;margin-bottom:2px}
|
||||
.mfa-notice span{opacity:.75}
|
||||
|
||||
/* MFA step */
|
||||
.mfa-step{display:none}
|
||||
.mfa-step.show{display:block}
|
||||
.login-step{display:block}
|
||||
.login-step.hide{display:none}
|
||||
|
||||
.otp-wrap{display:flex;gap:10px;margin-bottom:24px}
|
||||
.otp-wrap input{
|
||||
flex:1;text-align:center;font-size:1.6rem;font-weight:700;
|
||||
padding:14px 4px;border:2px solid var(--gray-200);border-radius:8px;
|
||||
font-family:inherit;outline:none;transition:border-color .2s;
|
||||
}
|
||||
.otp-wrap input:focus{border-color:var(--primary)}
|
||||
|
||||
.back-link{display:inline-flex;align-items:center;gap:6px;color:var(--gray-500);font-size:.85rem;cursor:pointer;margin-bottom:24px;background:none;border:none;font-family:inherit}
|
||||
.back-link:hover{color:var(--primary)}
|
||||
|
||||
/* ALERT */
|
||||
.alert{padding:12px 16px;border-radius:8px;font-size:.85rem;margin-bottom:20px;display:none}
|
||||
.alert.show{display:flex;gap:10px;align-items:center}
|
||||
.alert-error{background:#FEE2E2;color:#991B1B;border:1px solid #FCA5A5}
|
||||
|
||||
@media(max-width:768px){
|
||||
.auth-wrapper{grid-template-columns:1fr}
|
||||
.auth-left{display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<a href="index.html" class="nav-logo">Moraworld<span>.</span>Imports</a>
|
||||
<a href="index.html" class="nav-back">← Volver al inicio</a>
|
||||
</nav>
|
||||
|
||||
<div class="auth-wrapper">
|
||||
|
||||
<!-- ── LEFT ── -->
|
||||
<div class="auth-left">
|
||||
<div class="auth-left-content">
|
||||
<div class="al-badge">
|
||||
<span></span> Sistema de Casillero Internacional
|
||||
</div>
|
||||
<h2>Tu puerta al<br/><em>mercado americano</em></h2>
|
||||
<p>Gestiona tus compras en EE.UU., haz seguimiento en tiempo real y recibe en Ecuador con declaración SENAE incluida.</p>
|
||||
<div class="feature-list">
|
||||
<div class="feat-item">
|
||||
<div class="feat-icon">📦</div>
|
||||
<div class="feat-text">
|
||||
<strong>Suite personalizada en NJ</strong>
|
||||
<span>150 N Day St, City of Orange, NJ 07050</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feat-item">
|
||||
<div class="feat-icon">🔍</div>
|
||||
<div class="feat-text">
|
||||
<strong>Tracking en tiempo real</strong>
|
||||
<span>11 estados de seguimiento con notificaciones automáticas</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feat-item">
|
||||
<div class="feat-icon">🛃</div>
|
||||
<div class="feat-text">
|
||||
<strong>Aduana incluida</strong>
|
||||
<span>Declaración simplificada SENAE gestionada automáticamente</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feat-item">
|
||||
<div class="feat-icon">🔒</div>
|
||||
<div class="feat-text">
|
||||
<strong>Seguridad ISO/IEC 27001</strong>
|
||||
<span>MFA, cifrado AES-256 y protección de datos LOPDP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── RIGHT ── -->
|
||||
<div class="auth-right">
|
||||
<div class="auth-box">
|
||||
|
||||
<!-- ALERT -->
|
||||
<div class="alert alert-error" id="alert-error">
|
||||
<span>⚠️</span>
|
||||
<span id="alert-msg">Correo o contraseña incorrectos.</span>
|
||||
</div>
|
||||
|
||||
<!-- STEP 1: LOGIN -->
|
||||
<div class="login-step" id="step-login">
|
||||
<h1>Bienvenido de vuelta</h1>
|
||||
<p class="subtitle">¿No tienes cuenta? <a href="registro.html">Crea la tuya gratis →</a></p>
|
||||
|
||||
<div class="mfa-notice">
|
||||
<span>🔐</span>
|
||||
<div>
|
||||
<strong>Autenticación segura</strong>
|
||||
<span>Este portal utiliza verificación en dos pasos (MFA) para proteger tu cuenta.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Correo electrónico</label>
|
||||
<input type="email" id="email" placeholder="correo@ejemplo.com" autocomplete="email"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Contraseña</label>
|
||||
<div class="input-wrap">
|
||||
<input type="password" id="password" placeholder="Tu contraseña" autocomplete="current-password"/>
|
||||
<button class="toggle-pass" onclick="togglePassword('password', this)" type="button">👁️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-options">
|
||||
<label class="remember">
|
||||
<input type="checkbox" id="remember"/> Recordarme
|
||||
</label>
|
||||
<a href="#" class="forgot-link">¿Olvidaste tu contraseña?</a>
|
||||
</div>
|
||||
<button class="btn-submit" onclick="handleLogin()">Iniciar sesión →</button>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2: MFA -->
|
||||
<div class="mfa-step" id="step-mfa">
|
||||
<button class="back-link" onclick="backToLogin()">← Volver</button>
|
||||
<h1>Verificación MFA</h1>
|
||||
<p class="subtitle" style="margin-bottom:28px;">Ingresa el código de 6 dígitos de tu aplicación de autenticación (Google Authenticator, Authy, etc.)</p>
|
||||
<div class="otp-wrap">
|
||||
<input type="text" maxlength="1" class="otp-digit" oninput="otpMove(this, 0)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" oninput="otpMove(this, 1)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" oninput="otpMove(this, 2)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" oninput="otpMove(this, 3)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" oninput="otpMove(this, 4)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" oninput="otpMove(this, 5)"/>
|
||||
</div>
|
||||
<button class="btn-submit" onclick="verifyMFA()">Verificar código →</button>
|
||||
<p style="text-align:center;margin-top:16px;font-size:.82rem;color:var(--gray-500);">¿No tienes acceso? <a href="#" style="color:var(--primary);font-weight:600;">Recuperar cuenta</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword(id, btn){
|
||||
const inp = document.getElementById(id);
|
||||
if(inp.type === 'password'){inp.type='text'; btn.textContent='🙈';}
|
||||
else{inp.type='password'; btn.textContent='👁️';}
|
||||
}
|
||||
|
||||
function handleLogin(){
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const pass = document.getElementById('password').value;
|
||||
if(!email || !pass){
|
||||
showAlert('Por favor ingresa tu correo y contraseña.');
|
||||
return;
|
||||
}
|
||||
hideAlert();
|
||||
// Demo: mostrar paso MFA
|
||||
document.getElementById('step-login').classList.add('hide');
|
||||
document.getElementById('step-mfa').classList.add('show');
|
||||
document.querySelector('.otp-digit').focus();
|
||||
}
|
||||
|
||||
function backToLogin(){
|
||||
document.getElementById('step-login').classList.remove('hide');
|
||||
document.getElementById('step-mfa').classList.remove('show');
|
||||
hideAlert();
|
||||
}
|
||||
|
||||
function otpMove(inp, idx){
|
||||
const digits = document.querySelectorAll('.otp-digit');
|
||||
inp.value = inp.value.replace(/\D/g,'');
|
||||
if(inp.value && idx < 5) digits[idx+1].focus();
|
||||
// Auto-verify when all filled
|
||||
const code = Array.from(digits).map(d=>d.value).join('');
|
||||
if(code.length === 6) verifyMFA();
|
||||
}
|
||||
|
||||
function verifyMFA(){
|
||||
const digits = document.querySelectorAll('.otp-digit');
|
||||
const code = Array.from(digits).map(d=>d.value).join('');
|
||||
if(code.length < 6){ showAlert('Ingresa los 6 dígitos del código.'); return; }
|
||||
hideAlert();
|
||||
// Demo: redirigir al portal del cliente
|
||||
window.location.href = 'portal-cliente.html';
|
||||
}
|
||||
|
||||
function showAlert(msg){
|
||||
const el = document.getElementById('alert-error');
|
||||
document.getElementById('alert-msg').textContent = msg;
|
||||
el.classList.add('show');
|
||||
}
|
||||
function hideAlert(){
|
||||
document.getElementById('alert-error').classList.remove('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "moraworld",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Moraworld Imports — Casillero internacional EE.UU. → Ecuador",
|
||||
"scripts": {
|
||||
"dev": "turbo run dev",
|
||||
"build": "turbo run build",
|
||||
"lint": "turbo run lint",
|
||||
"setup": "npm install -g pnpm@9.15.0 && pnpm install",
|
||||
"db:generate": "pnpm --filter @moraworld/database generate",
|
||||
"db:migrate": "pnpm --filter @moraworld/database migrate:dev",
|
||||
"db:push": "pnpm --filter @moraworld/database push",
|
||||
"db:studio": "pnpm --filter @moraworld/database studio",
|
||||
"docker:up": "docker compose up -d",
|
||||
"docker:down": "docker compose down",
|
||||
"docker:logs": "docker compose logs -f"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@moraworld/database",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"generate": "prisma generate",
|
||||
"migrate:dev": "prisma migrate dev",
|
||||
"push": "prisma db push",
|
||||
"studio": "prisma studio",
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.21",
|
||||
"prisma": "^6.8.2",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "UserRole" AS ENUM ('SUPER_ADMIN', 'ADMIN_EMPRESA', 'OPERADOR_BODEGA', 'AGENTE_ADUANERO', 'CLIENTE', 'SOPORTE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PackageStatus" AS ENUM ('REGISTRADO', 'EN_TRANSITO_BODEGA', 'RECIBIDO_BODEGA', 'EN_VERIFICACION', 'VERIFICADO', 'DECLARACION_ADUANERA', 'EN_TRANSITO_ECUADOR', 'EN_ADUANA_ECUADOR', 'LISTO_ENTREGA', 'ENTREGADO', 'INCIDENCIA');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PreAlertStatus" AS ENUM ('PENDIENTE', 'VINCULADA', 'CANCELADA');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tenant" (
|
||||
"id" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"firstName" TEXT NOT NULL,
|
||||
"lastName" TEXT NOT NULL,
|
||||
"phone" TEXT,
|
||||
"role" "UserRole" NOT NULL DEFAULT 'CLIENTE',
|
||||
"mfaEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"mfaSecret" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"lastLoginAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Suite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Suite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Package" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"trackingId" TEXT NOT NULL,
|
||||
"status" "PackageStatus" NOT NULL DEFAULT 'REGISTRADO',
|
||||
"description" TEXT NOT NULL,
|
||||
"store" TEXT,
|
||||
"declaredValue" DECIMAL(10,2) NOT NULL,
|
||||
"declaredWeight" DECIMAL(8,2),
|
||||
"actualWeight" DECIMAL(8,2),
|
||||
"vendorTracking" TEXT,
|
||||
"productUrl" TEXT,
|
||||
"paidAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Package_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PackageStatusHistory" (
|
||||
"id" TEXT NOT NULL,
|
||||
"packageId" TEXT NOT NULL,
|
||||
"status" "PackageStatus" NOT NULL,
|
||||
"note" TEXT,
|
||||
"createdBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PackageStatusHistory_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PreAlert" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"packageId" TEXT,
|
||||
"store" TEXT NOT NULL,
|
||||
"vendorTracking" TEXT,
|
||||
"description" TEXT NOT NULL,
|
||||
"declaredValue" DECIMAL(10,2) NOT NULL,
|
||||
"invoiceKey" TEXT,
|
||||
"status" "PreAlertStatus" NOT NULL DEFAULT 'PENDIENTE',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PreAlert_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuditLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT,
|
||||
"userId" TEXT,
|
||||
"action" TEXT NOT NULL,
|
||||
"resource" TEXT,
|
||||
"resourceId" TEXT,
|
||||
"metadata" JSONB,
|
||||
"ipAddress" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Tenant_slug_key" ON "Tenant"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_tenantId_idx" ON "User"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_tenantId_email_key" ON "User"("tenantId", "email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Suite_userId_key" ON "Suite"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Suite_tenantId_idx" ON "Suite"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Suite_tenantId_code_key" ON "Suite"("tenantId", "code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Package_trackingId_key" ON "Package"("trackingId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Package_tenantId_status_idx" ON "Package"("tenantId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Package_userId_idx" ON "Package"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PackageStatusHistory_packageId_idx" ON "PackageStatusHistory"("packageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PreAlert_packageId_key" ON "PreAlert"("packageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PreAlert_tenantId_idx" ON "PreAlert"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditLog_tenantId_createdAt_idx" ON "AuditLog"("tenantId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuditLog_userId_idx" ON "AuditLog"("userId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Suite" ADD CONSTRAINT "Suite_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Suite" ADD CONSTRAINT "Suite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Package" ADD CONSTRAINT "Package_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Package" ADD CONSTRAINT "Package_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PackageStatusHistory" ADD CONSTRAINT "PackageStatusHistory_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "Package"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PreAlert" ADD CONSTRAINT "PreAlert_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PreAlert" ADD CONSTRAINT "PreAlert_packageId_fkey" FOREIGN KEY ("packageId") REFERENCES "Package"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,179 @@
|
||||
// Moraworld Imports — Schema v0 (Fase 0–1)
|
||||
// Multi-tenant por tenant_id en todas las tablas de negocio
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ─── Enums ───────────────────────────────────────────────────
|
||||
|
||||
enum UserRole {
|
||||
SUPER_ADMIN
|
||||
ADMIN_EMPRESA
|
||||
OPERADOR_BODEGA
|
||||
AGENTE_ADUANERO
|
||||
CLIENTE
|
||||
SOPORTE
|
||||
}
|
||||
|
||||
enum PackageStatus {
|
||||
REGISTRADO
|
||||
EN_TRANSITO_BODEGA
|
||||
RECIBIDO_BODEGA
|
||||
EN_VERIFICACION
|
||||
VERIFICADO
|
||||
DECLARACION_ADUANERA
|
||||
EN_TRANSITO_ECUADOR
|
||||
EN_ADUANA_ECUADOR
|
||||
LISTO_ENTREGA
|
||||
ENTREGADO
|
||||
INCIDENCIA
|
||||
}
|
||||
|
||||
enum PreAlertStatus {
|
||||
PENDIENTE
|
||||
VINCULADA
|
||||
CANCELADA
|
||||
}
|
||||
|
||||
// ─── Tenant (multi-tenant) ───────────────────────────────────
|
||||
|
||||
model Tenant {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
name String
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
users User[]
|
||||
suites Suite[]
|
||||
packages Package[]
|
||||
preAlerts PreAlert[]
|
||||
}
|
||||
|
||||
// ─── Usuarios ────────────────────────────────────────────────
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
tenantId String
|
||||
email String
|
||||
passwordHash String
|
||||
firstName String
|
||||
lastName String
|
||||
phone String?
|
||||
role UserRole @default(CLIENTE)
|
||||
mfaEnabled Boolean @default(false)
|
||||
mfaSecret String?
|
||||
isActive Boolean @default(true)
|
||||
lastLoginAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
suite Suite?
|
||||
packages Package[]
|
||||
|
||||
@@unique([tenantId, email])
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
// ─── Casillero / Suite ───────────────────────────────────────
|
||||
|
||||
model Suite {
|
||||
id String @id @default(cuid())
|
||||
tenantId String
|
||||
userId String @unique
|
||||
code String // ej: EC-00345
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([tenantId, code])
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
// ─── Paquetes ────────────────────────────────────────────────
|
||||
|
||||
model Package {
|
||||
id String @id @default(cuid())
|
||||
tenantId String
|
||||
userId String
|
||||
trackingId String @unique // EC-YYYYMMDD-XXXXXX
|
||||
status PackageStatus @default(REGISTRADO)
|
||||
description String
|
||||
store String?
|
||||
declaredValue Decimal @db.Decimal(10, 2)
|
||||
declaredWeight Decimal? @db.Decimal(8, 2)
|
||||
actualWeight Decimal? @db.Decimal(8, 2)
|
||||
vendorTracking String?
|
||||
productUrl String?
|
||||
paidAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
statusHistory PackageStatusHistory[]
|
||||
preAlert PreAlert?
|
||||
|
||||
@@index([tenantId, status])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model PackageStatusHistory {
|
||||
id String @id @default(cuid())
|
||||
packageId String
|
||||
status PackageStatus
|
||||
note String?
|
||||
createdBy String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
package Package @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([packageId])
|
||||
}
|
||||
|
||||
// ─── Pre-alertas ─────────────────────────────────────────────
|
||||
|
||||
model PreAlert {
|
||||
id String @id @default(cuid())
|
||||
tenantId String
|
||||
packageId String? @unique
|
||||
store String
|
||||
vendorTracking String?
|
||||
description String
|
||||
declaredValue Decimal @db.Decimal(10, 2)
|
||||
invoiceKey String?
|
||||
status PreAlertStatus @default(PENDIENTE)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
package Package? @relation(fields: [packageId], references: [id])
|
||||
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
// ─── Auditoría (append-only) ───────────────────────────────
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
tenantId String?
|
||||
userId String?
|
||||
action String
|
||||
resource String?
|
||||
resourceId String?
|
||||
metadata Json?
|
||||
ipAddress String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([tenantId, createdAt])
|
||||
@@index([userId])
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PrismaClient, UserRole } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const tenant = await prisma.tenant.upsert({
|
||||
where: { slug: "moraworld" },
|
||||
update: {},
|
||||
create: {
|
||||
slug: "moraworld",
|
||||
name: "Moraworld Imports",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✓ Tenant:", tenant.slug);
|
||||
|
||||
const suiteCode = "EC-00001";
|
||||
const demoEmail = "demo@moraworld.test";
|
||||
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { tenantId_email: { tenantId: tenant.id, email: demoEmail } },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
console.log("ℹ Usuario demo se creará en Fase 1 (auth con bcrypt)");
|
||||
}
|
||||
|
||||
console.log("✓ Seed completado");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,16 @@
|
||||
export { PrismaClient } from "@prisma/client";
|
||||
export * from "@prisma/client";
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Generated
+3949
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
@@ -0,0 +1,682 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Mi Casillero — Moraworld Imports</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--primary:#0057FF;--primary-d:#003FC7;--primary-l:#EEF3FF;
|
||||
--accent:#FF6B00;--dark:#0D1117;--green:#10B981;--red:#EF4444;
|
||||
--yellow:#F59E0B;--purple:#8B5CF6;
|
||||
--gray-900:#111827;--gray-800:#1F2937;--gray-700:#374151;
|
||||
--gray-600:#4B5563;--gray-500:#6B7280;--gray-400:#9CA3AF;
|
||||
--gray-300:#D1D5DB;--gray-200:#E5E7EB;--gray-100:#F3F4F6;
|
||||
--white:#fff;--sidebar-w:260px;
|
||||
--font-mono:'JetBrains Mono',monospace;
|
||||
}
|
||||
body{font-family:'Inter',sans-serif;background:var(--gray-100);color:var(--gray-900);display:flex;min-height:100vh}
|
||||
a{text-decoration:none;color:inherit}
|
||||
|
||||
/* SIDEBAR */
|
||||
aside{
|
||||
width:var(--sidebar-w);min-width:var(--sidebar-w);
|
||||
background:var(--dark);position:fixed;top:0;left:0;bottom:0;
|
||||
overflow-y:auto;z-index:100;display:flex;flex-direction:column;
|
||||
border-right:1px solid rgba(255,255,255,.06);
|
||||
}
|
||||
.sb-logo{padding:24px 20px 20px;border-bottom:1px solid rgba(255,255,255,.07)}
|
||||
.sb-logo .name{font-size:1rem;font-weight:800;color:#fff}
|
||||
.sb-logo .name span{color:var(--accent)}
|
||||
.sb-logo .tag{font-size:.68rem;color:rgba(255,255,255,.3);margin-top:4px;font-family:var(--font-mono)}
|
||||
.sb-user{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.07);display:flex;gap:12px;align-items:center}
|
||||
.sb-avatar{width:36px;height:36px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.85rem;color:#fff;flex-shrink:0}
|
||||
.sb-user-info .uname{font-size:.85rem;font-weight:600;color:#fff}
|
||||
.sb-user-info .urole{font-size:.7rem;color:rgba(255,255,255,.35)}
|
||||
.sb-section{padding:20px 16px 8px;font-size:.62rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:rgba(255,255,255,.25)}
|
||||
.sb-link{display:flex;align-items:center;gap:10px;padding:9px 20px;font-size:.82rem;color:rgba(255,255,255,.55);cursor:pointer;border-left:3px solid transparent;transition:all .15s}
|
||||
.sb-link:hover{color:#fff;background:rgba(255,255,255,.04);border-left-color:rgba(255,255,255,.15)}
|
||||
.sb-link.active{color:#fff;background:rgba(0,87,255,.12);border-left-color:var(--primary)}
|
||||
.sb-link .ico{font-size:.9rem;flex-shrink:0;width:16px;text-align:center}
|
||||
.sb-link .badge{margin-left:auto;background:var(--accent);color:#fff;font-size:.62rem;font-weight:700;padding:1px 7px;border-radius:999px}
|
||||
.sb-suite{
|
||||
margin:12px 16px;background:rgba(0,87,255,.12);border:1px solid rgba(0,87,255,.25);
|
||||
border-radius:10px;padding:14px 16px;
|
||||
}
|
||||
.sb-suite .st-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,.35);margin-bottom:8px}
|
||||
.sb-suite .st-addr{font-family:var(--font-mono);font-size:.75rem;color:#A5B4FC;line-height:1.7}
|
||||
.sb-suite .st-suite{color:var(--accent);font-weight:700;font-size:.82rem}
|
||||
.sb-suite .copy-btn{display:flex;align-items:center;gap:6px;margin-top:10px;background:rgba(0,87,255,.2);color:#7AADFF;border:none;border-radius:6px;padding:6px 10px;font-size:.72rem;font-weight:600;cursor:pointer;width:100%;justify-content:center;font-family:inherit;transition:background .2s}
|
||||
.sb-suite .copy-btn:hover{background:rgba(0,87,255,.35)}
|
||||
.sb-footer{margin-top:auto;padding:16px 20px;border-top:1px solid rgba(255,255,255,.07)}
|
||||
.sb-footer a{font-size:.78rem;color:rgba(255,255,255,.3);display:flex;align-items:center;gap:8px;padding:6px 0}
|
||||
.sb-footer a:hover{color:#fff}
|
||||
|
||||
/* MAIN */
|
||||
main{margin-left:var(--sidebar-w);flex:1;min-width:0}
|
||||
|
||||
/* TOPBAR */
|
||||
.topbar{
|
||||
position:sticky;top:0;z-index:90;background:rgba(255,255,255,.92);
|
||||
backdrop-filter:blur(12px);border-bottom:1px solid var(--gray-200);
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:0 32px;height:56px;gap:16px;
|
||||
}
|
||||
.topbar-title{font-size:.95rem;font-weight:700;color:var(--gray-900)}
|
||||
.topbar-right{display:flex;align-items:center;gap:12px}
|
||||
.notif-btn{position:relative;width:36px;height:36px;border-radius:8px;border:1px solid var(--gray-200);background:var(--white);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:1rem}
|
||||
.notif-dot{position:absolute;top:6px;right:6px;width:8px;height:8px;border-radius:50%;background:var(--accent);border:2px solid #fff}
|
||||
.tb-avatar{width:34px;height:34px;border-radius:50%;background:var(--primary);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.8rem;color:#fff;cursor:pointer}
|
||||
|
||||
/* CONTENT */
|
||||
.content{padding:32px;max-width:1100px}
|
||||
|
||||
/* STAT CARDS */
|
||||
.stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:28px}
|
||||
.stat-card{background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:20px 22px}
|
||||
.stat-card .sc-label{font-size:.78rem;color:var(--gray-500);font-weight:600;margin-bottom:8px;display:flex;align-items:center;gap:6px}
|
||||
.stat-card .sc-val{font-size:1.7rem;font-weight:800;color:var(--gray-900);margin-bottom:2px}
|
||||
.stat-card .sc-sub{font-size:.75rem;color:var(--gray-400)}
|
||||
.stat-card .sc-icon{font-size:1.4rem}
|
||||
|
||||
/* SECTION TITLE */
|
||||
.section-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
|
||||
.section-hd h2{font-size:1rem;font-weight:700;color:var(--gray-900)}
|
||||
.btn-sm{padding:8px 16px;border-radius:7px;font-size:.8rem;font-weight:600;border:none;cursor:pointer;font-family:inherit;transition:all .2s}
|
||||
.btn-primary{background:var(--primary);color:#fff}
|
||||
.btn-primary:hover{background:var(--primary-d)}
|
||||
.btn-outline{background:transparent;border:1.5px solid var(--gray-200);color:var(--gray-700)}
|
||||
.btn-outline:hover{border-color:var(--primary);color:var(--primary)}
|
||||
.btn-green{background:var(--green);color:#fff}
|
||||
|
||||
/* PACKAGES TABLE */
|
||||
.pkg-table{width:100%;border-collapse:collapse;font-size:.83rem}
|
||||
.pkg-table th{text-align:left;padding:10px 14px;background:var(--gray-900);color:#fff;font-size:.72rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase}
|
||||
.pkg-table th:first-child{border-radius:8px 0 0 0}
|
||||
.pkg-table th:last-child{border-radius:0 8px 0 0}
|
||||
.pkg-table td{padding:12px 14px;border-bottom:1px solid var(--gray-200);color:var(--gray-700);vertical-align:middle}
|
||||
.pkg-table tr:last-child td{border-bottom:none}
|
||||
.pkg-table tr:hover td{background:var(--gray-100)}
|
||||
.pkg-table .tid{font-family:var(--font-mono);font-size:.78rem;color:var(--primary);font-weight:600}
|
||||
.pkg-table .desc{font-weight:600;color:var(--gray-900)}
|
||||
.pkg-table .store{font-size:.75rem;color:var(--gray-500);margin-top:2px}
|
||||
|
||||
/* STATUS BADGES */
|
||||
.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:999px;font-size:.72rem;font-weight:700}
|
||||
.b-gray{background:#F3F4F6;color:#6B7280}
|
||||
.b-yellow{background:#FEF3C7;color:#92400E}
|
||||
.b-blue{background:#DBEAFE;color:#1D4ED8}
|
||||
.b-purple{background:#EDE9FE;color:#6D28D9}
|
||||
.b-green{background:#D1FAE5;color:#065F46}
|
||||
.b-orange{background:#FFEDD5;color:#C2410C}
|
||||
.b-red{background:#FEE2E2;color:#991B1B}
|
||||
.b-teal{background:#CCFBF1;color:#0F766E}
|
||||
.badge .dot{width:6px;height:6px;border-radius:50%;background:currentColor}
|
||||
|
||||
/* ACTION BTNS */
|
||||
.action-wrap{display:flex;gap:6px}
|
||||
.act-btn{padding:5px 10px;border-radius:6px;border:1px solid var(--gray-200);background:var(--white);font-size:.75rem;font-weight:600;cursor:pointer;color:var(--gray-700);font-family:inherit;transition:all .2s}
|
||||
.act-btn:hover{background:var(--primary);border-color:var(--primary);color:#fff}
|
||||
|
||||
/* MODAL */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:200;display:none;align-items:center;justify-content:center;padding:20px}
|
||||
.modal-overlay.show{display:flex}
|
||||
.modal{background:var(--white);border-radius:14px;padding:32px;width:100%;max-width:540px;max-height:90vh;overflow-y:auto;position:relative}
|
||||
.modal h3{font-size:1.15rem;font-weight:700;margin-bottom:20px}
|
||||
.modal-close{position:absolute;top:16px;right:16px;background:none;border:none;font-size:1.2rem;cursor:pointer;color:var(--gray-500);padding:4px}
|
||||
.modal-close:hover{color:var(--gray-900)}
|
||||
.form-group{margin-bottom:16px}
|
||||
.form-group label{display:block;font-size:.83rem;font-weight:600;color:var(--gray-700);margin-bottom:5px}
|
||||
.form-group input,.form-group select,.form-group textarea{width:100%;padding:11px 14px;border:2px solid var(--gray-200);border-radius:8px;font-size:.9rem;font-family:inherit;outline:none;transition:border-color .2s}
|
||||
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:var(--primary)}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.upload-box{border:2px dashed var(--gray-200);border-radius:8px;padding:20px;text-align:center;cursor:pointer;transition:border-color .2s}
|
||||
.upload-box:hover{border-color:var(--primary)}
|
||||
.upload-box p{font-size:.82rem;color:var(--gray-500);margin-top:6px}
|
||||
.tab-pills{display:flex;gap:4px;background:var(--gray-100);padding:4px;border-radius:8px;margin-bottom:20px}
|
||||
.tab-pill{flex:1;padding:8px 12px;border:none;border-radius:6px;font-size:.82rem;font-weight:600;cursor:pointer;font-family:inherit;color:var(--gray-600);background:transparent;transition:all .2s}
|
||||
.tab-pill.active{background:var(--white);color:var(--primary);box-shadow:0 1px 4px rgba(0,0,0,.08)}
|
||||
.tab-content{display:none}
|
||||
.tab-content.active{display:block}
|
||||
|
||||
/* TRACKING PANEL */
|
||||
.tracking-wrap{background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:24px}
|
||||
.tl{display:flex;flex-direction:column;gap:0}
|
||||
.tl-item{display:flex;gap:14px}
|
||||
.tl-line{display:flex;flex-direction:column;align-items:center}
|
||||
.tl-dot{width:14px;height:14px;border-radius:50%;border:2px solid var(--gray-200);background:var(--gray-200);flex-shrink:0}
|
||||
.tl-dot.done{background:var(--green);border-color:var(--green)}
|
||||
.tl-dot.active{background:var(--primary);border-color:var(--primary);box-shadow:0 0 0 4px rgba(0,87,255,.15)}
|
||||
.tl-connector{width:2px;flex:1;background:var(--gray-200);min-height:20px}
|
||||
.tl-connector.done{background:var(--green)}
|
||||
.tl-body{padding:0 0 18px 0}
|
||||
.tl-body strong{font-size:.87rem;display:block;margin-bottom:2px}
|
||||
.tl-body span{font-size:.78rem;color:var(--gray-500)}
|
||||
|
||||
/* EMPTY STATE */
|
||||
.empty-state{text-align:center;padding:60px 20px;color:var(--gray-500)}
|
||||
.empty-state .es-icon{font-size:3rem;margin-bottom:12px}
|
||||
.empty-state p{font-size:.9rem}
|
||||
|
||||
/* ALERT TOAST */
|
||||
.toast{position:fixed;bottom:24px;right:24px;z-index:500;background:var(--green);color:#fff;padding:12px 20px;border-radius:10px;font-size:.85rem;font-weight:600;display:flex;gap:10px;align-items:center;transform:translateY(80px);opacity:0;transition:all .3s;pointer-events:none}
|
||||
.toast.show{transform:translateY(0);opacity:1}
|
||||
|
||||
/* PANEL PAGES */
|
||||
.panel-page{display:none}
|
||||
.panel-page.active{display:block}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media(max-width:900px){
|
||||
aside{display:none}
|
||||
main{margin-left:0}
|
||||
.stats-row{grid-template-columns:1fr 1fr}
|
||||
.content{padding:20px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══ SIDEBAR ═══ -->
|
||||
<aside>
|
||||
<div class="sb-logo">
|
||||
<div class="name">Moraworld<span>.</span>Imports</div>
|
||||
<div class="tag">Portal del Cliente</div>
|
||||
</div>
|
||||
<div class="sb-user">
|
||||
<div class="sb-avatar">JP</div>
|
||||
<div class="sb-user-info">
|
||||
<div class="uname">Juan Pérez</div>
|
||||
<div class="urole">Cliente · Suite EC-04512</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sb-suite">
|
||||
<div class="st-label">Tu dirección en EE.UU.</div>
|
||||
<div class="st-addr">
|
||||
<div>150 N Day St,</div>
|
||||
<div class="st-suite">Suite EC-04512,</div>
|
||||
<div>City of Orange, NJ 07050</div>
|
||||
<div>United States 🇺🇸</div>
|
||||
</div>
|
||||
<button class="copy-btn" onclick="copySuite()">📋 Copiar dirección</button>
|
||||
</div>
|
||||
|
||||
<div class="sb-section">Principal</div>
|
||||
<div class="sb-link active" onclick="showPage('dashboard',this)"><span class="ico">🏠</span>Dashboard</div>
|
||||
<div class="sb-link" onclick="showPage('paquetes',this)"><span class="ico">📦</span>Mis paquetes<span class="badge">3</span></div>
|
||||
<div class="sb-link" onclick="showPage('prealerta',this)"><span class="ico">📬</span>Pre-alertas</div>
|
||||
<div class="sb-link" onclick="showPage('tracking',this)"><span class="ico">🔍</span>Tracking</div>
|
||||
|
||||
<div class="sb-section">Registro</div>
|
||||
<div class="sb-link" onclick="openModal('modal-compra')"><span class="ico">➕</span>Registrar compra</div>
|
||||
<div class="sb-link" onclick="openModal('modal-prealerta')"><span class="ico">📝</span>Nueva pre-alerta</div>
|
||||
|
||||
<div class="sb-section">Mi cuenta</div>
|
||||
<div class="sb-link" onclick="showPage('historial',this)"><span class="ico">📋</span>Historial</div>
|
||||
<div class="sb-link" onclick="showPage('calculadora',this)"><span class="ico">💰</span>Calculadora</div>
|
||||
<div class="sb-link" onclick="showPage('perfil',this)"><span class="ico">👤</span>Mi perfil</div>
|
||||
|
||||
<div class="sb-footer">
|
||||
<a href="index.html">← Ir al sitio web</a>
|
||||
<a href="login.html">🚪 Cerrar sesión</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ═══ MAIN ═══ -->
|
||||
<main>
|
||||
<div class="topbar">
|
||||
<div class="topbar-title" id="topbar-title">Dashboard</div>
|
||||
<div class="topbar-right">
|
||||
<button class="notif-btn" onclick="showToast('No hay notificaciones nuevas')">🔔<div class="notif-dot"></div></button>
|
||||
<div class="tb-avatar">JP</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<!-- ══ DASHBOARD ══ -->
|
||||
<div class="panel-page active" id="page-dashboard">
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="sc-label"><span class="sc-icon">📦</span>En tránsito a bodega</div>
|
||||
<div class="sc-val">2</div>
|
||||
<div class="sc-sub">paquetes en camino a NJ</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="sc-label"><span class="sc-icon">🔵</span>En bodega NJ</div>
|
||||
<div class="sc-val">1</div>
|
||||
<div class="sc-sub">recibido, en verificación</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="sc-label"><span class="sc-icon">✈️</span>En tránsito a Ecuador</div>
|
||||
<div class="sc-val">0</div>
|
||||
<div class="sc-sub">en camino a Ecuador</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="sc-label"><span class="sc-icon">✅</span>Entregados</div>
|
||||
<div class="sc-val">7</div>
|
||||
<div class="sc-sub">paquetes entregados</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:24px">
|
||||
<!-- Recent packages -->
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:20px">
|
||||
<div class="section-hd">
|
||||
<h2>Paquetes recientes</h2>
|
||||
<button class="btn-sm btn-outline" onclick="showPage('paquetes', null)">Ver todos</button>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
<div style="display:flex;gap:12px;align-items:center;padding:10px 0;border-bottom:1px solid var(--gray-100)">
|
||||
<div style="font-size:1.4rem">📱</div>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:.85rem;font-weight:600">Auriculares Sony WH-1000XM5</div>
|
||||
<div style="font-size:.75rem;color:var(--gray-500)">EC-20260510-014523</div>
|
||||
</div>
|
||||
<span class="badge b-yellow"><span class="dot"></span>En tránsito</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;align-items:center;padding:10px 0;border-bottom:1px solid var(--gray-100)">
|
||||
<div style="font-size:1.4rem">👟</div>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:.85rem;font-weight:600">Zapatillas Nike Air Max</div>
|
||||
<div style="font-size:.75rem;color:var(--gray-500)">EC-20260508-009821</div>
|
||||
</div>
|
||||
<span class="badge b-blue"><span class="dot"></span>En bodega</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;align-items:center;padding:10px 0">
|
||||
<div style="font-size:1.4rem">💻</div>
|
||||
<div style="flex:1">
|
||||
<div style="font-size:.85rem;font-weight:600">Laptop Dell XPS 13</div>
|
||||
<div style="font-size:.75rem;color:var(--gray-500)">EC-20260501-078234</div>
|
||||
</div>
|
||||
<span class="badge b-green"><span class="dot"></span>Entregado</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick actions -->
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:20px">
|
||||
<div class="section-hd"><h2>Acciones rápidas</h2></div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
<button onclick="openModal('modal-compra')" style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:var(--primary-l);border:1px solid #C7D7FF;border-radius:10px;cursor:pointer;text-align:left;font-family:inherit;transition:all .2s" onmouseover="this.style.background='#DEEAFF'" onmouseout="this.style.background='var(--primary-l)'">
|
||||
<span style="font-size:1.3rem">➕</span>
|
||||
<div><div style="font-size:.88rem;font-weight:700;color:var(--primary)">Registrar nueva compra</div><div style="font-size:.75rem;color:var(--gray-500)">Por enlace URL o ingreso manual</div></div>
|
||||
</button>
|
||||
<button onclick="openModal('modal-prealerta')" style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:#F0FDF4;border:1px solid #A7F3D0;border-radius:10px;cursor:pointer;text-align:left;font-family:inherit;transition:all .2s" onmouseover="this.style.background='#DCFCE7'" onmouseout="this.style.background='#F0FDF4'">
|
||||
<span style="font-size:1.3rem">📬</span>
|
||||
<div><div style="font-size:.88rem;font-weight:700;color:var(--green)">Crear pre-alerta</div><div style="font-size:.75rem;color:var(--gray-500)">Avísanos antes de que llegue tu paquete</div></div>
|
||||
</button>
|
||||
<button onclick="showPage('tracking',null)" style="display:flex;align-items:center;gap:12px;padding:14px 16px;background:#FFF3E8;border:1px solid #FED7AA;border-radius:10px;cursor:pointer;text-align:left;font-family:inherit;transition:all .2s" onmouseover="this.style.background='#FFE4C7'" onmouseout="this.style.background='#FFF3E8'">
|
||||
<span style="font-size:1.3rem">🔍</span>
|
||||
<div><div style="font-size:.88rem;font-weight:700;color:var(--accent)">Rastrear paquete</div><div style="font-size:.75rem;color:var(--gray-500)">Ingresa tu Tracking ID</div></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:20px">
|
||||
<div class="section-hd"><h2>Notificaciones recientes</h2></div>
|
||||
<div style="display:flex;flex-direction:column;gap:0">
|
||||
<div style="display:flex;gap:12px;padding:12px 0;border-bottom:1px solid var(--gray-100);align-items:flex-start">
|
||||
<div style="width:36px;height:36px;border-radius:10px;background:#D1FAE5;display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0">✅</div>
|
||||
<div><div style="font-size:.85rem;font-weight:600">Paquete verificado en bodega</div><div style="font-size:.78rem;color:var(--gray-500)">EC-20260508-009821 · Nike Air Max · Peso confirmado: 2.3 lbs</div><div style="font-size:.72rem;color:var(--gray-400);margin-top:2px">Hace 2 horas</div></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;padding:12px 0;border-bottom:1px solid var(--gray-100);align-items:flex-start">
|
||||
<div style="width:36px;height:36px;border-radius:10px;background:#DBEAFE;display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0">📦</div>
|
||||
<div><div style="font-size:.85rem;font-weight:600">Paquete recibido en bodega NJ</div><div style="font-size:.78rem;color:var(--gray-500)">EC-20260508-009821 · Recibido en 150 N Day St, Orange NJ</div><div style="font-size:.72rem;color:var(--gray-400);margin-top:2px">Ayer a las 11:30 AM</div></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;padding:12px 0;align-items:flex-start">
|
||||
<div style="width:36px;height:36px;border-radius:10px;background:#FEF3C7;display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0">🚚</div>
|
||||
<div><div style="font-size:.85rem;font-weight:600">Paquete en tránsito hacia la bodega</div><div style="font-size:.78rem;color:var(--gray-500)">EC-20260510-014523 · Auriculares Sony · UPS tracking: 1Z999AA10123456784</div><div style="font-size:.72rem;color:var(--gray-400);margin-top:2px">Hace 3 días</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ MIS PAQUETES ══ -->
|
||||
<div class="panel-page" id="page-paquetes">
|
||||
<div class="section-hd" style="margin-bottom:20px">
|
||||
<h2>Mis paquetes</h2>
|
||||
<button class="btn-sm btn-primary" onclick="openModal('modal-compra')">+ Registrar compra</button>
|
||||
</div>
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;overflow:hidden">
|
||||
<table class="pkg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tracking ID</th>
|
||||
<th>Producto</th>
|
||||
<th>Tienda</th>
|
||||
<th>Peso</th>
|
||||
<th>Valor</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><div class="tid">EC-20260510-014523</div></td>
|
||||
<td><div class="desc">Auriculares Sony WH-1000XM5</div></td>
|
||||
<td><div class="store">Amazon</div></td>
|
||||
<td>0.6 lbs</td>
|
||||
<td>$349.99</td>
|
||||
<td><span class="badge b-yellow"><span class="dot"></span>En tránsito a bodega</span></td>
|
||||
<td><div class="action-wrap"><button class="act-btn" onclick="showPkgDetail('EC-20260510-014523')">Ver</button><button class="act-btn">Fotos</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tid">EC-20260508-009821</div></td>
|
||||
<td><div class="desc">Zapatillas Nike Air Max 270</div></td>
|
||||
<td><div class="store">Nike.com</div></td>
|
||||
<td>2.3 lbs</td>
|
||||
<td>$129.00</td>
|
||||
<td><span class="badge b-purple"><span class="dot"></span>En verificación</span></td>
|
||||
<td><div class="action-wrap"><button class="act-btn" onclick="showPkgDetail('EC-20260508-009821')">Ver</button><button class="act-btn">📸 Fotos</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tid">EC-20260503-061004</div></td>
|
||||
<td><div class="desc">Cámara GoPro HERO12</div></td>
|
||||
<td><div class="store">Best Buy</div></td>
|
||||
<td>1.8 lbs</td>
|
||||
<td>$399.99</td>
|
||||
<td><span class="badge b-orange"><span class="dot"></span>En tránsito a Ecuador</span></td>
|
||||
<td><div class="action-wrap"><button class="act-btn" onclick="showPkgDetail('EC-20260503-061004')">Ver</button></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="tid">EC-20260501-078234</div></td>
|
||||
<td><div class="desc">Laptop Dell XPS 13</div></td>
|
||||
<td><div class="store">Dell.com</div></td>
|
||||
<td>4.4 lbs</td>
|
||||
<td>$1,299.00</td>
|
||||
<td><span class="badge b-green"><span class="dot"></span>Entregado</span></td>
|
||||
<td><div class="action-wrap"><button class="act-btn">Ver</button></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ PRE-ALERTAS ══ -->
|
||||
<div class="panel-page" id="page-prealerta">
|
||||
<div class="section-hd" style="margin-bottom:20px">
|
||||
<h2>Pre-alertas de paquetes</h2>
|
||||
<button class="btn-sm btn-primary" onclick="openModal('modal-prealerta')">+ Nueva pre-alerta</button>
|
||||
</div>
|
||||
<div style="background:#EFF6FF;border:1px solid #BFDBFE;border-radius:10px;padding:14px 18px;margin-bottom:20px;font-size:.85rem;color:#1D4ED8;display:flex;gap:10px">
|
||||
<span>💡</span>
|
||||
<span>Envíanos una pre-alerta antes de que llegue tu paquete a la bodega. El operador lo buscará y asignará a tu Suite más rápido.</span>
|
||||
</div>
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;overflow:hidden">
|
||||
<table class="pkg-table">
|
||||
<thead>
|
||||
<tr><th>Fecha</th><th>Tienda</th><th>Tracking vendedor</th><th>Descripción</th><th>Valor declarado</th><th>Estado</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>10 may 2026</td>
|
||||
<td>Amazon</td>
|
||||
<td><span style="font-family:var(--font-mono);font-size:.78rem">1Z999AA10123456784</span></td>
|
||||
<td>Auriculares Sony WH-1000XM5</td>
|
||||
<td>$349.99</td>
|
||||
<td><span class="badge b-yellow"><span class="dot"></span>En espera</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ TRACKING ══ -->
|
||||
<div class="panel-page" id="page-tracking">
|
||||
<h2 style="font-size:1rem;font-weight:700;margin-bottom:20px">Rastrear paquete</h2>
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:24px">
|
||||
<div style="display:flex;gap:0;max-width:500px;margin-bottom:28px">
|
||||
<input type="text" id="track-input" placeholder="Ej: EC-20260510-014523" style="flex:1;padding:13px 16px;border:2px solid var(--gray-200);border-right:none;border-radius:8px 0 0 8px;font-size:.93rem;font-family:inherit;outline:none;transition:border-color .2s" onfocus="this.style.borderColor='var(--primary)'" onblur="this.style.borderColor='var(--gray-200)'"/>
|
||||
<button onclick="doTracking()" style="padding:13px 24px;background:var(--primary);color:#fff;border:none;border-radius:0 8px 8px 0;font-weight:700;font-size:.9rem;cursor:pointer;font-family:inherit;transition:background .2s" onmouseover="this.style.background='var(--primary-d)'" onmouseout="this.style.background='var(--primary)'">Rastrear</button>
|
||||
</div>
|
||||
<div id="tracking-result" style="display:none">
|
||||
<div style="font-size:.9rem;font-weight:700;color:var(--gray-900);margin-bottom:20px">📦 <span id="t-id">EC-20260508-009821</span> — <span style="color:var(--primary)">En verificación en bodega NJ</span></div>
|
||||
<div class="tl">
|
||||
<div class="tl-item"><div class="tl-line"><div class="tl-dot done"></div><div class="tl-connector done"></div></div><div class="tl-body"><strong>✅ Registrado</strong><span>08 may 2026, 09:14 AM ECT</span></div></div>
|
||||
<div class="tl-item"><div class="tl-line"><div class="tl-dot done"></div><div class="tl-connector done"></div></div><div class="tl-body"><strong>✅ En tránsito a bodega NJ</strong><span>09 may 2026, 10:00 AM EST</span></div></div>
|
||||
<div class="tl-item"><div class="tl-line"><div class="tl-dot done"></div><div class="tl-connector done"></div></div><div class="tl-body"><strong>✅ Recibido en bodega NJ</strong><span>10 may 2026, 11:30 AM EST</span></div></div>
|
||||
<div class="tl-item"><div class="tl-line"><div class="tl-dot active"></div><div class="tl-connector"></div></div><div class="tl-body"><strong style="color:var(--primary)">🔵 En verificación</strong><span>Operador verificando peso y dimensiones</span></div></div>
|
||||
<div class="tl-item"><div class="tl-line"><div class="tl-dot"></div><div class="tl-connector"></div></div><div class="tl-body"><strong style="color:var(--gray-400)">⏳ Declaración SENAE</strong><span>Pendiente</span></div></div>
|
||||
<div class="tl-item"><div class="tl-line"><div class="tl-dot"></div><div class="tl-connector"></div></div><div class="tl-body"><strong style="color:var(--gray-400)">⏳ En tránsito a Ecuador</strong><span>Pendiente</span></div></div>
|
||||
<div class="tl-item"><div class="tl-line"><div class="tl-dot"></div></div><div class="tl-body"><strong style="color:var(--gray-400)">⏳ Entregado</strong><span>Pendiente</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ HISTORIAL ══ -->
|
||||
<div class="panel-page" id="page-historial">
|
||||
<h2 style="font-size:1rem;font-weight:700;margin-bottom:20px">Historial de envíos</h2>
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;overflow:hidden">
|
||||
<table class="pkg-table">
|
||||
<thead><tr><th>Tracking ID</th><th>Producto</th><th>Fecha entrega</th><th>Peso</th><th>Total pagado</th><th>Estado</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><div class="tid">EC-20260501-078234</div></td><td>Laptop Dell XPS 13</td><td>15 may 2026</td><td>4.4 lbs</td><td>$78.50</td><td><span class="badge b-green"><span class="dot"></span>Entregado</span></td></tr>
|
||||
<tr><td><div class="tid">EC-20260420-045001</div></td><td>Auriculares Bose 700</td><td>05 may 2026</td><td>0.8 lbs</td><td>$23.40</td><td><span class="badge b-green"><span class="dot"></span>Entregado</span></td></tr>
|
||||
<tr><td><div class="tid">EC-20260410-033210</div></td><td>Cargador iPhone 15 Pro</td><td>25 abr 2026</td><td>0.2 lbs</td><td>$8.90</td><td><span class="badge b-green"><span class="dot"></span>Entregado</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ CALCULADORA ══ -->
|
||||
<div class="panel-page" id="page-calculadora">
|
||||
<h2 style="font-size:1rem;font-weight:700;margin-bottom:20px">Calculadora de envío</h2>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:24px">
|
||||
<h3 style="font-size:1rem;font-weight:700;margin-bottom:20px">📊 Datos del producto</h3>
|
||||
<div class="form-group"><label>Peso (lbs)</label><input type="number" id="cc-w" placeholder="Ej: 2.5" step="0.1"/></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="form-group"><label>Largo (cm)</label><input type="number" id="cc-l" placeholder="30"/></div>
|
||||
<div class="form-group"><label>Ancho (cm)</label><input type="number" id="cc-a" placeholder="20"/></div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="form-group"><label>Alto (cm)</label><input type="number" id="cc-h" placeholder="15"/></div>
|
||||
<div class="form-group"><label>Valor ($USD)</label><input type="number" id="cc-v" placeholder="150"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Categoría SENAE</label>
|
||||
<select id="cc-cat">
|
||||
<option value="4x4">4x4 — Sin arancel (máx $400, 4 kg)</option>
|
||||
<option value="B">Cat. B — Consumo general (10%)</option>
|
||||
<option value="C">Cat. C — Textiles, calzado (20%)</option>
|
||||
<option value="D">Cat. D — Electrónicos (10%)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="calcPortal()" style="width:100%;padding:12px;background:var(--primary);color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-family:inherit;font-size:.9rem">Calcular →</button>
|
||||
</div>
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:24px">
|
||||
<h3 style="font-size:1rem;font-weight:700;margin-bottom:20px">💰 Desglose estimado</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:0">
|
||||
<div style="display:flex;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--gray-100);font-size:.87rem"><span style="color:var(--gray-500)">Peso a cobrar</span><span id="cc-r-peso" style="font-weight:600">—</span></div>
|
||||
<div style="display:flex;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--gray-100);font-size:.87rem"><span style="color:var(--gray-500)">Flete ($3.50/lb)</span><span id="cc-r-flete" style="font-weight:600">$0.00</span></div>
|
||||
<div style="display:flex;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--gray-100);font-size:.87rem"><span style="color:var(--gray-500)">Seguro (2%)</span><span id="cc-r-seg" style="font-weight:600">$0.00</span></div>
|
||||
<div style="display:flex;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--gray-100);font-size:.87rem"><span style="color:var(--gray-500)">FODINFA (0.5%)</span><span id="cc-r-fod" style="font-weight:600">$0.00</span></div>
|
||||
<div style="display:flex;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--gray-100);font-size:.87rem"><span style="color:var(--gray-500)">Arancel</span><span id="cc-r-ara" style="font-weight:600">$0.00</span></div>
|
||||
<div style="display:flex;justify-content:space-between;padding:11px 0;border-bottom:1px solid var(--gray-100);font-size:.87rem"><span style="color:var(--gray-500)">IVA (15%)</span><span id="cc-r-iva" style="font-weight:600">$0.00</span></div>
|
||||
</div>
|
||||
<div style="background:linear-gradient(135deg,var(--primary),var(--primary-d));border-radius:10px;padding:18px 20px;margin-top:16px;display:flex;justify-content:space-between;align-items:center">
|
||||
<span style="color:rgba(255,255,255,.8);font-size:.88rem;font-weight:600">TOTAL ESTIMADO</span>
|
||||
<span id="cc-r-total" style="color:#fff;font-size:1.4rem;font-weight:800">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ PERFIL ══ -->
|
||||
<div class="panel-page" id="page-perfil">
|
||||
<h2 style="font-size:1rem;font-weight:700;margin-bottom:20px">Mi perfil</h2>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:24px">
|
||||
<h3 style="font-size:.95rem;font-weight:700;margin-bottom:20px">Datos personales</h3>
|
||||
<div class="form-group"><label>Nombre completo</label><input type="text" value="Juan Pérez"/></div>
|
||||
<div class="form-group"><label>Correo electrónico</label><input type="email" value="juan.perez@ejemplo.com"/></div>
|
||||
<div class="form-group"><label>Teléfono WhatsApp</label><input type="tel" value="+593 99 123 4567"/></div>
|
||||
<div class="form-group"><label>Ciudad</label><input type="text" value="Cuenca, Ecuador"/></div>
|
||||
<div class="form-group"><label>Cédula / Pasaporte</label><input type="text" value="0102030405"/></div>
|
||||
<button class="btn-sm btn-primary" style="width:100%">Guardar cambios</button>
|
||||
</div>
|
||||
<div style="background:var(--white);border:1px solid var(--gray-200);border-radius:12px;padding:24px">
|
||||
<h3 style="font-size:.95rem;font-weight:700;margin-bottom:20px">Mi Suite asignada</h3>
|
||||
<div style="background:var(--dark);border-radius:10px;padding:18px 20px;font-family:var(--font-mono);font-size:.85rem;color:#A5B4FC;line-height:2;border:1px solid rgba(0,87,255,.3);margin-bottom:16px">
|
||||
<div style="color:#fff;font-weight:700">Juan Pérez</div>
|
||||
<div>150 N Day St,</div>
|
||||
<div style="color:var(--accent);font-weight:700;font-size:.95rem">Suite EC-04512,</div>
|
||||
<div>City of Orange, NJ 07050</div>
|
||||
<div>United States 🇺🇸</div>
|
||||
</div>
|
||||
<button onclick="copySuite()" style="width:100%;padding:12px;background:var(--primary-l);color:var(--primary);border:1px solid #C7D7FF;border-radius:8px;font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit">📋 Copiar dirección completa</button>
|
||||
<div style="margin-top:20px">
|
||||
<h3 style="font-size:.95rem;font-weight:700;margin-bottom:16px">Seguridad</h3>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--gray-100);font-size:.85rem"><span>Contraseña</span><button class="btn-sm btn-outline">Cambiar</button></div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 0;font-size:.85rem"><span>MFA (autenticación 2 pasos)</span><span style="background:#D1FAE5;color:#065F46;font-size:.72rem;font-weight:700;padding:3px 10px;border-radius:999px">Activo ✓</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /content -->
|
||||
</main>
|
||||
|
||||
<!-- ═══ MODAL: REGISTRAR COMPRA ═══ -->
|
||||
<div class="modal-overlay" id="modal-compra">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeModal('modal-compra')">✕</button>
|
||||
<h3>➕ Registrar nueva compra</h3>
|
||||
<div class="tab-pills">
|
||||
<button class="tab-pill active" onclick="switchTab('tab-url','tab-manual',this)">🔗 Por enlace URL</button>
|
||||
<button class="tab-pill" onclick="switchTab('tab-manual','tab-url',this)">✏️ Ingreso manual</button>
|
||||
</div>
|
||||
<div class="tab-content active" id="tab-url">
|
||||
<div class="form-group">
|
||||
<label>Pega el enlace del producto (Amazon, eBay, etc.)</label>
|
||||
<div style="display:flex;gap:8px">
|
||||
<input type="url" id="url-input" placeholder="https://www.amazon.com/dp/..." style="flex:1;padding:11px 14px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;font-size:.9rem;outline:none" onfocus="this.style.borderColor='var(--primary)'" onblur="this.style.borderColor='var(--gray-200)'"/>
|
||||
<button onclick="scanUrl()" style="padding:11px 16px;background:var(--primary);color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap">Escanear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="url-preview" style="display:none;background:var(--gray-100);border-radius:10px;padding:16px;margin-bottom:16px">
|
||||
<div style="font-size:.85rem;font-weight:700;margin-bottom:4px" id="url-prod-name">Auriculares Sony WH-1000XM5</div>
|
||||
<div style="font-size:.8rem;color:var(--gray-500)">Amazon · $349.99 · ~0.6 lbs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content" id="tab-manual">
|
||||
<div class="form-group"><label>Nombre del producto</label><input type="text" placeholder="Ej: Auriculares Sony WH-1000XM5"/></div>
|
||||
<div class="form-group"><label>Tienda / Plataforma</label><select><option>Amazon</option><option>eBay</option><option>Walmart</option><option>Shein</option><option>Otra</option></select></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="form-group"><label>Valor ($USD)</label><input type="number" placeholder="349.99"/></div>
|
||||
<div class="form-group"><label>Peso estimado (lbs)</label><input type="number" placeholder="0.6"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Categoría SENAE</label><select><option>4x4 — Sin arancel</option><option>Cat. B — 10%</option><option>Cat. C — 20%</option><option>Cat. D — 10%</option></select></div>
|
||||
<div class="form-group"><label>Cantidad</label><input type="number" value="1" min="1"/></div>
|
||||
</div>
|
||||
<button onclick="submitCompra()" style="width:100%;padding:13px;background:var(--primary);color:#fff;border:none;border-radius:8px;font-weight:700;font-size:.95rem;cursor:pointer;font-family:inherit;margin-top:8px">Registrar compra →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ MODAL: PRE-ALERTA ═══ -->
|
||||
<div class="modal-overlay" id="modal-prealerta">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeModal('modal-prealerta')">✕</button>
|
||||
<h3>📬 Nueva pre-alerta</h3>
|
||||
<div class="form-group"><label>Tienda / Plataforma</label><select><option>Amazon</option><option>eBay</option><option>Walmart</option><option>Shein</option><option>Otra</option></select></div>
|
||||
<div class="form-group"><label>Número de tracking del vendedor</label><input type="text" placeholder="Ej: 1Z999AA10123456784 (UPS)"/></div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||
<div class="form-group"><label>Valor declarado ($USD)</label><input type="number" placeholder="120"/></div>
|
||||
<div class="form-group"><label>Peso estimado (lbs)</label><input type="number" placeholder="1.5"/></div>
|
||||
</div>
|
||||
<div class="form-group"><label>Descripción del producto</label><input type="text" placeholder="Ej: Auriculares Bluetooth Sony"/></div>
|
||||
<div class="form-group"><label>Fecha estimada de llegada a NJ</label><input type="date"/></div>
|
||||
<div class="form-group">
|
||||
<label>Factura comercial (PDF o imagen)</label>
|
||||
<div class="upload-box" onclick="document.getElementById('factura-file').click()">
|
||||
<div style="font-size:1.8rem">📎</div>
|
||||
<p>Haz clic para subir la factura de Amazon o la tienda</p>
|
||||
<input type="file" id="factura-file" style="display:none" accept=".pdf,image/*"/>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="submitPrealerta()" style="width:100%;padding:13px;background:var(--green);color:#fff;border:none;border-radius:8px;font-weight:700;font-size:.95rem;cursor:pointer;font-family:inherit">Enviar pre-alerta →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOAST -->
|
||||
<div class="toast" id="toast">✅ <span id="toast-msg"></span></div>
|
||||
|
||||
<script>
|
||||
// Page navigation
|
||||
function showPage(id, el){
|
||||
document.querySelectorAll('.panel-page').forEach(p=>p.classList.remove('active'));
|
||||
document.getElementById('page-'+id).classList.add('active');
|
||||
document.querySelectorAll('.sb-link').forEach(l=>l.classList.remove('active'));
|
||||
if(el) el.classList.add('active');
|
||||
const titles={dashboard:'Dashboard',paquetes:'Mis paquetes',prealerta:'Pre-alertas',tracking:'Tracking',historial:'Historial de envíos',calculadora:'Calculadora de envío',perfil:'Mi perfil'};
|
||||
document.getElementById('topbar-title').textContent = titles[id]||id;
|
||||
}
|
||||
// Modals
|
||||
function openModal(id){ document.getElementById(id).classList.add('show'); document.body.style.overflow='hidden'; }
|
||||
function closeModal(id){ document.getElementById(id).classList.remove('show'); document.body.style.overflow=''; }
|
||||
document.querySelectorAll('.modal-overlay').forEach(o=>{ o.addEventListener('click',function(e){ if(e.target===this) this.classList.remove('show'),document.body.style.overflow=''; }); });
|
||||
// Tabs
|
||||
function switchTab(show, hide, btn){
|
||||
document.getElementById(show).classList.add('active');
|
||||
document.getElementById(hide).classList.remove('active');
|
||||
btn.closest('.tab-pills').querySelectorAll('.tab-pill').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
}
|
||||
// Copy suite
|
||||
function copySuite(){
|
||||
const addr = 'Juan Pérez\n150 N Day St, Suite EC-04512\nCity of Orange, NJ 07050\nUnited States';
|
||||
navigator.clipboard.writeText(addr).catch(()=>{});
|
||||
showToast('¡Dirección copiada al portapapeles!');
|
||||
}
|
||||
// Tracking
|
||||
function doTracking(){
|
||||
const v = document.getElementById('track-input').value.trim();
|
||||
if(!v){ alert('Ingresa un Tracking ID'); return; }
|
||||
document.getElementById('t-id').textContent = v;
|
||||
document.getElementById('tracking-result').style.display='block';
|
||||
}
|
||||
// Scan URL (demo)
|
||||
function scanUrl(){
|
||||
const url = document.getElementById('url-input').value.trim();
|
||||
if(!url){ return; }
|
||||
document.getElementById('url-preview').style.display='block';
|
||||
}
|
||||
// Calc
|
||||
function calcPortal(){
|
||||
const w=parseFloat(document.getElementById('cc-w').value)||0;
|
||||
const l=parseFloat(document.getElementById('cc-l').value)||0;
|
||||
const a=parseFloat(document.getElementById('cc-a').value)||0;
|
||||
const h=parseFloat(document.getElementById('cc-h').value)||0;
|
||||
const v=parseFloat(document.getElementById('cc-v').value)||0;
|
||||
const cat=document.getElementById('cc-cat').value;
|
||||
const volW=(l*a*h)/139;
|
||||
const pf=Math.max(w,volW);
|
||||
const aranceles={'4x4':0,'B':.10,'C':.20,'D':.10};
|
||||
const flete=pf*3.5,seg=v*.02,fod=v*.005,ara=v*(aranceles[cat]||0),iva=(v+fod+ara)*.15;
|
||||
const total=flete+seg+fod+ara+iva;
|
||||
const f=n=>'$'+n.toFixed(2);
|
||||
document.getElementById('cc-r-peso').textContent=pf.toFixed(2)+' lbs';
|
||||
document.getElementById('cc-r-flete').textContent=f(flete);
|
||||
document.getElementById('cc-r-seg').textContent=f(seg);
|
||||
document.getElementById('cc-r-fod').textContent=f(fod);
|
||||
document.getElementById('cc-r-ara').textContent=f(ara)+(cat==='4x4'?' (exento)':'');
|
||||
document.getElementById('cc-r-iva').textContent=f(iva);
|
||||
document.getElementById('cc-r-total').textContent=f(total);
|
||||
}
|
||||
function submitCompra(){ closeModal('modal-compra'); showToast('¡Compra registrada con éxito! Tracking ID asignado.'); }
|
||||
function submitPrealerta(){ closeModal('modal-prealerta'); showToast('¡Pre-alerta enviada a la bodega NJ!'); }
|
||||
function showToast(msg){
|
||||
const t=document.getElementById('toast');
|
||||
document.getElementById('toast-msg').textContent=msg;
|
||||
t.classList.add('show');
|
||||
setTimeout(()=>t.classList.remove('show'),3000);
|
||||
}
|
||||
function showPkgDetail(tid){ showToast('Abriendo detalle de '+tid); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,957 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Moraworld Imports — Presentación de Requerimientos v1.1</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--blue:#0057FF;--blue-d:#003FC7;--blue-l:#EEF3FF;
|
||||
--orange:#FF6B00;--green:#10B981;--red:#EF4444;--purple:#8B5CF6;--yellow:#F59E0B;
|
||||
--dark:#0D1117;--gray-900:#111827;--gray-700:#374151;--gray-500:#6B7280;
|
||||
--gray-300:#D1D5DB;--gray-200:#E5E7EB;--gray-100:#F3F4F6;--white:#fff;
|
||||
}
|
||||
html,body{height:100%;overflow:hidden;font-family:'Inter',sans-serif;background:var(--dark);color:var(--white)}
|
||||
|
||||
/* ── DECK SHELL ── */
|
||||
.deck{width:100vw;height:100vh;position:relative;overflow:hidden}
|
||||
|
||||
/* ── PROGRESS BAR ── */
|
||||
#progress{position:fixed;top:0;left:0;height:3px;background:linear-gradient(90deg,var(--blue),var(--orange));width:0;z-index:999;transition:width .4s ease}
|
||||
|
||||
/* ── COUNTER ── */
|
||||
#counter{
|
||||
position:fixed;bottom:24px;right:32px;z-index:999;
|
||||
font-size:.78rem;font-weight:600;color:rgba(255,255,255,.4);
|
||||
font-family:'JetBrains Mono',monospace;letter-spacing:.05em
|
||||
}
|
||||
|
||||
/* ── NAV ARROWS ── */
|
||||
.nav-btn{
|
||||
position:fixed;top:50%;transform:translateY(-50%);z-index:999;
|
||||
width:44px;height:44px;border-radius:50%;background:rgba(255,255,255,.08);
|
||||
border:1px solid rgba(255,255,255,.12);cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:1.1rem;color:rgba(255,255,255,.6);transition:all .2s;
|
||||
}
|
||||
.nav-btn:hover{background:rgba(255,255,255,.15);color:var(--white)}
|
||||
#btn-prev{left:16px}
|
||||
#btn-next{right:16px}
|
||||
|
||||
/* ── KEYBOARD HINT ── */
|
||||
#hint{
|
||||
position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
|
||||
font-size:.72rem;color:rgba(255,255,255,.25);letter-spacing:.05em;
|
||||
font-family:'JetBrains Mono',monospace;
|
||||
}
|
||||
|
||||
/* ── SLIDE BASE ── */
|
||||
.slide{
|
||||
position:absolute;inset:0;display:flex;flex-direction:column;
|
||||
opacity:0;pointer-events:none;transition:opacity .5s ease,transform .5s ease;
|
||||
transform:translateX(60px);
|
||||
}
|
||||
.slide.active{opacity:1;pointer-events:all;transform:translateX(0)}
|
||||
.slide.exit{opacity:0;transform:translateX(-60px)}
|
||||
|
||||
/* ── SLIDE INNER ── */
|
||||
.s-wrap{
|
||||
flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;
|
||||
padding:60px 80px;overflow:auto;
|
||||
}
|
||||
.s-wrap.top{justify-content:flex-start;padding-top:80px}
|
||||
|
||||
/* ── SLIDE HEADERS ── */
|
||||
.slide-num{
|
||||
font-size:.7rem;font-weight:700;letter-spacing:.15em;text-transform:uppercase;
|
||||
color:rgba(255,255,255,.3);font-family:'JetBrains Mono',monospace;
|
||||
position:absolute;top:28px;left:80px;
|
||||
}
|
||||
.slide-section{
|
||||
font-size:.7rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
|
||||
color:var(--blue);font-family:'JetBrains Mono',monospace;
|
||||
position:absolute;top:28px;right:80px;
|
||||
}
|
||||
|
||||
/* ── TYPOGRAPHY ── */
|
||||
.tag{display:inline-block;background:rgba(0,87,255,.2);color:#7AADFF;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:4px 14px;border-radius:999px;margin-bottom:18px}
|
||||
h1.hero-title{font-size:clamp(2.8rem,6vw,5rem);font-weight:900;line-height:1.05;text-align:center}
|
||||
h1.hero-title em{color:var(--orange);font-style:normal}
|
||||
h2.stitle{font-size:clamp(1.8rem,3.5vw,2.8rem);font-weight:800;line-height:1.2;margin-bottom:12px}
|
||||
h2.stitle em{color:var(--blue);font-style:normal}
|
||||
.sub{font-size:1rem;color:rgba(255,255,255,.55);max-width:680px;line-height:1.7}
|
||||
|
||||
/* ── GRID UTILS ── */
|
||||
.g2{display:grid;grid-template-columns:1fr 1fr;gap:24px;width:100%;max-width:1000px}
|
||||
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;width:100%;max-width:1000px}
|
||||
.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;width:100%;max-width:1000px}
|
||||
|
||||
/* ── CARDS ── */
|
||||
.card{
|
||||
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.09);
|
||||
border-radius:14px;padding:24px 22px;
|
||||
}
|
||||
.card.accent{border-color:var(--blue);background:rgba(0,87,255,.1)}
|
||||
.card.orange{border-color:var(--orange);background:rgba(255,107,0,.1)}
|
||||
.card.green{border-color:var(--green);background:rgba(16,185,129,.1)}
|
||||
.card.purple{border-color:var(--purple);background:rgba(139,92,246,.1)}
|
||||
|
||||
.card-icon{font-size:1.8rem;margin-bottom:12px}
|
||||
.card h3{font-size:1rem;font-weight:700;margin-bottom:8px}
|
||||
.card p{font-size:.82rem;color:rgba(255,255,255,.55);line-height:1.6}
|
||||
.card ul{margin-top:10px;display:flex;flex-direction:column;gap:6px}
|
||||
.card li{font-size:.8rem;color:rgba(255,255,255,.65);display:flex;gap:8px;align-items:flex-start}
|
||||
.card li::before{content:'›';color:var(--blue);font-weight:700;flex-shrink:0}
|
||||
.card .badge{display:inline-block;margin-top:10px;font-size:.68rem;font-weight:700;padding:3px 10px;border-radius:999px;letter-spacing:.05em}
|
||||
.bg-blue-b{background:rgba(0,87,255,.25);color:#7AADFF}
|
||||
.bg-green-b{background:rgba(16,185,129,.2);color:#6EE7B7}
|
||||
.bg-orange-b{background:rgba(255,107,0,.2);color:#FDB87A}
|
||||
.bg-purple-b{background:rgba(139,92,246,.2);color:#C4B5FD}
|
||||
|
||||
/* ── FLOW DIAGRAM ── */
|
||||
.flow{display:flex;flex-direction:column;align-items:center;gap:0;width:100%}
|
||||
.flow-step{
|
||||
display:flex;align-items:center;gap:14px;width:100%;max-width:720px;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);
|
||||
border-radius:10px;padding:12px 18px;
|
||||
}
|
||||
.flow-step.done{border-color:rgba(16,185,129,.4);background:rgba(16,185,129,.07)}
|
||||
.flow-step.active{border-color:var(--blue);background:rgba(0,87,255,.12)}
|
||||
.flow-step.decision{border-color:rgba(245,158,11,.4);background:rgba(245,158,11,.08);border-style:dashed}
|
||||
.flow-num{width:28px;height:28px;border-radius:50%;background:var(--blue);color:var(--white);font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.flow-num.g{background:var(--green)}
|
||||
.flow-num.o{background:var(--orange)}
|
||||
.flow-num.y{background:var(--yellow)}
|
||||
.flow-text{font-size:.83rem;color:rgba(255,255,255,.8);flex:1;line-height:1.4}
|
||||
.flow-arrow{text-align:center;color:rgba(255,255,255,.2);font-size:1rem;padding:3px 0}
|
||||
.flow-split{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:100%;max-width:720px}
|
||||
|
||||
/* ── TABLE ── */
|
||||
.stbl{width:100%;max-width:960px;border-collapse:collapse;font-size:.78rem}
|
||||
.stbl th{text-align:left;padding:10px 14px;background:rgba(0,87,255,.2);color:#7AADFF;font-weight:700;font-size:.72rem;letter-spacing:.06em;text-transform:uppercase}
|
||||
.stbl td{padding:9px 14px;border-bottom:1px solid rgba(255,255,255,.06);color:rgba(255,255,255,.75)}
|
||||
.stbl tr:hover td{background:rgba(255,255,255,.03)}
|
||||
.stbl .yes{color:var(--green);font-weight:700}
|
||||
.stbl .no{color:rgba(255,255,255,.2)}
|
||||
.stbl .part{color:var(--yellow);font-weight:600}
|
||||
|
||||
/* ── STATES TIMELINE ── */
|
||||
.timeline{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;max-width:900px}
|
||||
.t-state{
|
||||
display:flex;align-items:center;gap:8px;
|
||||
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);
|
||||
border-radius:8px;padding:9px 14px;font-size:.78rem;font-weight:500;
|
||||
}
|
||||
.t-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.t-num{font-size:.65rem;font-family:'JetBrains Mono',monospace;color:rgba(255,255,255,.3);margin-left:auto}
|
||||
|
||||
/* ── ARCH DIAGRAM ── */
|
||||
.arch{display:flex;flex-direction:column;align-items:center;gap:12px;margin-top:8px}
|
||||
.arch-row{display:flex;gap:12px;align-items:center;justify-content:center}
|
||||
.arch-box{
|
||||
background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);
|
||||
border-radius:10px;padding:10px 18px;font-size:.8rem;font-weight:600;
|
||||
text-align:center;min-width:120px;
|
||||
}
|
||||
.arch-box.internet{border-color:rgba(0,87,255,.5);background:rgba(0,87,255,.12);color:#7AADFF}
|
||||
.arch-box.gateway{border-color:rgba(245,158,11,.4);background:rgba(245,158,11,.1);color:#FDE68A}
|
||||
.arch-box.tenant{border-color:rgba(139,92,246,.4);background:rgba(139,92,246,.1);color:#DDD6FE;min-width:100px}
|
||||
.arch-box.db{border-color:rgba(16,185,129,.4);background:rgba(16,185,129,.1);color:#A7F3D0}
|
||||
.arch-arrow{color:rgba(255,255,255,.25);font-size:.9rem;text-align:center}
|
||||
|
||||
/* ── ISO TABLE ── */
|
||||
.iso-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%;max-width:900px}
|
||||
.iso-item{
|
||||
display:flex;gap:12px;align-items:flex-start;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:10px;padding:12px 14px;
|
||||
}
|
||||
.iso-label{font-size:.72rem;font-weight:700;color:#7AADFF;font-family:'JetBrains Mono',monospace;white-space:nowrap;padding-top:1px}
|
||||
.iso-desc{font-size:.78rem;color:rgba(255,255,255,.65);line-height:1.5}
|
||||
|
||||
/* ── HERO SLIDE BG ── */
|
||||
.slide-hero{background:radial-gradient(ellipse at 30% 50%,rgba(0,87,255,.2) 0%,transparent 60%),radial-gradient(ellipse at 70% 70%,rgba(255,107,0,.12) 0%,transparent 60%),var(--dark)}
|
||||
.slide-dark{background:var(--dark)}
|
||||
.slide-blue{background:linear-gradient(135deg,#0D2150,#0D1117)}
|
||||
.slide-orange{background:linear-gradient(135deg,#1a0a00,#0D1117)}
|
||||
|
||||
/* ── ROLE BADGES ── */
|
||||
.role-grid{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-top:12px}
|
||||
.role-card{
|
||||
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);
|
||||
border-radius:12px;padding:16px 20px;text-align:center;min-width:140px;
|
||||
}
|
||||
.role-icon{font-size:1.6rem;margin-bottom:8px}
|
||||
.role-name{font-size:.85rem;font-weight:700;margin-bottom:4px}
|
||||
.role-desc{font-size:.72rem;color:rgba(255,255,255,.45);line-height:1.4}
|
||||
|
||||
/* ── FORMULA BOX ── */
|
||||
.formula{
|
||||
background:rgba(0,0,0,.4);border:1px solid rgba(0,87,255,.3);
|
||||
border-radius:10px;padding:18px 24px;font-family:'JetBrains Mono',monospace;
|
||||
font-size:.78rem;color:#A5B4FC;line-height:2;white-space:pre;max-width:660px;
|
||||
}
|
||||
.formula .comment{color:rgba(255,255,255,.2)}
|
||||
.formula .value{color:#FCD34D}
|
||||
.formula .result{color:#6EE7B7;font-weight:700}
|
||||
|
||||
/* ── INTEGRATION CHIPS ── */
|
||||
.int-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;max-width:860px;width:100%}
|
||||
.int-item{
|
||||
display:flex;align-items:center;gap:12px;
|
||||
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);
|
||||
border-radius:10px;padding:12px 16px;
|
||||
}
|
||||
.int-icon{font-size:1.4rem;flex-shrink:0}
|
||||
.int-name{font-size:.82rem;font-weight:700;display:block}
|
||||
.int-proto{font-size:.7rem;color:rgba(255,255,255,.35);font-family:'JetBrains Mono',monospace}
|
||||
|
||||
/* ── COVER DOTS ── */
|
||||
.cover-meta{display:flex;gap:24px;margin-top:40px;flex-wrap:wrap;justify-content:center}
|
||||
.cover-meta .m{display:flex;flex-direction:column;align-items:center;gap:4px}
|
||||
.cover-meta .m span:first-child{font-size:.68rem;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.3);font-family:'JetBrains Mono',monospace}
|
||||
.cover-meta .m span:last-child{font-size:.9rem;font-weight:600;color:rgba(255,255,255,.8)}
|
||||
|
||||
/* ── SENAE CATEGORY ── */
|
||||
.senae-cats{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;max-width:760px;width:100%}
|
||||
.cat-card{border-radius:10px;padding:16px 18px}
|
||||
.cat-card .cat-name{font-weight:800;font-size:1rem;margin-bottom:6px}
|
||||
.cat-card .cat-rate{font-size:1.5rem;font-weight:900;margin-bottom:6px}
|
||||
.cat-card .cat-desc{font-size:.78rem;opacity:.75;line-height:1.5}
|
||||
|
||||
/* ── NONFUNC ── */
|
||||
.nf-list{display:flex;flex-direction:column;gap:10px;max-width:720px;width:100%}
|
||||
.nf-item{display:flex;align-items:center;gap:14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);border-radius:10px;padding:12px 16px}
|
||||
.nf-key{font-size:.78rem;font-weight:700;color:#7AADFF;font-family:'JetBrains Mono',monospace;min-width:150px}
|
||||
.nf-val{font-size:.82rem;color:rgba(255,255,255,.7)}
|
||||
|
||||
/* ── COMPANY SIDE BY SIDE ── */
|
||||
.companies{display:flex;gap:20px;flex-wrap:wrap;justify-content:center;margin-top:20px}
|
||||
.co-card{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:14px;padding:24px 28px;flex:1;min-width:260px;max-width:340px}
|
||||
.co-flag{font-size:2.8rem;margin-bottom:12px}
|
||||
.co-name{font-size:1rem;font-weight:800;margin-bottom:6px}
|
||||
.co-detail{font-size:.8rem;color:rgba(255,255,255,.5);line-height:1.6}
|
||||
.co-badge{display:inline-block;margin-top:10px;background:rgba(0,87,255,.2);color:#7AADFF;font-size:.7rem;font-weight:700;padding:3px 12px;border-radius:999px}
|
||||
|
||||
/* ── ENDING ── */
|
||||
.ending-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;max-width:780px;margin-top:28px}
|
||||
.ending-item{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:20px 16px;text-align:center}
|
||||
.ending-item span{font-size:1.6rem;display:block;margin-bottom:10px}
|
||||
.ending-item p{font-size:.8rem;color:rgba(255,255,255,.55);line-height:1.5}
|
||||
|
||||
@media(max-width:800px){
|
||||
.s-wrap{padding:48px 28px}
|
||||
.g2,.g3,.g4{grid-template-columns:1fr}
|
||||
.iso-grid{grid-template-columns:1fr}
|
||||
.stbl{font-size:.7rem}
|
||||
h2.stitle{font-size:1.6rem}
|
||||
h1.hero-title{font-size:2.2rem}
|
||||
.slide-num,.slide-section{display:none}
|
||||
.int-grid{grid-template-columns:1fr 1fr}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="progress"></div>
|
||||
<button class="nav-btn" id="btn-prev" onclick="prev()">‹</button>
|
||||
<button class="nav-btn" id="btn-next" onclick="next()">›</button>
|
||||
<div id="counter">1 / 20</div>
|
||||
<div id="hint">← → para navegar | ESC para overview</div>
|
||||
|
||||
<div class="deck" id="deck">
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 01 — PORTADA ═══════════════════════════ -->
|
||||
<div class="slide slide-hero active" id="s1">
|
||||
<div class="s-wrap" style="text-align:center;">
|
||||
<div class="tag">Documento de Requerimientos v1.1</div>
|
||||
<h1 class="hero-title" style="max-width:800px">
|
||||
<em>Moraworld</em> Imports<br/>Sistema de Casillero Internacional
|
||||
</h1>
|
||||
<p class="sub" style="text-align:center;margin-top:20px">
|
||||
Arquitectura de Software · Requerimientos Funcionales · Flujos de Usuario y Sistema
|
||||
</p>
|
||||
<div class="cover-meta">
|
||||
<div class="m"><span>Fecha</span><span>6 de mayo de 2026</span></div>
|
||||
<div class="m"><span>Versión</span><span>1.1</span></div>
|
||||
<div class="m"><span>Estado</span><span>Borrador</span></div>
|
||||
<div class="m"><span>Normativa</span><span>ISO/IEC 27001 · SENAE</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 02 — EMPRESAS ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s2">
|
||||
<div class="slide-num">02 / 20</div><div class="slide-section">Identidad</div>
|
||||
<div class="s-wrap" style="text-align:center;">
|
||||
<div class="tag">Las empresas</div>
|
||||
<h2 class="stitle">Dos empresas, <em>un ecosistema</em></h2>
|
||||
<p class="sub" style="text-align:center">Operación transfronteriza legalmente constituida en Ecuador y EE.UU.</p>
|
||||
<div class="companies">
|
||||
<div class="co-card">
|
||||
<div class="co-flag">🇪🇨</div>
|
||||
<div class="co-name">Moraworld Imports S.A.S.</div>
|
||||
<div class="co-detail">Constituida bajo las leyes de <strong>Ecuador</strong>.<br/>Representante legal en <strong>Cuenca</strong>.<br/>RUC activo · Permisos SENAE vigentes.</div>
|
||||
<span class="co-badge">Ecuador · S.A.S. · SENAE</span>
|
||||
</div>
|
||||
<div class="co-card">
|
||||
<div class="co-flag">🇺🇸</div>
|
||||
<div class="co-name">Mora Global Import LLC</div>
|
||||
<div class="co-detail">Registrada en el estado de <strong>New Jersey, EE.UU.</strong><br/>Bodega operativa en:<br/><strong>150 N Day St, City of Orange, NJ 07050</strong></div>
|
||||
<span class="co-badge">New Jersey · LLC · NJ 07050</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 03 — RESUMEN EJECUTIVO ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s3">
|
||||
<div class="slide-num">03 / 20</div><div class="slide-section">Resumen Ejecutivo</div>
|
||||
<div class="s-wrap">
|
||||
<div class="tag">¿Qué construimos?</div>
|
||||
<h2 class="stitle" style="text-align:center">El problema que resolvemos</h2>
|
||||
<div class="g2" style="margin-top:32px;max-width:900px">
|
||||
<div class="card accent">
|
||||
<div class="card-icon">🏭</div>
|
||||
<h3>La bodega en EE.UU.</h3>
|
||||
<p>Bodega física en <strong>150 N Day St, NJ 07050</strong>. Cada cliente recibe una Suite personalizada como dirección de entrega en tiendas online.</p>
|
||||
<ul>
|
||||
<li>Dirección Suite: EC-XXXXX</li>
|
||||
<li>Asignación automática al registrarse</li>
|
||||
<li>Verificación física de peso y dimensiones</li>
|
||||
<li>Fotos del paquete sincronizadas al portal</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card orange">
|
||||
<div class="card-icon">🛃</div>
|
||||
<h3>La logística a Ecuador</h3>
|
||||
<p>El sistema gestiona el reenvío, el cálculo de impuestos y la declaración automática ante la <strong>SENAE</strong>.</p>
|
||||
<ul>
|
||||
<li>Calculadora SENAE integrada (categorías 4x4, B, C, D)</li>
|
||||
<li>Declaración Simplificada (DSI) automática</li>
|
||||
<li>Tracking ID único por paquete</li>
|
||||
<li>Notificaciones por WhatsApp, SMS y correo</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="g2" style="margin-top:16px;max-width:900px">
|
||||
<div class="card" style="text-align:center;padding:20px">
|
||||
<div style="font-size:2rem;font-weight:900;color:var(--blue)">B2C</div>
|
||||
<div style="font-size:.85rem;font-weight:700;margin-top:4px">Casillero Personal</div>
|
||||
<div style="font-size:.78rem;color:rgba(255,255,255,.45);margin-top:4px">Compras pequeñas · Flujo automatizado</div>
|
||||
</div>
|
||||
<div class="card" style="text-align:center;padding:20px">
|
||||
<div style="font-size:2rem;font-weight:900;color:var(--orange)">B2B</div>
|
||||
<div style="font-size:.85rem;font-weight:700;margin-top:4px">Carga Pesada / Pallets</div>
|
||||
<div style="font-size:.78rem;color:rgba(255,255,255,.45);margin-top:4px">Importación mayorista · Cotización especial</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 04 — ARQUITECTURA ═══════════════════════════ -->
|
||||
<div class="slide slide-blue" id="s4">
|
||||
<div class="slide-num">04 / 20</div><div class="slide-section">Arquitectura</div>
|
||||
<div class="s-wrap" style="text-align:center;">
|
||||
<div class="tag">Arquitectura de Software</div>
|
||||
<h2 class="stitle">Multi-Tenant · ISO/IEC <em>27001</em></h2>
|
||||
<div class="arch" style="margin-top:28px">
|
||||
<div class="arch-box internet" style="width:320px">🌐 INTERNET (HTTPS / TLS 1.3)</div>
|
||||
<div class="arch-arrow">▼</div>
|
||||
<div class="arch-box gateway" style="width:260px">⚡ Load Balancer / API Gateway</div>
|
||||
<div class="arch-arrow">▼</div>
|
||||
<div class="arch-row">
|
||||
<div class="arch-box tenant">Tenant A<br/><span style="font-size:.7rem;opacity:.6">Empresa 1</span></div>
|
||||
<div class="arch-box tenant">Tenant B<br/><span style="font-size:.7rem;opacity:.6">Empresa 2</span></div>
|
||||
<div class="arch-box tenant">Tenant N<br/><span style="font-size:.7rem;opacity:.6">Empresa N</span></div>
|
||||
</div>
|
||||
<div class="arch-arrow">▼</div>
|
||||
<div class="arch-box db" style="width:300px">🗄️ PostgreSQL · Schema aislado por Tenant</div>
|
||||
</div>
|
||||
<div class="g3" style="margin-top:24px;max-width:860px">
|
||||
<div class="card" style="padding:14px 16px;text-align:center">
|
||||
<div style="font-size:.7rem;font-weight:700;color:#7AADFF;letter-spacing:.08em;text-transform:uppercase;margin-bottom:6px">Frontend</div>
|
||||
<div style="font-size:.85rem;font-weight:600">Next.js / React</div>
|
||||
<div style="font-size:.72rem;color:rgba(255,255,255,.4)">SSR · SEO optimizado</div>
|
||||
</div>
|
||||
<div class="card" style="padding:14px 16px;text-align:center">
|
||||
<div style="font-size:.7rem;font-weight:700;color:#7AADFF;letter-spacing:.08em;text-transform:uppercase;margin-bottom:6px">Backend</div>
|
||||
<div style="font-size:.85rem;font-weight:600">Node.js + NestJS</div>
|
||||
<div style="font-size:.72rem;color:rgba(255,255,255,.4)">REST / GraphQL</div>
|
||||
</div>
|
||||
<div class="card" style="padding:14px 16px;text-align:center">
|
||||
<div style="font-size:.7rem;font-weight:700;color:#7AADFF;letter-spacing:.08em;text-transform:uppercase;margin-bottom:6px">Seguridad</div>
|
||||
<div style="font-size:.85rem;font-weight:600">OAuth 2.0 + MFA</div>
|
||||
<div style="font-size:.72rem;color:rgba(255,255,255,.4)">JWT · AES-256 · WAF</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 05 — ISO 27001 ═══════════════════════════ -->
|
||||
<div class="slide slide-blue" id="s5">
|
||||
<div class="slide-num">05 / 20</div><div class="slide-section">Seguridad</div>
|
||||
<div class="s-wrap top">
|
||||
<div class="tag">Seguridad de la Información</div>
|
||||
<h2 class="stitle" style="text-align:center">Controles <em>ISO/IEC 27001:2022</em></h2>
|
||||
<div class="iso-grid" style="margin-top:24px">
|
||||
<div class="iso-item"><span class="iso-label">A.5 Políticas</span><span class="iso-desc">Política de uso aceptable, manejo de datos y retención</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.9 Acceso</span><span class="iso-desc">Autenticación MFA obligatoria, RBAC por roles</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.6 Organización</span><span class="iso-desc">Roles definidos, segregación de funciones estricta</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.10 Criptografía</span><span class="iso-desc">TLS 1.3 en tránsito · AES-256 en reposo</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.8 Activos</span><span class="iso-desc">Inventario de datos personales y de paquetes (LOPDP)</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.12 Operaciones</span><span class="iso-desc">Logs de auditoría inmutables, monitoreo continuo (SIEM)</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.14 Desarrollo</span><span class="iso-desc">SDLC seguro, revisiones de código, OWASP Top 10</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.16 Incidentes</span><span class="iso-desc">Proceso de gestión y notificación de incidentes</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.7 RRHH</span><span class="iso-desc">Procedimientos de onboarding/offboarding de usuarios</span></div>
|
||||
<div class="iso-item"><span class="iso-label">A.18 Cumplimiento</span><span class="iso-desc">Declaración aduanera automática conforme a SENAE</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 06 — ROLES ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s6">
|
||||
<div class="slide-num">06 / 20</div><div class="slide-section">Roles</div>
|
||||
<div class="s-wrap" style="text-align:center;">
|
||||
<div class="tag">Sistema de Roles</div>
|
||||
<h2 class="stitle">6 roles, acceso <em>diferenciado</em></h2>
|
||||
<div class="role-grid" style="margin-top:24px">
|
||||
<div class="role-card" style="border-color:rgba(0,87,255,.4);background:rgba(0,87,255,.1)">
|
||||
<div class="role-icon">👑</div>
|
||||
<div class="role-name">Super Admin</div>
|
||||
<div class="role-desc">Acceso total. Gestiona tenants, usuarios y configuración global</div>
|
||||
</div>
|
||||
<div class="role-card" style="border-color:rgba(139,92,246,.4);background:rgba(139,92,246,.1)">
|
||||
<div class="role-icon">🏢</div>
|
||||
<div class="role-name">Admin Empresa</div>
|
||||
<div class="role-desc">Administra su tenant: usuarios, tarifas, bodegas, reportes</div>
|
||||
</div>
|
||||
<div class="role-card" style="border-color:rgba(245,158,11,.4);background:rgba(245,158,11,.1)">
|
||||
<div class="role-icon">📦</div>
|
||||
<div class="role-name">Operador Bodega</div>
|
||||
<div class="role-desc">Verifica, pesa, fotografía y despacha paquetes en NJ</div>
|
||||
</div>
|
||||
<div class="role-card" style="border-color:rgba(16,185,129,.4);background:rgba(16,185,129,.1)">
|
||||
<div class="role-icon">🛃</div>
|
||||
<div class="role-name">Agente Aduanero</div>
|
||||
<div class="role-desc">Revisa y aprueba declaraciones antes de enviar a la SENAE</div>
|
||||
</div>
|
||||
<div class="role-card" style="border-color:rgba(255,107,0,.4);background:rgba(255,107,0,.1)">
|
||||
<div class="role-icon">🧑💻</div>
|
||||
<div class="role-name">Cliente Final</div>
|
||||
<div class="role-desc">Usuario ecuatoriano. Registra compras y da seguimiento</div>
|
||||
</div>
|
||||
<div class="role-card" style="border-color:rgba(255,255,255,.15)">
|
||||
<div class="role-icon">🎧</div>
|
||||
<div class="role-name">Soporte</div>
|
||||
<div class="role-desc">Visualiza info de clientes y paquetes. Sin modificaciones</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 07 — MATRIZ PERMISOS ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s7">
|
||||
<div class="slide-num">07 / 20</div><div class="slide-section">Permisos</div>
|
||||
<div class="s-wrap top">
|
||||
<div class="tag">Matriz de Acceso</div>
|
||||
<h2 class="stitle" style="text-align:center">¿Quién puede hacer <em>qué</em>?</h2>
|
||||
<div style="overflow-x:auto;width:100%;max-width:960px;margin-top:20px">
|
||||
<table class="stbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Acción</th>
|
||||
<th>Super Admin</th>
|
||||
<th>Admin</th>
|
||||
<th>Bodega</th>
|
||||
<th>Aduanero</th>
|
||||
<th>Cliente</th>
|
||||
<th>Soporte</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Ver dashboard</td><td class="yes">✅</td><td class="yes">✅</td><td class="yes">✅</td><td class="yes">✅</td><td class="yes">✅</td><td class="yes">✅</td></tr>
|
||||
<tr><td>Crear / editar usuarios</td><td class="yes">✅</td><td class="yes">✅</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td></tr>
|
||||
<tr><td>Configurar tarifas</td><td class="yes">✅</td><td class="yes">✅</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td></tr>
|
||||
<tr><td>Registrar compra</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="yes">✅</td><td class="no">❌</td></tr>
|
||||
<tr><td>Ver mis paquetes</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="yes">✅</td><td class="yes">✅</td></tr>
|
||||
<tr><td>Verificar en bodega</td><td class="no">❌</td><td class="no">❌</td><td class="yes">✅</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td></tr>
|
||||
<tr><td>Confirmar peso/dims.</td><td class="no">❌</td><td class="no">❌</td><td class="yes">✅</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td></tr>
|
||||
<tr><td>Generar DSI (aduana)</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="yes">✅</td><td class="no">❌</td><td class="no">❌</td></tr>
|
||||
<tr><td>Enviar datos a SENAE</td><td class="yes">✅</td><td class="yes">✅</td><td class="no">❌</td><td class="yes">✅</td><td class="no">❌</td><td class="no">❌</td></tr>
|
||||
<tr><td>Logs de auditoría</td><td class="yes">✅</td><td class="yes">✅</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td><td class="no">❌</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 08 — MÓDULOS ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s8">
|
||||
<div class="slide-num">08 / 20</div><div class="slide-section">Módulos</div>
|
||||
<div class="s-wrap top">
|
||||
<div class="tag">Módulos del Sistema</div>
|
||||
<h2 class="stitle" style="text-align:center">8 módulos <em>integrados</em></h2>
|
||||
<div class="g4" style="margin-top:24px">
|
||||
<div class="card" style="padding:16px">
|
||||
<div class="card-icon">🌐</div>
|
||||
<h3 style="font-size:.9rem">Landing Page</h3>
|
||||
<p>Pública, SEO, calculadora, WhatsApp flotante, legalidad</p>
|
||||
<span class="badge bg-green-b">Público</span>
|
||||
</div>
|
||||
<div class="card accent" style="padding:16px">
|
||||
<div class="card-icon">👤</div>
|
||||
<h3 style="font-size:.9rem">Portal Cliente</h3>
|
||||
<p>Casillero Suite, pre-alerta, registro por URL o manual, fotos</p>
|
||||
<span class="badge bg-blue-b">Cliente</span>
|
||||
</div>
|
||||
<div class="card orange" style="padding:16px">
|
||||
<div class="card-icon">🏭</div>
|
||||
<h3 style="font-size:.9rem">Portal Bodega NJ</h3>
|
||||
<p>Recepción, verificación, peso real, fotos, despacho</p>
|
||||
<span class="badge bg-orange-b">Operador</span>
|
||||
</div>
|
||||
<div class="card purple" style="padding:16px">
|
||||
<div class="card-icon">⚙️</div>
|
||||
<h3 style="font-size:.9rem">Portal Admin</h3>
|
||||
<p>Dashboard, métricas, tarifas, reportes, auditoría</p>
|
||||
<span class="badge bg-purple-b">Admin</span>
|
||||
</div>
|
||||
<div class="card green" style="padding:16px">
|
||||
<div class="card-icon">🛃</div>
|
||||
<h3 style="font-size:.9rem">Módulo SENAE</h3>
|
||||
<p>DSI automática, cálculo impuestos, WebService oficial</p>
|
||||
<span class="badge bg-green-b">Aduanero</span>
|
||||
</div>
|
||||
<div class="card" style="padding:16px">
|
||||
<div class="card-icon">📍</div>
|
||||
<h3 style="font-size:.9rem">Tracking</h3>
|
||||
<p>ID único EC-YYYYMMDD-XXXXXX, 11 estados, visible sin login</p>
|
||||
<span class="badge bg-blue-b">Todos</span>
|
||||
</div>
|
||||
<div class="card" style="padding:16px">
|
||||
<div class="card-icon">📄</div>
|
||||
<h3 style="font-size:.9rem">Pre-alerta</h3>
|
||||
<p>Aviso previo + carga de factura PDF/imagen + tracking vendedor</p>
|
||||
<span class="badge bg-blue-b">Cliente</span>
|
||||
</div>
|
||||
<div class="card orange" style="padding:16px">
|
||||
<div class="card-icon">🚢</div>
|
||||
<h3 style="font-size:.9rem">Carga Pesada B2B</h3>
|
||||
<p>Pallets, INEN, cotización especial, DAI</p>
|
||||
<span class="badge bg-orange-b">Mayorista</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 09 — ESTADOS TRACKING ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s9">
|
||||
<div class="slide-num">09 / 20</div><div class="slide-section">Tracking</div>
|
||||
<div class="s-wrap" style="text-align:center;">
|
||||
<div class="tag">Sistema de Tracking</div>
|
||||
<h2 class="stitle">11 estados · ID único por paquete</h2>
|
||||
<p class="sub" style="text-align:center;margin-bottom:24px">Formato: <span style="font-family:'JetBrains Mono',monospace;color:#7AADFF">EC-YYYYMMDD-XXXXXX</span> | Ej: EC-20260506-082341</p>
|
||||
<div class="timeline">
|
||||
<div class="t-state"><div class="t-dot" style="background:#6B7280"></div>REGISTRADO<div class="t-num">01</div></div>
|
||||
<div class="t-state"><div class="t-dot" style="background:#F59E0B"></div>EN_TRANSITO_BODEGA<div class="t-num">02</div></div>
|
||||
<div class="t-state"><div class="t-dot" style="background:#3B82F6"></div>RECIBIDO_BODEGA<div class="t-num">03</div></div>
|
||||
<div class="t-state"><div class="t-dot" style="background:#8B5CF6"></div>EN_VERIFICACION<div class="t-num">04</div></div>
|
||||
<div class="t-state" style="border-color:rgba(16,185,129,.4)"><div class="t-dot" style="background:#10B981"></div>VERIFICADO<div class="t-num">05</div></div>
|
||||
<div class="t-state" style="border-color:rgba(0,87,255,.4)"><div class="t-dot" style="background:#0057FF"></div>DECLARACION_ADUANERA<div class="t-num">06</div></div>
|
||||
<div class="t-state"><div class="t-dot" style="background:#F97316"></div>EN_TRANSITO_ECUADOR<div class="t-num">07</div></div>
|
||||
<div class="t-state"><div class="t-dot" style="background:#EF4444"></div>EN_ADUANA_ECUADOR<div class="t-num">08</div></div>
|
||||
<div class="t-state"><div class="t-dot" style="background:#84CC16"></div>LISTO_ENTREGA<div class="t-num">09</div></div>
|
||||
<div class="t-state" style="border-color:rgba(16,185,129,.5)"><div class="t-dot" style="background:#10B981"></div>ENTREGADO ✓<div class="t-num">10</div></div>
|
||||
<div class="t-state" style="border-color:rgba(239,68,68,.5);background:rgba(239,68,68,.07)"><div class="t-dot" style="background:#EF4444"></div>INCIDENCIA ⚠<div class="t-num">11</div></div>
|
||||
</div>
|
||||
<p style="font-size:.78rem;color:rgba(255,255,255,.3);margin-top:20px">Notificaciones automáticas al cliente en cada cambio de estado · WhatsApp · SMS · Email</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 10 — FLUJO CLIENTE ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s10">
|
||||
<div class="slide-num">10 / 20</div><div class="slide-section">Flujo de Usuario</div>
|
||||
<div class="s-wrap top" style="align-items:center">
|
||||
<div class="tag">Flujo del Cliente Final</div>
|
||||
<h2 class="stitle" style="text-align:center">Del click en Amazon a la <em>puerta en Ecuador</em></h2>
|
||||
<div class="flow" style="margin-top:20px;gap:6px">
|
||||
<div class="flow-step done"><div class="flow-num g">1</div><div class="flow-text">Accede a la Landing Page y se registra (correo + MFA)</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step done"><div class="flow-num g">2</div><div class="flow-text">Sistema asigna automáticamente: <span style="font-family:'JetBrains Mono';color:#7AADFF">150 N Day St, Suite EC-XXXXX, Orange NJ 07050</span></div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">3</div><div class="flow-text">Compra en Amazon/eBay/Walmart usando esa dirección Suite como destino</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step decision"><div class="flow-num y">4</div><div class="flow-text">⚡ PRE-ALERTA (opcional): carga tracking vendedor + factura PDF → La bodega se prepara</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-split">
|
||||
<div class="flow-step active" style="font-size:.78rem"><div class="flow-num" style="font-size:.65rem">5A</div><div class="flow-text">🔗 Pega el enlace de Amazon → Sistema extrae: nombre, precio, peso, imagen</div></div>
|
||||
<div class="flow-step active" style="font-size:.78rem"><div class="flow-num" style="font-size:.65rem">5B</div><div class="flow-text">✏️ Ingreso manual: nombre, valor ($USD), tienda, peso estimado</div></div>
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step"><div class="flow-num o">6</div><div class="flow-text">Sistema calcula costo total con desglose SENAE · Cliente ACEPTA y PAGA en línea</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step done"><div class="flow-num g">7</div><div class="flow-text">🎉 Tracking ID generado · Notificaciones automáticas hasta entrega en Ecuador</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 11 — FLUJO BODEGA ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s11">
|
||||
<div class="slide-num">11 / 20</div><div class="slide-section">Flujo de Usuario</div>
|
||||
<div class="s-wrap" style="align-items:center">
|
||||
<div class="tag">Flujo del Operador de Bodega NJ</div>
|
||||
<h2 class="stitle" style="text-align:center">Verificación y despacho <em>en bodega</em></h2>
|
||||
<div class="flow" style="margin-top:20px;gap:6px;max-width:680px">
|
||||
<div class="flow-step done"><div class="flow-num g">1</div><div class="flow-text">Inicia sesión en el Portal de Bodega · Ve paquetes esperados con pre-alertas</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">2</div><div class="flow-text">Llega paquete físico → Escanea código de barras o busca por tracking manual</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">3</div><div class="flow-text">Confirma recepción → Estado: RECIBIDO_BODEGA · Pesa y mide · Toma fotos</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-split">
|
||||
<div class="flow-step done" style="font-size:.78rem"><div class="flow-num g" style="font-size:.65rem">4A</div><div class="flow-text">✅ Sin discrepancia → Estado: VERIFICADO · Asigna a despacho</div></div>
|
||||
<div class="flow-step decision" style="font-size:.78rem"><div class="flow-num y" style="font-size:.65rem">4B</div><div class="flow-text">⚠️ Discrepancia >10% → Notifica Admin y cliente · Ajuste de cobro</div></div>
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step"><div class="flow-num o">5</div><div class="flow-text">Asigna a envío consolidado o individual · Genera orden de despacho a Ecuador</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 12 — FLUJO ADUANERO ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s12">
|
||||
<div class="slide-num">12 / 20</div><div class="slide-section">Flujo de Usuario</div>
|
||||
<div class="s-wrap" style="align-items:center">
|
||||
<div class="tag">Flujo del Agente Aduanero</div>
|
||||
<h2 class="stitle" style="text-align:center">De la bodega a la <em>SENAE</em></h2>
|
||||
<div class="flow" style="margin-top:20px;gap:6px;max-width:680px">
|
||||
<div class="flow-step active"><div class="flow-num">1</div><div class="flow-text">Ve paquetes con estado VERIFICADO · Revisa descripción, valor declarado y fotos</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-split">
|
||||
<div class="flow-step done" style="font-size:.78rem"><div class="flow-num g" style="font-size:.65rem">2A</div><div class="flow-text">✅ Valor ≤ $400 y ≤ 4 kg → Proceso 4x4 simplificado · DSI automática</div></div>
|
||||
<div class="flow-step decision" style="font-size:.78rem"><div class="flow-num y" style="font-size:.65rem">2B</div><div class="flow-text">⚠️ Supera límite → Notifica cliente · Proceso de importación formal (DAI)</div></div>
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">3</div><div class="flow-text">Sistema genera la <strong>Declaración Simplificada de Importación (DSI)</strong> automáticamente</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">4</div><div class="flow-text">Agente REVISA y APRUEBA · Sistema envía DSI al WebService oficial de la SENAE</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step done"><div class="flow-num g">5</div><div class="flow-text">🎉 SENAE devuelve número de autorización · Estado: DECLARACION_ADUANERA · Notificación al cliente</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 13 — FLUJO IMPORTADOR B2B ═══════════════════════════ -->
|
||||
<div class="slide slide-orange" id="s13">
|
||||
<div class="slide-num">13 / 20</div><div class="slide-section">Flujo B2B</div>
|
||||
<div class="s-wrap" style="align-items:center">
|
||||
<div class="tag">Flujo Importador Mayorista (B2B)</div>
|
||||
<h2 class="stitle" style="text-align:center">Carga Pesada · <em>Pallets · INEN</em></h2>
|
||||
<div class="flow" style="margin-top:20px;gap:6px;max-width:680px">
|
||||
<div class="flow-step active"><div class="flow-num o">1</div><div class="flow-text">Importador accede a la sección "Carga Pesada / Pallets" en la Landing Page</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num o">2</div><div class="flow-text">Llena formulario de cotización especial: tipo de mercancía, peso, pallets, valor</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step decision"><div class="flow-num y">3</div><div class="flow-text">⚠️ Verificación INEN: calzado, textiles, electrónicos requieren certificación previa</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step"><div class="flow-num o">4</div><div class="flow-text">Equipo Moraworld prepara cotización · La envía por correo y WhatsApp</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num o">5</div><div class="flow-text">Importador acepta → Se crea expediente con ID de seguimiento B2B propio</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step done"><div class="flow-num g">6</div><div class="flow-text">🚢 Coordinación de embarque · DAI o DAS · Seguimiento hasta entrega en Ecuador</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 14 — FLUJO SISTEMA URL ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s14">
|
||||
<div class="slide-num">14 / 20</div><div class="slide-section">Flujo del Sistema</div>
|
||||
<div class="s-wrap" style="align-items:center;text-align:center">
|
||||
<div class="tag">Flujo del Sistema</div>
|
||||
<h2 class="stitle">Registro por <em>enlace</em> de Amazon</h2>
|
||||
<div class="flow" style="margin-top:20px;gap:6px;max-width:680px">
|
||||
<div class="flow-step active"><div class="flow-num">1</div><div class="flow-text">Cliente pega URL del producto de Amazon en el portal</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">2</div><div class="flow-text">Sistema llama Amazon SP-API (o scraping) · Extrae: nombre, precio, peso, imagen</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">3</div><div class="flow-text">Pre-llena el formulario automáticamente con los datos extraídos</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">4</div><div class="flow-text">Sistema calcula: flete + seguro 2% + FODINFA 0.5% + IVA 15% + Arancel por categoría</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step decision"><div class="flow-num y">5</div><div class="flow-text">Muestra desglose completo al cliente · Cliente confirma y paga en línea</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step done"><div class="flow-num g">6</div><div class="flow-text">✅ Tracking ID generado · Correo de confirmación · Estado → REGISTRADO</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 15 — CALCULADORA SENAE ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s15">
|
||||
<div class="slide-num">15 / 20</div><div class="slide-section">Calculadora SENAE</div>
|
||||
<div class="s-wrap" style="text-align:center">
|
||||
<div class="tag">Cálculo de Impuestos</div>
|
||||
<h2 class="stitle">Categorías <em>SENAE</em> · Fórmulas</h2>
|
||||
<div class="senae-cats" style="margin-top:20px">
|
||||
<div class="cat-card" style="background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3)">
|
||||
<div class="cat-name" style="color:#6EE7B7">Régimen 4×4</div>
|
||||
<div class="cat-rate" style="color:#10B981">0% Arancel</div>
|
||||
<div class="cat-desc" style="color:rgba(255,255,255,.6)">Hasta 4 envíos/año · máx. $400 · máx. 4 kg · Proceso simplificado automático</div>
|
||||
</div>
|
||||
<div class="cat-card" style="background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3)">
|
||||
<div class="cat-name" style="color:#93C5FD">Categoría B</div>
|
||||
<div class="cat-rate" style="color:#3B82F6">10%</div>
|
||||
<div class="cat-desc" style="color:rgba(255,255,255,.6)">Bienes de consumo general fuera del régimen 4×4 · + FODINFA + IVA</div>
|
||||
</div>
|
||||
<div class="cat-card" style="background:rgba(255,107,0,.1);border:1px solid rgba(255,107,0,.3)">
|
||||
<div class="cat-name" style="color:#FDB87A">Categoría C</div>
|
||||
<div class="cat-rate" style="color:#FF6B00">20%</div>
|
||||
<div class="cat-desc" style="color:rgba(255,255,255,.6)">Textiles, calzado, artículos del hogar · + FODINFA + IVA</div>
|
||||
</div>
|
||||
<div class="cat-card" style="background:rgba(139,92,246,.1);border:1px solid rgba(139,92,246,.3)">
|
||||
<div class="cat-name" style="color:#C4B5FD">Categoría D</div>
|
||||
<div class="cat-rate" style="color:#8B5CF6">0–15%</div>
|
||||
<div class="cat-desc" style="color:rgba(255,255,255,.6)">Electrónicos, telecomunicaciones · Puede requerir certificación INEN</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formula" style="margin-top:20px;text-align:left">
|
||||
<span class="comment">// Peso a cobrar</span>
|
||||
<span class="result">Peso_final</span> = MAX(peso_real, <span class="value">(largo × ancho × alto) / 139</span>)
|
||||
|
||||
<span class="comment">// Costos</span>
|
||||
Flete = Peso_final × <span class="value">$3.50/lb</span>
|
||||
Seguro = valor_declarado × <span class="value">0.02</span>
|
||||
FODINFA = valor_declarado × <span class="value">0.005</span>
|
||||
Arancel = valor_declarado × <span class="value">% categoría</span>
|
||||
IVA = (valor + FODINFA + Arancel) × <span class="value">0.15</span>
|
||||
|
||||
<span class="result">TOTAL</span> = Flete + Seguro + FODINFA + Arancel + IVA</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 16 — FLUJO NOTIFICACIONES ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s16">
|
||||
<div class="slide-num">16 / 20</div><div class="slide-section">Flujo del Sistema</div>
|
||||
<div class="s-wrap" style="align-items:center">
|
||||
<div class="tag">Flujo de Notificaciones</div>
|
||||
<h2 class="stitle" style="text-align:center">Notificaciones <em>automáticas</em> multicanal</h2>
|
||||
<div class="g2" style="margin-top:32px;max-width:800px;align-items:start">
|
||||
<div class="flow" style="gap:6px">
|
||||
<div class="flow-step active"><div class="flow-num">1</div><div class="flow-text">Cambio de estado del paquete en base de datos</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step active"><div class="flow-num">2</div><div class="flow-text">Sistema detecta el evento y activa el motor de notificaciones</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step done"><div class="flow-num g">3</div><div class="flow-text">Envío paralelo a todos los canales configurados del usuario</div></div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step done"><div class="flow-num g">4</div><div class="flow-text">Log de auditoría inmutable registra el evento con timestamp UTC</div></div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px">
|
||||
<div class="card" style="padding:14px 18px;display:flex;gap:12px;align-items:center">
|
||||
<span style="font-size:1.4rem">✉️</span>
|
||||
<div><strong style="font-size:.85rem;display:block">Correo electrónico</strong><span style="font-size:.75rem;color:rgba(255,255,255,.45)">Detalle completo del estado · SendGrid / SES</span></div>
|
||||
</div>
|
||||
<div class="card" style="padding:14px 18px;display:flex;gap:12px;align-items:center">
|
||||
<span style="font-size:1.4rem">💬</span>
|
||||
<div><strong style="font-size:.85rem;display:block">WhatsApp Business API</strong><span style="font-size:.75rem;color:rgba(255,255,255,.45)">Mensaje directo al número del cliente</span></div>
|
||||
</div>
|
||||
<div class="card" style="padding:14px 18px;display:flex;gap:12px;align-items:center">
|
||||
<span style="font-size:1.4rem">📱</span>
|
||||
<div><strong style="font-size:.85rem;display:block">SMS</strong><span style="font-size:.75rem;color:rgba(255,255,255,.45)">Resumen corto · SMS Gateway API</span></div>
|
||||
</div>
|
||||
<div class="card" style="padding:14px 18px;display:flex;gap:12px;align-items:center">
|
||||
<span style="font-size:1.4rem">🔔</span>
|
||||
<div><strong style="font-size:.85rem;display:block">Push (portal web)</strong><span style="font-size:.75rem;color:rgba(255,255,255,.45)">Notificación en tiempo real en el dashboard</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 17 — INTEGRACIONES ═══════════════════════════ -->
|
||||
<div class="slide slide-blue" id="s17">
|
||||
<div class="slide-num">17 / 20</div><div class="slide-section">Integraciones</div>
|
||||
<div class="s-wrap" style="text-align:center">
|
||||
<div class="tag">Integraciones Externas</div>
|
||||
<h2 class="stitle">9 integraciones <em>clave</em></h2>
|
||||
<div class="int-grid" style="margin-top:28px">
|
||||
<div class="int-item">
|
||||
<span class="int-icon">🛒</span>
|
||||
<div><span class="int-name">Amazon SP-API</span><span class="int-proto">REST API · Datos del producto</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">🛃</span>
|
||||
<div><span class="int-name">WebService SENAE</span><span class="int-proto">SOAP / REST · Declaraciones DSI/DAI</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">💳</span>
|
||||
<div><span class="int-name">Pasarela de Pagos</span><span class="int-proto">REST API · Stripe / PayPhone</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">💬</span>
|
||||
<div><span class="int-name">WhatsApp Business</span><span class="int-proto">REST API · Notificaciones + Soporte</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">✉️</span>
|
||||
<div><span class="int-name">Email (SendGrid/SES)</span><span class="int-proto">SMTP / REST · Notificaciones</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">📱</span>
|
||||
<div><span class="int-name">SMS Gateway</span><span class="int-proto">REST API · Notificaciones cortas</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">🚚</span>
|
||||
<div><span class="int-name">Courier (FedEx/DHL)</span><span class="int-proto">REST API · Estado físico del envío</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">🏭</span>
|
||||
<div><span class="int-name">Software de Bodega</span><span class="int-proto">REST API · Sync estados en tiempo real</span></div>
|
||||
</div>
|
||||
<div class="int-item">
|
||||
<span class="int-icon">🏷️</span>
|
||||
<div><span class="int-name">Normas INEN</span><span class="int-proto">REST / Web · Certificaciones</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 18 — REQS NO FUNCIONALES ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s18">
|
||||
<div class="slide-num">18 / 20</div><div class="slide-section">Calidad</div>
|
||||
<div class="s-wrap" style="text-align:center">
|
||||
<div class="tag">Requerimientos No Funcionales</div>
|
||||
<h2 class="stitle">Calidad y <em>Rendimiento</em></h2>
|
||||
<div class="nf-list" style="margin-top:24px">
|
||||
<div class="nf-item"><span class="nf-key">Disponibilidad</span><span class="nf-val">99.9% uptime (SLA garantizado)</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Tiempo de respuesta</span><span class="nf-val">{"<"} 2 segundos para operaciones normales</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Escalabilidad</span><span class="nf-val">Multi-tenant · preparado para múltiples empresas y miles de usuarios</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Seguridad</span><span class="nf-val">ISO/IEC 27001 · OWASP Top 10 · TLS 1.3 · AES-256 · MFA obligatorio · WAF</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Accesibilidad</span><span class="nf-val">100% web · responsive mobile-first · accesible desde cualquier dispositivo</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Respaldo</span><span class="nf-val">Backups automáticos diarios · retención mínima 90 días</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Recuperación</span><span class="nf-val">RTO {"<"} 4 horas · RPO {"<"} 1 hora</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Auditoría</span><span class="nf-val">Todos los eventos registrados con trazabilidad completa e inmutable</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Multi-idioma</span><span class="nf-val">Español (clientes Ecuador) · Inglés (operadores NJ)</span></div>
|
||||
<div class="nf-item"><span class="nf-key">Zona horaria</span><span class="nf-val">EST (New Jersey) y ECT (Ecuador) — mostrado según ubicación del usuario</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 19 — NAVEGACIÓN WEB ═══════════════════════════ -->
|
||||
<div class="slide slide-dark" id="s19">
|
||||
<div class="slide-num">19 / 20</div><div class="slide-section">Estructura Web</div>
|
||||
<div class="s-wrap" style="text-align:center">
|
||||
<div class="tag">Navegación del Sitio</div>
|
||||
<h2 class="stitle">Estructura de <em>rutas</em></h2>
|
||||
<div class="g2" style="margin-top:28px;max-width:860px;align-items:start">
|
||||
<div>
|
||||
<p style="font-size:.78rem;font-weight:700;color:rgba(255,255,255,.35);margin-bottom:12px;letter-spacing:.08em;text-transform:uppercase">Área Pública</p>
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:.78rem;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:18px 20px;line-height:2.2;color:rgba(255,255,255,.7)">
|
||||
<span style="color:#7AADFF">/ </span>Landing Page principal<br/>
|
||||
<span style="color:#7AADFF">/como-funciona</span><br/>
|
||||
<span style="color:#7AADFF">/calculadora</span><br/>
|
||||
<span style="color:#7AADFF">/tarifas</span><br/>
|
||||
<span style="color:#7AADFF">/quienes-somos</span><br/>
|
||||
<span style="color:#FDB87A">/casillero/</span>como-usar · calculadora · registro<br/>
|
||||
<span style="color:#FF6B00">/carga-pesada/</span>como-funciona · cotizacion · inen<br/>
|
||||
<span style="color:#6EE7B7">/tracking</span> <span style="color:rgba(255,255,255,.3)">← sin login</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p style="font-size:.78rem;font-weight:700;color:rgba(255,255,255,.35);margin-bottom:12px;letter-spacing:.08em;text-transform:uppercase">Área Autenticada</p>
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:.78rem;background:rgba(0,0,0,.4);border:1px solid rgba(0,87,255,.3);border-radius:10px;padding:18px 20px;line-height:2.2;color:rgba(255,255,255,.7)">
|
||||
<span style="color:#7AADFF">/portal/dashboard</span><br/>
|
||||
<span style="color:#7AADFF">/portal/mi-casillero</span> 📦<br/>
|
||||
<span style="color:#7AADFF">/portal/pre-alerta</span> 📄<br/>
|
||||
<span style="color:#7AADFF">/portal/mis-paquetes</span> 🔍<br/>
|
||||
<span style="color:#7AADFF">/portal/calculadora</span> 💰<br/>
|
||||
<span style="color:#7AADFF">/portal/perfil</span><br/>
|
||||
<span style="color:#C4B5FD">/admin/</span>usuarios · tarifas · reportes<br/>
|
||||
<span style="color:#FDE68A">/bodega/</span>paquetes · verificacion
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════ SLIDE 20 — CIERRE ═══════════════════════════ -->
|
||||
<div class="slide slide-hero" id="s20">
|
||||
<div class="slide-num">20 / 20</div>
|
||||
<div class="s-wrap" style="text-align:center">
|
||||
<div class="tag">Resumen</div>
|
||||
<h1 class="hero-title" style="font-size:clamp(2rem,5vw,3.5rem);max-width:700px">
|
||||
Un sistema completo, seguro y <em>escalable</em>
|
||||
</h1>
|
||||
<p class="sub" style="text-align:center;margin-top:16px">
|
||||
Diseñado para crecer con Moraworld Imports desde el primer día.
|
||||
</p>
|
||||
<div class="ending-grid">
|
||||
<div class="ending-item"><span>🏗️</span><p>Arquitectura Multi-Tenant ISO 27001</p></div>
|
||||
<div class="ending-item"><span>👥</span><p>6 roles con permisos granulares</p></div>
|
||||
<div class="ending-item"><span>📦</span><p>8 módulos integrados</p></div>
|
||||
<div class="ending-item"><span>🔁</span><p>5 flujos de usuario documentados</p></div>
|
||||
<div class="ending-item"><span>🛃</span><p>Integración directa con SENAE</p></div>
|
||||
<div class="ending-item"><span>📍</span><p>11 estados de tracking en tiempo real</p></div>
|
||||
<div class="ending-item"><span>🔗</span><p>9 integraciones externas</p></div>
|
||||
<div class="ending-item"><span>💰</span><p>Calculadora SENAE (4x4, B, C, D)</p></div>
|
||||
<div class="ending-item"><span>🌐</span><p>100% web · mobile-first · responsive</p></div>
|
||||
</div>
|
||||
<div style="margin-top:32px;display:flex;gap:12px;flex-wrap:wrap;justify-content:center">
|
||||
<span style="background:rgba(0,87,255,.2);border:1px solid rgba(0,87,255,.4);color:#7AADFF;font-size:.75rem;font-weight:700;padding:6px 16px;border-radius:999px;">ISO/IEC 27001</span>
|
||||
<span style="background:rgba(16,185,129,.15);border:1px solid rgba(16,185,129,.3);color:#6EE7B7;font-size:.75rem;font-weight:700;padding:6px 16px;border-radius:999px;">SENAE Autorizado</span>
|
||||
<span style="background:rgba(255,107,0,.15);border:1px solid rgba(255,107,0,.3);color:#FDB87A;font-size:.75rem;font-weight:700;padding:6px 16px;border-radius:999px;">Cumplimiento INEN</span>
|
||||
<span style="background:rgba(139,92,246,.15);border:1px solid rgba(139,92,246,.3);color:#C4B5FD;font-size:.75rem;font-weight:700;padding:6px 16px;border-radius:999px;">LOPDP Ecuador</span>
|
||||
<span style="background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.7);font-size:.75rem;font-weight:700;padding:6px 16px;border-radius:999px;">Multi-Tenant</span>
|
||||
</div>
|
||||
<p style="margin-top:28px;font-size:.75rem;color:rgba(255,255,255,.2);font-family:'JetBrains Mono',monospace">
|
||||
Moraworld Imports S.A.S. · Mora Global Import LLC · v1.1 · Mayo 2026
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /deck -->
|
||||
|
||||
<script>
|
||||
const TOTAL = 20;
|
||||
let cur = 1;
|
||||
|
||||
function go(n){
|
||||
const prev = document.getElementById('s'+cur);
|
||||
if(prev) prev.classList.add('exit');
|
||||
setTimeout(()=>{ if(prev){ prev.classList.remove('active','exit'); }}, 480);
|
||||
|
||||
cur = Math.max(1, Math.min(n, TOTAL));
|
||||
const next = document.getElementById('s'+cur);
|
||||
if(next){ next.classList.add('active'); }
|
||||
|
||||
document.getElementById('progress').style.width = ((cur/TOTAL)*100)+'%';
|
||||
document.getElementById('counter').textContent = cur + ' / ' + TOTAL;
|
||||
document.getElementById('btn-prev').style.opacity = cur===1 ? '.2':'1';
|
||||
document.getElementById('btn-prev').style.pointerEvents = cur===1 ? 'none':'all';
|
||||
document.getElementById('btn-next').style.opacity = cur===TOTAL ? '.2':'1';
|
||||
document.getElementById('btn-next').style.pointerEvents = cur===TOTAL ? 'none':'all';
|
||||
}
|
||||
|
||||
function next(){ go(cur+1); }
|
||||
function prev(){ go(cur-1); }
|
||||
|
||||
document.addEventListener('keydown', e=>{
|
||||
if(e.key==='ArrowRight'||e.key==='ArrowDown'||e.key===' ') { e.preventDefault(); next(); }
|
||||
if(e.key==='ArrowLeft'||e.key==='ArrowUp') { e.preventDefault(); prev(); }
|
||||
if(e.key==='Home') go(1);
|
||||
if(e.key==='End') go(TOTAL);
|
||||
});
|
||||
|
||||
// Touch swipe
|
||||
let tx=0;
|
||||
document.addEventListener('touchstart',e=>{ tx=e.touches[0].clientX; },{passive:true});
|
||||
document.addEventListener('touchend',e=>{
|
||||
const dx=tx-e.changedTouches[0].clientX;
|
||||
if(Math.abs(dx)>50){ dx>0?next():prev(); }
|
||||
},{passive:true});
|
||||
|
||||
// Init
|
||||
go(1);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Crear cuenta — Moraworld Imports</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--primary:#0057FF;--primary-d:#003FC7;--accent:#FF6B00;
|
||||
--dark:#0D1117;--gray-900:#111827;--gray-700:#374151;
|
||||
--gray-500:#6B7280;--gray-200:#E5E7EB;--gray-100:#F3F4F6;
|
||||
--white:#fff;--green:#10B981;--red:#EF4444;
|
||||
}
|
||||
body{font-family:'Inter',sans-serif;background:var(--gray-100);min-height:100vh;display:flex;flex-direction:column}
|
||||
a{text-decoration:none;color:inherit}
|
||||
|
||||
nav{background:rgba(255,255,255,.95);backdrop-filter:blur(12px);border-bottom:1px solid var(--gray-200);padding:0 24px;height:64px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
||||
.nav-logo{font-size:1.2rem;font-weight:800;color:var(--primary)}
|
||||
.nav-logo span{color:var(--accent)}
|
||||
.nav-back{font-size:.85rem;color:var(--gray-500);display:flex;align-items:center;gap:6px;transition:color .2s}
|
||||
.nav-back:hover{color:var(--primary)}
|
||||
|
||||
.reg-wrapper{flex:1;display:grid;grid-template-columns:1fr 1fr}
|
||||
|
||||
/* LEFT */
|
||||
.reg-left{
|
||||
background:linear-gradient(135deg,#0D1117 0%,#0D2150 100%);
|
||||
display:flex;flex-direction:column;justify-content:center;padding:60px 56px;
|
||||
position:relative;overflow:hidden;
|
||||
}
|
||||
.reg-left::before{
|
||||
content:'';position:absolute;inset:0;
|
||||
background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none'%3E%3Cg fill='%230057FF' fill-opacity='0.06'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
.reg-left-content{position:relative;z-index:1;max-width:420px}
|
||||
.al-badge{display:inline-flex;align-items:center;gap:8px;background:rgba(16,185,129,.15);border:1px solid rgba(16,185,129,.3);color:#6EE7B7;font-size:.8rem;font-weight:600;padding:6px 14px;border-radius:999px;margin-bottom:32px}
|
||||
.al-badge span{width:8px;height:8px;border-radius:50%;background:#6EE7B7;animation:pulse 2s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
.reg-left h2{font-size:2rem;font-weight:800;color:#fff;line-height:1.2;margin-bottom:12px}
|
||||
.reg-left h2 em{color:var(--accent);font-style:normal}
|
||||
.reg-left p{color:#9CA3AF;font-size:.95rem;line-height:1.7;margin-bottom:36px}
|
||||
.steps-list{display:flex;flex-direction:column;gap:0}
|
||||
.reg-step{display:flex;gap:14px;align-items:flex-start}
|
||||
.rs-connector{width:2px;background:rgba(255,255,255,.1);min-height:24px;margin:4px 0 4px 19px}
|
||||
.rs-num{width:40px;height:40px;border-radius:50%;background:var(--primary);color:#fff;font-size:.85rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.rs-text{padding-top:8px;padding-bottom:20px}
|
||||
.rs-text strong{display:block;color:#fff;font-size:.9rem;margin-bottom:2px}
|
||||
.rs-text span{color:#6B7280;font-size:.82rem}
|
||||
.address-preview{
|
||||
background:rgba(0,87,255,.15);border:1px solid rgba(0,87,255,.3);
|
||||
border-radius:10px;padding:16px 20px;margin-top:24px;
|
||||
font-family:'Courier New',monospace;font-size:.82rem;color:#A5B4FC;line-height:1.8;
|
||||
}
|
||||
.address-preview .suite{color:var(--accent);font-weight:700;font-size:.95rem}
|
||||
|
||||
/* RIGHT */
|
||||
.reg-right{display:flex;align-items:flex-start;justify-content:center;padding:48px 24px;background:var(--white);overflow-y:auto}
|
||||
.reg-box{width:100%;max-width:460px}
|
||||
.reg-box h1{font-size:1.7rem;font-weight:800;color:var(--gray-900);margin-bottom:8px}
|
||||
.reg-box .subtitle{color:var(--gray-500);font-size:.9rem;margin-bottom:32px}
|
||||
.reg-box .subtitle a{color:var(--primary);font-weight:600}
|
||||
|
||||
/* Progress */
|
||||
.progress-bar{display:flex;gap:0;margin-bottom:32px}
|
||||
.prog-step{flex:1;height:4px;background:var(--gray-200);border-radius:4px;transition:background .3s}
|
||||
.prog-step.done{background:var(--green)}
|
||||
.prog-step.active{background:var(--primary)}
|
||||
.prog-labels{display:flex;justify-content:space-between;font-size:.72rem;color:var(--gray-500);margin-top:6px;margin-bottom:28px}
|
||||
.prog-labels span.active{color:var(--primary);font-weight:600}
|
||||
.prog-labels span.done{color:var(--green)}
|
||||
|
||||
.form-group{margin-bottom:18px}
|
||||
.form-group label{display:block;font-size:.85rem;font-weight:600;color:var(--gray-700);margin-bottom:5px}
|
||||
.form-group input,.form-group select{
|
||||
width:100%;padding:12px 16px;border:2px solid var(--gray-200);
|
||||
border-radius:8px;font-size:.93rem;font-family:inherit;
|
||||
transition:border-color .2s;outline:none;background:var(--white);
|
||||
}
|
||||
.form-group input:focus,.form-group select:focus{border-color:var(--primary)}
|
||||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||||
.input-wrap{position:relative}
|
||||
.input-wrap input{padding-right:44px}
|
||||
.toggle-pass{position:absolute;right:12px;top:50%;transform:translateY(-50%);cursor:pointer;font-size:.95rem;color:var(--gray-500);background:none;border:none;padding:0}
|
||||
|
||||
.pass-strength{margin-top:6px;display:flex;gap:4px}
|
||||
.ps-bar{flex:1;height:3px;border-radius:2px;background:var(--gray-200);transition:background .3s}
|
||||
.ps-label{font-size:.72rem;color:var(--gray-500);margin-top:4px}
|
||||
|
||||
.terms-check{display:flex;gap:10px;align-items:flex-start;font-size:.83rem;color:var(--gray-700);margin-bottom:24px;cursor:pointer}
|
||||
.terms-check input{accent-color:var(--primary);width:16px;height:16px;flex-shrink:0;margin-top:2px}
|
||||
.terms-check a{color:var(--primary)}
|
||||
|
||||
.btn-submit{width:100%;padding:14px;background:var(--primary);color:#fff;border:none;border-radius:8px;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;transition:all .2s}
|
||||
.btn-submit:hover{background:var(--primary-d);transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,87,255,.3)}
|
||||
.btn-outline{width:100%;padding:13px;background:transparent;color:var(--gray-700);border:2px solid var(--gray-200);border-radius:8px;font-size:.95rem;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s;margin-top:10px}
|
||||
.btn-outline:hover{border-color:var(--primary);color:var(--primary)}
|
||||
|
||||
/* Step panels */
|
||||
.step-panel{display:none}
|
||||
.step-panel.active{display:block}
|
||||
|
||||
/* Success */
|
||||
.success-box{text-align:center;padding:20px 0}
|
||||
.success-icon{font-size:4rem;margin-bottom:16px}
|
||||
.success-box h2{font-size:1.5rem;font-weight:800;margin-bottom:10px}
|
||||
.success-box p{color:var(--gray-500);font-size:.9rem;line-height:1.7;margin-bottom:24px}
|
||||
.suite-display{
|
||||
background:var(--dark);border-radius:10px;padding:20px;
|
||||
font-family:'Courier New',monospace;font-size:.85rem;color:#A5B4FC;
|
||||
line-height:2;margin-bottom:24px;text-align:left;
|
||||
}
|
||||
.suite-display .suite-num{color:var(--accent);font-weight:700;font-size:1.1rem}
|
||||
|
||||
.alert{padding:12px 16px;border-radius:8px;font-size:.85rem;margin-bottom:16px;display:none}
|
||||
.alert.show{display:flex;gap:10px;align-items:center}
|
||||
.alert-error{background:#FEE2E2;color:#991B1B;border:1px solid #FCA5A5}
|
||||
|
||||
@media(max-width:768px){
|
||||
.reg-wrapper{grid-template-columns:1fr}
|
||||
.reg-left{display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<a href="index.html" class="nav-logo">Moraworld<span>.</span>Imports</a>
|
||||
<a href="index.html" class="nav-back">← Volver al inicio</a>
|
||||
</nav>
|
||||
|
||||
<div class="reg-wrapper">
|
||||
|
||||
<!-- LEFT -->
|
||||
<div class="reg-left">
|
||||
<div class="reg-left-content">
|
||||
<div class="al-badge"><span></span> Registro gratuito — 2 minutos</div>
|
||||
<h2>Abre tu <em>Suite</em> en<br/>New Jersey hoy</h2>
|
||||
<p>Compra en Amazon, eBay, Walmart y más. Te asignamos una dirección personalizada en nuestra bodega de NJ y enviamos a Ecuador.</p>
|
||||
<div class="steps-list">
|
||||
<div class="reg-step">
|
||||
<div class="rs-num">1</div>
|
||||
<div class="rs-text"><strong>Crea tu cuenta</strong><span>Datos personales y contraseña segura</span></div>
|
||||
</div>
|
||||
<div class="rs-connector"></div>
|
||||
<div class="reg-step">
|
||||
<div class="rs-num">2</div>
|
||||
<div class="rs-text"><strong>Verifica tu correo</strong><span>Código de 6 dígitos a tu bandeja</span></div>
|
||||
</div>
|
||||
<div class="rs-connector"></div>
|
||||
<div class="reg-step">
|
||||
<div class="rs-num">3</div>
|
||||
<div class="rs-text"><strong>Configura MFA</strong><span>Activa la autenticación en dos pasos</span></div>
|
||||
</div>
|
||||
<div class="rs-connector"></div>
|
||||
<div class="reg-step">
|
||||
<div class="rs-num">4</div>
|
||||
<div class="rs-text"><strong>¡Listo!</strong><span>Tu Suite queda disponible de inmediato</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="address-preview">
|
||||
<div>150 N Day St,</div>
|
||||
<div class="suite">Suite EC-XXXXX,</div>
|
||||
<div>City of Orange, NJ 07050</div>
|
||||
<div>United States 🇺🇸</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT -->
|
||||
<div class="reg-right">
|
||||
<div class="reg-box">
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="progress-bar" id="progress-bar">
|
||||
<div class="prog-step active" id="pb-1"></div>
|
||||
<div class="prog-step" id="pb-2"></div>
|
||||
<div class="prog-step" id="pb-3"></div>
|
||||
</div>
|
||||
<div class="prog-labels">
|
||||
<span class="active" id="pl-1">Datos personales</span>
|
||||
<span id="pl-2">Verificación</span>
|
||||
<span id="pl-3">Seguridad MFA</span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-error" id="alert-error">
|
||||
<span>⚠️</span><span id="alert-msg"></span>
|
||||
</div>
|
||||
|
||||
<!-- ─── STEP 1: Datos ─── -->
|
||||
<div class="step-panel active" id="s1">
|
||||
<h1>Crear cuenta gratis</h1>
|
||||
<p class="subtitle">¿Ya tienes cuenta? <a href="login.html">Inicia sesión aquí →</a></p>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Nombre</label>
|
||||
<input type="text" id="nombre" placeholder="Juan"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Apellido</label>
|
||||
<input type="text" id="apellido" placeholder="Pérez"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Correo electrónico</label>
|
||||
<input type="email" id="email" placeholder="correo@ejemplo.com"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Teléfono (WhatsApp)</label>
|
||||
<input type="tel" id="telefono" placeholder="+593 99 123 4567"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ciudad en Ecuador</label>
|
||||
<select id="ciudad">
|
||||
<option value="">Selecciona tu ciudad</option>
|
||||
<option>Cuenca</option>
|
||||
<option>Quito</option>
|
||||
<option>Guayaquil</option>
|
||||
<option>Ambato</option>
|
||||
<option>Loja</option>
|
||||
<option>Riobamba</option>
|
||||
<option>Manta</option>
|
||||
<option>Otra ciudad</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Cédula / Pasaporte</label>
|
||||
<input type="text" id="cedula" placeholder="Ej: 0102030405"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Contraseña</label>
|
||||
<div class="input-wrap">
|
||||
<input type="password" id="password" placeholder="Mínimo 8 caracteres" oninput="checkStrength(this.value)"/>
|
||||
<button class="toggle-pass" onclick="togglePassword('password',this)" type="button">👁️</button>
|
||||
</div>
|
||||
<div class="pass-strength" id="ps">
|
||||
<div class="ps-bar" id="psb1"></div>
|
||||
<div class="ps-bar" id="psb2"></div>
|
||||
<div class="ps-bar" id="psb3"></div>
|
||||
<div class="ps-bar" id="psb4"></div>
|
||||
</div>
|
||||
<div class="ps-label" id="ps-label"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirmar contraseña</label>
|
||||
<div class="input-wrap">
|
||||
<input type="password" id="password2" placeholder="Repite la contraseña"/>
|
||||
<button class="toggle-pass" onclick="togglePassword('password2',this)" type="button">👁️</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="terms-check">
|
||||
<input type="checkbox" id="terms"/>
|
||||
Acepto los <a href="#">Términos de uso</a> y la <a href="#">Política de privacidad</a> de Moraworld Imports (LOPDP Ecuador).
|
||||
</label>
|
||||
<button class="btn-submit" onclick="goStep2()">Continuar →</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── STEP 2: Verificación correo ─── -->
|
||||
<div class="step-panel" id="s2">
|
||||
<h1>Verifica tu correo</h1>
|
||||
<p class="subtitle" style="margin-bottom:28px;">Enviamos un código de 6 dígitos a <strong id="email-display"></strong>. Revisa tu bandeja de entrada.</p>
|
||||
<div style="display:flex;gap:10px;margin-bottom:24px">
|
||||
<input type="text" maxlength="1" class="otp-digit" style="flex:1;text-align:center;font-size:1.6rem;font-weight:700;padding:14px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none;transition:border-color .2s" oninput="otpMove(this,0)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" style="flex:1;text-align:center;font-size:1.6rem;font-weight:700;padding:14px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none;transition:border-color .2s" oninput="otpMove(this,1)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" style="flex:1;text-align:center;font-size:1.6rem;font-weight:700;padding:14px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none;transition:border-color .2s" oninput="otpMove(this,2)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" style="flex:1;text-align:center;font-size:1.6rem;font-weight:700;padding:14px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none;transition:border-color .2s" oninput="otpMove(this,3)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" style="flex:1;text-align:center;font-size:1.6rem;font-weight:700;padding:14px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none;transition:border-color .2s" oninput="otpMove(this,4)"/>
|
||||
<input type="text" maxlength="1" class="otp-digit" style="flex:1;text-align:center;font-size:1.6rem;font-weight:700;padding:14px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none;transition:border-color .2s" oninput="otpMove(this,5)"/>
|
||||
</div>
|
||||
<button class="btn-submit" onclick="goStep3()">Verificar y continuar →</button>
|
||||
<button class="btn-outline" onclick="goStep1()">← Volver</button>
|
||||
<p style="text-align:center;margin-top:16px;font-size:.82rem;color:var(--gray-500)">¿No recibiste el código? <a href="#" style="color:var(--primary);font-weight:600" onclick="return false">Reenviar código</a></p>
|
||||
</div>
|
||||
|
||||
<!-- ─── STEP 3: MFA Setup ─── -->
|
||||
<div class="step-panel" id="s3">
|
||||
<h1>Activar MFA</h1>
|
||||
<p class="subtitle" style="margin-bottom:24px;">Escanea este código QR con <strong>Google Authenticator</strong> o <strong>Authy</strong> para activar la verificación en dos pasos.</p>
|
||||
<div style="background:var(--gray-100);border-radius:12px;padding:24px;text-align:center;margin-bottom:24px">
|
||||
<!-- QR placeholder -->
|
||||
<div style="width:160px;height:160px;background:#fff;margin:0 auto 16px;border-radius:8px;border:2px solid var(--gray-200);display:flex;align-items:center;justify-content:center;font-size:2.5rem">🔲</div>
|
||||
<p style="font-size:.78rem;color:var(--gray-500);margin-bottom:8px">O ingresa esta clave manual:</p>
|
||||
<code style="background:var(--dark);color:#A5B4FC;padding:8px 16px;border-radius:6px;font-size:.82rem;letter-spacing:.1em">MORA-XXXX-YYYY-ZZZZ</code>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Confirma el código generado por la app</label>
|
||||
<div style="display:flex;gap:10px">
|
||||
<input type="text" maxlength="1" class="otp-mfa" style="flex:1;text-align:center;font-size:1.4rem;font-weight:700;padding:12px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none" oninput="mfaMove(this,0)"/>
|
||||
<input type="text" maxlength="1" class="otp-mfa" style="flex:1;text-align:center;font-size:1.4rem;font-weight:700;padding:12px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none" oninput="mfaMove(this,1)"/>
|
||||
<input type="text" maxlength="1" class="otp-mfa" style="flex:1;text-align:center;font-size:1.4rem;font-weight:700;padding:12px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none" oninput="mfaMove(this,2)"/>
|
||||
<input type="text" maxlength="1" class="otp-mfa" style="flex:1;text-align:center;font-size:1.4rem;font-weight:700;padding:12px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none" oninput="mfaMove(this,3)"/>
|
||||
<input type="text" maxlength="1" class="otp-mfa" style="flex:1;text-align:center;font-size:1.4rem;font-weight:700;padding:12px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none" oninput="mfaMove(this,4)"/>
|
||||
<input type="text" maxlength="1" class="otp-mfa" style="flex:1;text-align:center;font-size:1.4rem;font-weight:700;padding:12px 4px;border:2px solid var(--gray-200);border-radius:8px;font-family:inherit;outline:none" oninput="mfaMove(this,5)"/>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-submit" onclick="finishReg()">Activar MFA y abrir mi casillero →</button>
|
||||
<button class="btn-outline" onclick="goStep2()">← Volver</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── SUCCESS ─── -->
|
||||
<div class="step-panel" id="s-success">
|
||||
<div class="success-box">
|
||||
<div class="success-icon">🎉</div>
|
||||
<h2>¡Tu casillero está listo!</h2>
|
||||
<p>Tu Suite personalizada en New Jersey ha sido creada. Usa esta dirección en tus compras online.</p>
|
||||
<div class="suite-display">
|
||||
<div id="suc-name" style="color:#fff;font-weight:700;margin-bottom:4px"></div>
|
||||
<div>150 N Day St,</div>
|
||||
<div class="suite-num">Suite EC-<span id="suc-suite">00001</span>,</div>
|
||||
<div>City of Orange, NJ 07050</div>
|
||||
<div>United States 🇺🇸</div>
|
||||
</div>
|
||||
<a href="portal-cliente.html" style="display:block;padding:14px;background:var(--primary);color:#fff;border-radius:8px;font-weight:700;font-size:1rem;margin-bottom:12px">Ir a mi portal →</a>
|
||||
<a href="index.html" style="display:block;padding:12px;background:var(--gray-100);color:var(--gray-700);border-radius:8px;font-weight:600;font-size:.9rem">Volver al inicio</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showStep(n){
|
||||
['s1','s2','s3','s-success'].forEach(id=>{
|
||||
document.getElementById(id).classList.remove('active');
|
||||
});
|
||||
document.getElementById('s'+n).classList.add('active');
|
||||
|
||||
const bars = [null,'pb-1','pb-2','pb-3'];
|
||||
const labels = [null,'pl-1','pl-2','pl-3'];
|
||||
[1,2,3].forEach(i=>{
|
||||
const b=document.getElementById(bars[i]);
|
||||
const l=document.getElementById(labels[i]);
|
||||
if(!b) return;
|
||||
b.className='prog-step';
|
||||
l.className='';
|
||||
if(i < n){b.classList.add('done');l.classList.add('done')}
|
||||
else if(i === n){b.classList.add('active');l.classList.add('active')}
|
||||
});
|
||||
}
|
||||
|
||||
function goStep1(){ showStep(1); }
|
||||
function goStep2(){
|
||||
const nome = document.getElementById('nombre').value.trim();
|
||||
const mail = document.getElementById('email').value.trim();
|
||||
const pass = document.getElementById('password').value;
|
||||
const pass2= document.getElementById('password2').value;
|
||||
const terms= document.getElementById('terms').checked;
|
||||
if(!nome||!mail||!pass){ showAlert('Completa todos los campos requeridos.'); return; }
|
||||
if(pass !== pass2){ showAlert('Las contraseñas no coinciden.'); return; }
|
||||
if(pass.length < 8){ showAlert('La contraseña debe tener al menos 8 caracteres.'); return; }
|
||||
if(!terms){ showAlert('Debes aceptar los términos de uso.'); return; }
|
||||
hideAlert();
|
||||
document.getElementById('email-display').textContent = mail;
|
||||
showStep(2);
|
||||
document.querySelector('#s2 .otp-digit').focus();
|
||||
}
|
||||
function goStep3(){
|
||||
const digits = document.querySelectorAll('#s2 .otp-digit');
|
||||
const code = Array.from(digits).map(d=>d.value).join('');
|
||||
if(code.length < 6){ showAlert('Ingresa el código de 6 dígitos.'); return; }
|
||||
hideAlert();
|
||||
showStep(3);
|
||||
}
|
||||
function finishReg(){
|
||||
const digits = document.querySelectorAll('.otp-mfa');
|
||||
const code = Array.from(digits).map(d=>d.value).join('');
|
||||
if(code.length < 6){ showAlert('Confirma el código MFA.'); return; }
|
||||
hideAlert();
|
||||
const nome = document.getElementById('nombre').value.trim();
|
||||
const ape = document.getElementById('apellido').value.trim();
|
||||
document.getElementById('suc-name').textContent = nome + ' ' + ape;
|
||||
// Suite aleatoria para demo
|
||||
document.getElementById('suc-suite').textContent = String(Math.floor(Math.random()*89999+10000));
|
||||
['s1','s2','s3','s-success'].forEach(id=>document.getElementById(id).classList.remove('active'));
|
||||
document.getElementById('s-success').classList.add('active');
|
||||
document.getElementById('progress-bar').style.display='none';
|
||||
document.querySelector('.prog-labels').style.display='none';
|
||||
}
|
||||
|
||||
function otpMove(inp, idx){
|
||||
const digits = document.querySelectorAll('#s2 .otp-digit');
|
||||
inp.value = inp.value.replace(/\D/g,'');
|
||||
if(inp.value && idx < 5) digits[idx+1].focus();
|
||||
const code = Array.from(digits).map(d=>d.value).join('');
|
||||
if(code.length===6) goStep3();
|
||||
}
|
||||
function mfaMove(inp, idx){
|
||||
const digits = document.querySelectorAll('.otp-mfa');
|
||||
inp.value = inp.value.replace(/\D/g,'');
|
||||
if(inp.value && idx < 5) digits[idx+1].focus();
|
||||
const code = Array.from(digits).map(d=>d.value).join('');
|
||||
if(code.length===6) finishReg();
|
||||
}
|
||||
|
||||
function togglePassword(id,btn){
|
||||
const inp=document.getElementById(id);
|
||||
if(inp.type==='password'){inp.type='text';btn.textContent='🙈';}
|
||||
else{inp.type='password';btn.textContent='👁️';}
|
||||
}
|
||||
|
||||
function checkStrength(v){
|
||||
const bars=[document.getElementById('psb1'),document.getElementById('psb2'),document.getElementById('psb3'),document.getElementById('psb4')];
|
||||
const label=document.getElementById('ps-label');
|
||||
let score=0;
|
||||
if(v.length>=8)score++;
|
||||
if(/[A-Z]/.test(v))score++;
|
||||
if(/[0-9]/.test(v))score++;
|
||||
if(/[^A-Za-z0-9]/.test(v))score++;
|
||||
const colors=['#EF4444','#F59E0B','#3B82F6','#10B981'];
|
||||
const labels=['Muy débil','Débil','Buena','Fuerte'];
|
||||
bars.forEach((b,i)=>{ b.style.background = i < score ? colors[score-1] : 'var(--gray-200)'; });
|
||||
label.textContent = score ? labels[score-1] : '';
|
||||
label.style.color = score ? colors[score-1] : '';
|
||||
}
|
||||
|
||||
function showAlert(msg){
|
||||
const el=document.getElementById('alert-error');
|
||||
document.getElementById('alert-msg').textContent=msg;
|
||||
el.classList.add('show');
|
||||
}
|
||||
function hideAlert(){ document.getElementById('alert-error').classList.remove('show'); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,716 @@
|
||||
# Moraworld Imports — Sistema de Casillero Internacional
|
||||
## Requerimientos y Arquitectura de Software
|
||||
|
||||
**Empresas:**
|
||||
- **Moraworld Imports S.A.S.** — Constituida bajo las leyes de Ecuador (Cuenca)
|
||||
- **Mora Global Import LLC** — Constituida bajo las leyes de EE.UU. (New Jersey)
|
||||
|
||||
**Versión:** 1.1
|
||||
**Fecha:** 6 de mayo de 2026
|
||||
**Estado:** Borrador — Levantamiento de Requerimientos
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen Ejecutivo
|
||||
|
||||
**Moraworld Imports S.A.S.** (Ecuador) y **Mora Global Import LLC** (EE.UU.) operan un ecosistema logístico transfronterizo con bodega física ubicada en:
|
||||
|
||||
> **150 N Day St, City of Orange, NJ 07050, EE.UU.**
|
||||
|
||||
El sistema permite a usuarios ecuatorianos realizar compras en plataformas como **Amazon**, **eBay**, **Walmart**, entre otras, utilizando una Suite/casillero personalizado dentro de esa dirección como destino. Una vez que el paquete llega a la bodega en New Jersey, la plataforma gestiona el proceso logístico de reenvío hacia **Ecuador**, incluyendo la declaración automática ante la **Aduana Ecuatoriana** (SENAE).
|
||||
|
||||
El negocio opera dos líneas de servicio claramente diferenciadas:
|
||||
|
||||
| Servicio | Descripción | Flujo |
|
||||
|-----------------------|-----------------------------------------------------------------------------|:------------:|
|
||||
| **Casillero (B2C)** | Compras pequeñas: Amazon, eBay, ropa, tecnología. Clientes individuales. | Automatizado |
|
||||
| **Carga Pesada (B2B)**| Importación mayorista: pallets, contenedores (ej: 1,000 pares de zapatos). | Cotización |
|
||||
|
||||
---
|
||||
|
||||
## 2. Arquitectura del Sistema
|
||||
|
||||
### 2.1 Modelo: Multi-Tenant
|
||||
|
||||
El sistema estará desarrollado bajo una arquitectura **Multi-Tenant**, lo que permite operar bajo un único despliegue de software, pero con datos completamente aislados por organización o cliente empresa.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ INTERNET (HTTPS) │
|
||||
└──────────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌──────────▼──────────┐
|
||||
│ Load Balancer / │
|
||||
│ API Gateway │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐ ┌─────▼───────┐
|
||||
│ Tenant A │ │ Tenant B │ │ Tenant N │
|
||||
│ (Empresa 1) │ │ (Empresa 2) │ │ (Empresa N) │
|
||||
└──────┬──────┘ └──────┬──────┘ └─────┬───────┘
|
||||
│ │ │
|
||||
└────────────────┼───────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ Base de Datos │
|
||||
│ (Schema por Tenant) │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Cumplimiento ISO/IEC 27001
|
||||
|
||||
El sistema deberá estar diseñado cumpliendo los controles del estándar **ISO/IEC 27001:2022** para la gestión de seguridad de la información:
|
||||
|
||||
| Control ISO 27001 | Implementación en el sistema |
|
||||
|-------------------------|-------------------------------------------------------------------|
|
||||
| A.5 — Políticas | Política de uso aceptable, manejo de datos y retención |
|
||||
| A.6 — Organización | Roles definidos, segregación de funciones |
|
||||
| A.7 — RRHH | Procedimientos de onboarding/offboarding de usuarios internos |
|
||||
| A.8 — Gestión de activos| Inventario de datos personales y de paquetes (conforme a PDPA/LOPD) |
|
||||
| A.9 — Control de acceso | Autenticación MFA, RBAC (Role-Based Access Control) |
|
||||
| A.10 — Criptografía | TLS 1.3 en tránsito, AES-256 en reposo |
|
||||
| A.12 — Operaciones | Logs de auditoría inmutables, monitoreo continuo |
|
||||
| A.14 — Desarrollo seguro| SDLC seguro, revisiones de código, OWASP Top 10 |
|
||||
| A.16 — Incidentes | Proceso de gestión y notificación de incidentes de seguridad |
|
||||
| A.18 — Cumplimiento | Declaración aduanera automática conforme a normativa SENAE |
|
||||
|
||||
### 2.3 Stack Tecnológico Sugerido
|
||||
|
||||
| Capa | Tecnología sugerida |
|
||||
|-------------------|--------------------------------------------------|
|
||||
| Frontend | Next.js / React (SSR para SEO en landing page) |
|
||||
| Backend / API | Node.js + Express o NestJS (REST / GraphQL) |
|
||||
| Base de datos | PostgreSQL (multi-schema por tenant) |
|
||||
| Autenticación | OAuth 2.0 + JWT + MFA (TOTP) |
|
||||
| Almacenamiento | AWS S3 o similar (fotos de paquetes, documentos) |
|
||||
| Integración | API SENAE (Aduana Ecuador), Amazon SP-API |
|
||||
| Tracking | Webhooks + sistema propio de tracking IDs |
|
||||
| Infraestructura | AWS / GCP con ambientes: dev, staging, producción|
|
||||
| Seguridad | WAF, rate limiting, SIEM para auditoría |
|
||||
|
||||
---
|
||||
|
||||
## 3. Roles y Permisos del Sistema
|
||||
|
||||
### 3.1 Definición de Roles
|
||||
|
||||
| Rol | Descripción |
|
||||
|-----------------------|-----------------------------------------------------------------------------|
|
||||
| **Super Admin** | Acceso total al sistema. Gestiona tenants, usuarios globales y configuración|
|
||||
| **Admin Empresa** | Administra su propio tenant: usuarios, tarifas, bodegas asignadas |
|
||||
| **Operador Bodega** | Personal en EE.UU. que verifica, pesa y escanea paquetes físicamente |
|
||||
| **Agente Aduanero** | Revisa y confirma las declaraciones antes de enviarlas a la SENAE |
|
||||
| **Cliente Final** | Usuario ecuatoriano que registra compras y hace seguimiento |
|
||||
| **Soporte** | Puede ver información del cliente y paquetes, no puede modificar datos clave|
|
||||
|
||||
### 3.2 Matriz de Permisos
|
||||
|
||||
| Módulo / Acción | Super Admin | Admin Empresa | Operador Bodega | Agente Aduanero | Cliente | Soporte |
|
||||
|------------------------------|:-----------:|:-------------:|:---------------:|:---------------:|:-------:|:-------:|
|
||||
| Ver dashboard general | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Crear/editar usuarios | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Configurar tarifas y zonas | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| Registrar compra (link/manual)| ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
| Ver mis paquetes | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| Verificar paquete en bodega | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Confirmar peso/dimensiones | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||
| Generar declaración aduanera | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||
| Enviar datos a SENAE | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
|
||||
| Ver historial de envíos | ✅ | ✅ | ✅ | ✅ | ✅ (solo propios) | ✅ |
|
||||
| Acceder a logs de auditoría | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 4. Módulos del Sistema
|
||||
|
||||
### 4.1 Landing Page (Pública)
|
||||
|
||||
Página de presentación del servicio accesible desde cualquier dispositivo con internet.
|
||||
|
||||
**Secciones de contenido:**
|
||||
- Hero con propuesta de valor: casillero en EE.UU. para Ecuador
|
||||
- **Cómo funciona** — pasos visuales diferenciados por tipo de servicio
|
||||
- **Calculadora de envío** visible en la landing (sin necesidad de registro)
|
||||
- Tarifas y categorías de SENAE
|
||||
- Sección **"Quiénes Somos"** — dos empresas constituidas legalmente:
|
||||
- Moraworld Imports S.A.S. — RUC Ecuador, rep. legal en Cuenca
|
||||
- Mora Global Import LLC — Registro EE.UU., rep. legal en New Jersey
|
||||
- **Sección de Legalidad:** Mostrar documentos de constitución, RUC, permisos aduaneros SENAE, cumplimiento normas INEN
|
||||
- Sección de servicios: **Casillero** vs **Carga Pesada/Pallets**
|
||||
- Preguntas frecuentes
|
||||
- Registro / inicio de sesión de clientes
|
||||
- Blog o noticias (opcional)
|
||||
|
||||
**Elementos de contacto y confianza:**
|
||||
- **Botón de WhatsApp flotante** con opción de contactar:
|
||||
- Representante en New Jersey (bodega / operaciones)
|
||||
- Representante legal en Cuenca, Ecuador
|
||||
- Sección de certificaciones y normativas (ISO, INEN, SENAE)
|
||||
|
||||
**Requisitos técnicos:**
|
||||
- SEO optimizado (Next.js SSR)
|
||||
- Responsive — mobile-first
|
||||
- HTTPS obligatorio
|
||||
- Carga < 3 segundos
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Portal del Cliente Final (Panel de Casillero)
|
||||
|
||||
Acceso mediante credenciales. El cliente ecuatoriano gestiona sus pedidos aquí.
|
||||
|
||||
**Al registrarse, el sistema asigna automáticamente una Suite/casillero personalizado:**
|
||||
```
|
||||
150 N Day St, Suite [#CASILLERO], City of Orange, NJ 07050, EE.UU.
|
||||
Ejemplo: 150 N Day St, Suite EC-00345, City of Orange, NJ 07050
|
||||
```
|
||||
Esta dirección se muestra de forma destacada en el panel del cliente para que la use en sus compras en línea.
|
||||
|
||||
**Funcionalidades:**
|
||||
- Registro y verificación de cuenta (correo + MFA)
|
||||
- Ver su **Suite/casillero asignado** con dirección completa y copiable
|
||||
- **Pre-alerta de compra (Gestión Documental):**
|
||||
- El cliente notifica antes de que llegue el paquete: tienda, número de tracking del vendedor, valor, descripción
|
||||
- Puede cargar la **factura comercial** (PDF, imagen) emitida por Amazon / eBay / tienda
|
||||
- Esto permite a la bodega organizar y anticipar la llegada del paquete
|
||||
- Registrar una compra por:
|
||||
- **Enlace (URL):** El sistema escanea el producto de Amazon u otra plataforma y pre-llena el formulario
|
||||
- **Manual:** Nombre, descripción, valor declarado, tienda, cantidad, peso estimado
|
||||
- Ver **fotos del paquete** cuando éste llega a la bodega NJ
|
||||
- Ver cotización de envío con desglose por categoría SENAE antes del pago
|
||||
- Pagar el costo de envío anticipadamente
|
||||
- Ver historial de pedidos con **ID de Tracking único**
|
||||
- Seguimiento en tiempo real del estado del paquete
|
||||
- Notificaciones por correo / SMS / WhatsApp en cada etapa
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Portal de Bodega (New Jersey, EE.UU.)
|
||||
|
||||
Usado por los operadores físicos en la bodega de **150 N Day St, City of Orange, NJ**.
|
||||
|
||||
**Funcionalidades:**
|
||||
- Ver paquetes esperados con pre-alertas enviadas por los clientes
|
||||
- Registrar llegada de paquete (escaneo de código de barras o QR)
|
||||
- Ver las facturas y documentos cargados por el cliente en la pre-alerta
|
||||
- Confirmar y corregir:
|
||||
- Peso real (báscula)
|
||||
- Dimensiones (largo, ancho, alto)
|
||||
- **Fotos del paquete** (se sincronizan con el portal del cliente)
|
||||
- Alertar discrepancias entre peso declarado y real (diferencia > 10%)
|
||||
- Asignar paquete a su Suite/casillero correspondiente
|
||||
- Sincronización en tiempo real vía API con el software propio de bodega
|
||||
- Generar orden de despacho hacia Ecuador
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Portal Administrativo
|
||||
|
||||
Usado por Admin Empresa y Super Admin.
|
||||
|
||||
**Funcionalidades:**
|
||||
- Dashboard con métricas: paquetes en tránsito, recibidos, enviados, con problemas
|
||||
- Gestión de usuarios y roles
|
||||
- Configuración de tarifas: precio por libra/kg, por zona geográfica en Ecuador
|
||||
- Gestión de bodegas (múltiples posibles)
|
||||
- Reportes: ingresos, volumen de envíos, tiempo promedio de entrega
|
||||
- Logs de auditoría (quién hizo qué y cuándo)
|
||||
- Configuración de notificaciones automáticas
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Módulo de Integración con Aduanas (SENAE)
|
||||
|
||||
**Funcionalidades:**
|
||||
- Generación automática de la **Declaración Simplificada de Importación (DSI)**
|
||||
- Cálculo automático de impuestos (fodinfa, IVA, arancel) según tabla SENAE
|
||||
- Envío electrónico de la declaración a la SENAE vía WebService oficial
|
||||
- Recepción de respuesta / número de autorización SENAE
|
||||
- Almacenamiento del documento aduanero asociado al tracking ID
|
||||
- Alerta si el valor del paquete supera el umbral de importación simplificada ($400 USD)
|
||||
|
||||
---
|
||||
|
||||
### 4.6 Módulo de Tracking en Tiempo Real
|
||||
|
||||
Cada paquete tendrá un **ID de Tracking único** generado automáticamente al registrar la compra o la pre-alerta.
|
||||
|
||||
**Formato sugerido:** `EC-YYYYMMDD-XXXXXX`
|
||||
Ejemplo: `EC-20260506-082341`
|
||||
|
||||
El cliente puede buscar el estado de su paquete ingresando el Tracking ID en la landing page (sin necesidad de iniciar sesión) o en su panel de usuario.
|
||||
|
||||
**Estados del paquete:**
|
||||
|
||||
| # | Estado | Descripción |
|
||||
|---|-------------------------------|----------------------------------------------------------|
|
||||
| 1 | `REGISTRADO` | El cliente registró la compra en el sistema |
|
||||
| 2 | `EN_TRANSITO_BODEGA` | El pedido fue despachado por el vendedor hacia la bodega |
|
||||
| 3 | `RECIBIDO_BODEGA` | La bodega confirmó la recepción del paquete |
|
||||
| 4 | `EN_VERIFICACION` | El operador está pesando y revisando el paquete |
|
||||
| 5 | `VERIFICADO` | Peso y dimensiones confirmados, pago aplicado |
|
||||
| 6 | `DECLARACION_ADUANERA` | Declaración enviada a la SENAE |
|
||||
| 7 | `EN_TRANSITO_ECUADOR` | El paquete viaja desde EE.UU. hacia Ecuador |
|
||||
| 8 | `EN_ADUANA_ECUADOR` | El paquete está en proceso aduanero en Ecuador |
|
||||
| 9 | `LISTO_ENTREGA` | Paquete aprobado por aduana, listo para entrega |
|
||||
|10 | `ENTREGADO` | Paquete entregado al cliente final |
|
||||
|11 | `INCIDENCIA` | Problema reportado (daño, pérdida, retención en aduana) |
|
||||
|
||||
---
|
||||
|
||||
### 4.7 Módulo de Pre-alerta y Gestión Documental
|
||||
|
||||
Permite al cliente avisar con anticipación sobre paquetes en camino hacia la bodega.
|
||||
|
||||
**Formulario de pre-alerta:**
|
||||
- Tienda / plataforma de compra (Amazon, eBay, etc.)
|
||||
- Número de tracking del vendedor (ej: 1Z999AA10123456784 de UPS)
|
||||
- Descripción del producto
|
||||
- Valor declarado ($USD)
|
||||
- Carga de factura comercial (PDF o imagen)
|
||||
- Fecha estimada de llegada a la bodega
|
||||
|
||||
**Resultado:**
|
||||
- La bodega recibe la pre-alerta y organiza el espacio
|
||||
- El sistema crea un registro asociado al Tracking ID interno
|
||||
- Cuando el paquete llega, el operador lo vincula a la pre-alerta existente
|
||||
|
||||
---
|
||||
|
||||
### 4.8 Servicio de Carga Pesada / Pallets (B2B)
|
||||
|
||||
Diferenciado del casillero, este módulo atiende a importadores mayoristas.
|
||||
|
||||
**Características:**
|
||||
- Flujo NO automatizado — requiere cotización especial
|
||||
- Formulario de "Solicitud de Cotización" con:
|
||||
- Tipo de mercancía (ej: calzado al por mayor)
|
||||
- Cantidad / peso estimado / número de pallets
|
||||
- Valor comercial total
|
||||
- Puerto/ciudad de origen en EE.UU.
|
||||
- Destino en Ecuador
|
||||
- Datos de contacto del importador
|
||||
- Verificación de cumplimiento de normas **INEN** (especialmente para calzado, textiles)
|
||||
- El equipo de Moraworld responde con cotización personalizada
|
||||
- Una vez aceptada, se genera un expediente con ID de seguimiento propio
|
||||
|
||||
**Nota INEN:** Los productos que requieren certificación INEN (ej: calzado, productos eléctricos) deben declararse con el número de registro de certificación correspondiente en la declaración aduanera.
|
||||
|
||||
---
|
||||
|
||||
## 5. Flujos de Usuario
|
||||
|
||||
### 5.1 Flujo del Cliente Final (Servicio Casillero)
|
||||
|
||||
```
|
||||
[1] Accede a la Landing Page
|
||||
│
|
||||
▼
|
||||
[2] Se registra / inicia sesión
|
||||
(Correo + contraseña + MFA)
|
||||
│
|
||||
▼
|
||||
[3] El sistema asigna automáticamente su Suite personalizada:
|
||||
150 N Day St, Suite EC-00345, City of Orange, NJ 07050
|
||||
│
|
||||
▼
|
||||
[4] Hace su compra en Amazon / otra tienda,
|
||||
usando la dirección de la Suite como destino
|
||||
│
|
||||
▼
|
||||
[5A] OPCIONAL — Pre-alerta:
|
||||
El cliente vuelve al portal y crea una pre-alerta:
|
||||
- Tracking del vendedor, valor, descripción
|
||||
- Carga la factura comercial de Amazon/eBay
|
||||
→ La bodega ya sabe que ese paquete viene
|
||||
│
|
||||
▼
|
||||
[5B] Registra formalmente su compra en el portal:
|
||||
┌────────────────────────────┐
|
||||
│ OPCIÓN A: Pega el enlace │──► El sistema escanea el producto:
|
||||
│ de Amazon / tienda │ nombre, foto, precio, peso estimado
|
||||
└────────────────────────────┘
|
||||
┌────────────────────────────┐
|
||||
│ OPCIÓN B: Ingreso manual │──► Llena: nombre, descripción, valor,
|
||||
│ │ tienda, cantidad, peso estimado
|
||||
└────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
[6] El sistema calcula el costo de envío:
|
||||
Peso (lbs/kg) × tarifa + seguro + tasas aduaneras estimadas
|
||||
│
|
||||
▼
|
||||
[7] El cliente revisa y ACEPTA el costo
|
||||
│
|
||||
▼
|
||||
[8] Realiza el PAGO del envío (en línea)
|
||||
│
|
||||
▼
|
||||
[9] El sistema genera el TRACKING ID y confirma por correo
|
||||
│
|
||||
▼
|
||||
[10] El cliente recibe notificaciones en cada cambio de estado
|
||||
vía: correo, SMS y/o WhatsApp
|
||||
│
|
||||
▼
|
||||
[11] Seguimiento en tiempo real desde el portal o la landing page
|
||||
(ingresando el Tracking ID sin necesidad de login)
|
||||
│
|
||||
▼
|
||||
[12] Recibe el paquete en Ecuador
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.5 Flujo del Importador Mayorista (Carga Pesada B2B)
|
||||
|
||||
```
|
||||
[1] Accede a la landing page, sección "Carga Pesada / Pallets"
|
||||
│
|
||||
▼
|
||||
[2] Llena el formulario de "Solicitud de Cotización Especial":
|
||||
- Tipo de mercancía, cantidad, peso, valor
|
||||
- Verificación INEN si aplica (calzado, electrónicos, etc.)
|
||||
│
|
||||
▼
|
||||
[3] El equipo Moraworld revisa y prepara cotización
|
||||
│
|
||||
▼
|
||||
[4] El importador recibe la cotización por correo o WhatsApp
|
||||
│
|
||||
▼
|
||||
[5] Si acepta → se crea expediente con ID de seguimiento B2B
|
||||
│
|
||||
▼
|
||||
[6] Coordinación de embarque, documentos aduaneros,
|
||||
certificación INEN, DAS o DAI según corresponda
|
||||
│
|
||||
▼
|
||||
[7] Seguimiento del embarque hasta entrega en Ecuador
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Flujo del Operador de Bodega
|
||||
|
||||
```
|
||||
[1] Inicia sesión en el Portal de Bodega
|
||||
│
|
||||
▼
|
||||
[2] Ve la lista de paquetes esperados (con sus tracking IDs)
|
||||
│
|
||||
▼
|
||||
[3] Llega un paquete físico a la bodega
|
||||
│
|
||||
▼
|
||||
[4] Escanea el código del paquete o busca manual
|
||||
│
|
||||
▼
|
||||
[5] Confirma recepción → Estado cambia a RECIBIDO_BODEGA
|
||||
│
|
||||
▼
|
||||
[6] Verifica el paquete:
|
||||
- Pesa en báscula (peso real)
|
||||
- Mide dimensiones
|
||||
- Toma fotos
|
||||
- Compara con lo declarado por el cliente
|
||||
│
|
||||
┌───┴────────────────────────────────┐
|
||||
│ │
|
||||
[7A] Sin discrepancia [7B] Hay discrepancia (>10%)
|
||||
│ │
|
||||
▼ ▼
|
||||
Estado: VERIFICADO Notificación al Admin y cliente
|
||||
│ Se cobra diferencia o se ajusta
|
||||
│ │
|
||||
└──────────────┬─────────────────────┘
|
||||
│
|
||||
▼
|
||||
[8] Asigna paquete a envío consolidado o individual
|
||||
│
|
||||
▼
|
||||
[9] Genera orden de despacho hacia Ecuador
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Flujo del Agente Aduanero
|
||||
|
||||
```
|
||||
[1] Inicia sesión en el Portal Administrativo
|
||||
│
|
||||
▼
|
||||
[2] Ve paquetes con estado VERIFICADO
|
||||
│
|
||||
▼
|
||||
[3] Revisa información del paquete:
|
||||
- Descripción del producto
|
||||
- Valor declarado
|
||||
- Fotos
|
||||
- Peso y dimensiones confirmados
|
||||
│
|
||||
▼
|
||||
[4] Verifica que el valor esté dentro del límite
|
||||
de importación simplificada ($400 USD)
|
||||
│
|
||||
┌───┴──────────────────────┐
|
||||
│ │
|
||||
[4A] Dentro del límite [4B] Supera el límite
|
||||
│ │
|
||||
▼ ▼
|
||||
Sigue el proceso Notificación al cliente
|
||||
simplificado para proceso de importación
|
||||
│ formal (diferente declaración)
|
||||
│ │
|
||||
└──────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
[5] El sistema genera la Declaración Simplificada
|
||||
de Importación (DSI) automáticamente
|
||||
│
|
||||
▼
|
||||
[6] El agente revisa y APRUEBA la declaración
|
||||
│
|
||||
▼
|
||||
[7] El sistema envía la DSI al WebService de la SENAE
|
||||
│
|
||||
▼
|
||||
[8] SENAE devuelve número de autorización
|
||||
│
|
||||
▼
|
||||
[9] Estado cambia a DECLARACION_ADUANERA
|
||||
Notificación automática al cliente
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Flujo del Admin Empresa
|
||||
|
||||
```
|
||||
[1] Inicia sesión en Portal Administrativo
|
||||
│
|
||||
▼
|
||||
[2] Ve dashboard general:
|
||||
- Paquetes por estado
|
||||
- Ingresos del día/mes
|
||||
- Alertas e incidencias
|
||||
│
|
||||
├──► Gestiona usuarios y roles
|
||||
├──► Configura tarifas de envío
|
||||
├──► Genera reportes
|
||||
├──► Revisa logs de auditoría
|
||||
└──► Gestiona configuración de bodega
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Flujos del Sistema
|
||||
|
||||
### 6.1 Flujo de Registro de Compra (con enlace)
|
||||
|
||||
```
|
||||
Cliente pega URL de Amazon
|
||||
│
|
||||
▼
|
||||
Sistema hace scraping / llama API del producto
|
||||
(Amazon SP-API o equivalente)
|
||||
│
|
||||
▼
|
||||
Extrae: nombre, descripción, precio, peso estimado, imagen
|
||||
│
|
||||
▼
|
||||
Pre-llena el formulario de registro
|
||||
│
|
||||
▼
|
||||
Sistema calcula costo de envío:
|
||||
Peso (lbs) × tarifa_por_lb
|
||||
+ seguro (% del valor declarado)
|
||||
+ tasas aduaneras estimadas (FODINFA 0.5% + IVA 15%)
|
||||
│
|
||||
▼
|
||||
Muestra resumen al cliente para confirmación
|
||||
│
|
||||
▼
|
||||
Cliente confirma y paga
|
||||
│
|
||||
▼
|
||||
Sistema genera Tracking ID único
|
||||
Sistema envía correo de confirmación
|
||||
Sistema actualiza estado → REGISTRADO
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.2 Flujo de Calculadora Dinámica de Costos (SENAE)
|
||||
|
||||
La calculadora estará disponible tanto en la **landing page** (acceso público) como dentro del portal del cliente.
|
||||
|
||||
**Categorías de importación SENAE** (régimen 4x4 / simplificado):
|
||||
|
||||
| Categoría | Descripción | Arancel | Observaciones |
|
||||
|-----------|---------------------------------------------------|:-------:|--------------------------------------|
|
||||
| **4x4** | Hasta 4 envíos/año, hasta $400 c/u y hasta 4 kg | 0% | Régimen simplificado sin arancel |
|
||||
| **Cat. B**| Bienes de consumo general fuera del 4x4 | 10% | + FODINFA + IVA |
|
||||
| **Cat. C**| Textiles, calzado, artículos del hogar | 20% | + FODINFA + IVA |
|
||||
| **Cat. D**| Electrónicos, equipos de telecomunicaciones | 0-15% | Puede requerir certificación INEN |
|
||||
|
||||
**Variables de entrada:**
|
||||
```
|
||||
- Peso declarado (lbs o kg)
|
||||
- Dimensiones estimadas (para peso volumétrico)
|
||||
- Valor declarado del producto ($USD)
|
||||
- Categoría de producto (la calculadora sugiere la categoría)
|
||||
- Destino en Ecuador (ciudad / zona)
|
||||
```
|
||||
|
||||
**Fórmulas:**
|
||||
```
|
||||
Peso a cobrar = MAX(peso_real, peso_volumétrico)
|
||||
Peso_volumétrico = (largo × ancho × alto) / factor_dim (139 o 166)
|
||||
|
||||
Costo base de envío = Peso_a_cobrar × tarifa_zona
|
||||
Seguro = valor_declarado × 0.02 (2% — configurable)
|
||||
|
||||
FODINFA = valor_declarado × 0.005 (0.5%)
|
||||
Arancel = valor_declarado × % según categoría SENAE
|
||||
IVA = (valor_declarado + FODINFA + Arancel) × 0.15
|
||||
|
||||
TOTAL = costo_envío + seguro + FODINFA + Arancel + IVA
|
||||
```
|
||||
|
||||
**El sistema muestra el desglose completo al cliente antes de confirmar el pago.**
|
||||
|
||||
---
|
||||
|
||||
### 6.3 Flujo de Notificaciones Automáticas
|
||||
|
||||
```
|
||||
Cambio de estado del paquete
|
||||
│
|
||||
▼
|
||||
Sistema detecta el cambio en base de datos
|
||||
│
|
||||
▼
|
||||
Dispara evento al motor de notificaciones
|
||||
│
|
||||
├──► Correo electrónico (con detalle del estado)
|
||||
├──► SMS (resumen corto)
|
||||
├──► Notificación push en el portal web
|
||||
└──► Mensaje de WhatsApp (vía WhatsApp Business API)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6.4 Flujo de Seguridad y Auditoría (ISO 27001)
|
||||
|
||||
```
|
||||
Cualquier acción en el sistema
|
||||
│
|
||||
▼
|
||||
Se registra en Log de Auditoría:
|
||||
- ID del usuario
|
||||
- Rol
|
||||
- Acción realizada
|
||||
- Timestamp (UTC)
|
||||
- IP de origen
|
||||
- Recurso afectado (ej: Tracking ID)
|
||||
│
|
||||
▼
|
||||
Log inmutable (no editable, ni por admins)
|
||||
│
|
||||
▼
|
||||
Reportes de auditoría disponibles para Super Admin
|
||||
│
|
||||
▼
|
||||
Alertas automáticas ante:
|
||||
- Múltiples intentos de login fallidos
|
||||
- Acceso desde IPs inusuales (geolocalización)
|
||||
- Cambios masivos de datos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Integraciones Externas
|
||||
|
||||
| Integración | Propósito | Protocolo |
|
||||
|---------------------------|---------------------------------------------------------------------|-----------------|
|
||||
| Amazon SP-API | Obtener info del producto desde enlace (pre-llenar formulario) | REST API |
|
||||
| WebService SENAE | Enviar declaraciones simplificadas (DSI) / formales | SOAP / REST |
|
||||
| Pasarela de pagos | Cobro en línea (Stripe, PayPhone, PayPal, etc.) | REST API |
|
||||
| SMS Gateway | Notificaciones por SMS a clientes | REST API |
|
||||
| Servicio de correo | Notificaciones por email (SendGrid o AWS SES) | SMTP / REST |
|
||||
| Courier internacional | Estado de envío físico en EE.UU. (FedEx, DHL, UPS API) | REST API |
|
||||
| **WhatsApp Business API** | Notificaciones automáticas + botón flotante de contacto en la web | REST API |
|
||||
| **Software de bodega** | Sincronización en tiempo real de estados de tracking (propio) | REST API |
|
||||
| **Normas INEN** | Consulta de certificaciones para carga sujeta a reglamento técnico | REST API / Web |
|
||||
|
||||
---
|
||||
|
||||
## 8. Requerimientos No Funcionales
|
||||
|
||||
| Requerimiento | Detalle |
|
||||
|----------------------|-----------------------------------------------------------------------|
|
||||
| Disponibilidad | 99.9% uptime (SLA) |
|
||||
| Tiempo de respuesta | < 2 segundos para operaciones normales |
|
||||
| Escalabilidad | Arquitectura multi-tenant preparada para múltiples empresas |
|
||||
| Seguridad | ISO/IEC 27001, OWASP Top 10, TLS 1.3, AES-256, MFA obligatorio |
|
||||
| Accesibilidad | 100% web, responsive, accesible desde cualquier dispositivo |
|
||||
| Respaldo de datos | Backups automáticos diarios, retención mínima 90 días |
|
||||
| Recuperación | RTO < 4 horas, RPO < 1 hora |
|
||||
| Auditoría | Todos los eventos registrados con trazabilidad completa |
|
||||
|
||||
---
|
||||
|
||||
## 9. Consideraciones Adicionales
|
||||
|
||||
- **Identidad legal visible:** El sitio debe mostrar prominentemente que la operación está respaldada por dos empresas constituidas legalmente: Moraworld Imports S.A.S. (RUC Ecuador, Cuenca) y Mora Global Import LLC (New Jersey, EE.UU.).
|
||||
- **Representantes legales:** El botón de WhatsApp mostrará dos opciones de contacto: representante en NJ y representante legal en Cuenca.
|
||||
- **Protección de datos:** Los datos personales de clientes ecuatorianos están sujetos a la **Ley Orgánica de Protección de Datos Personales (LOPDP) de Ecuador** y, al procesarse en EE.UU., también a la normativa aplicable del estado de New Jersey.
|
||||
- **Umbral aduanero:** El régimen **4x4** aplica hasta 4 envíos por año, máximo $400 USD y 4 kg por envío. Todo paquete que exceda estos límites requiere proceso de importación formal (DAI).
|
||||
- **Categorías SENAE:** La calculadora debe actualizarse cuando el SENAE modifique las tablas arancelarias.
|
||||
- **Cumplimiento INEN:** Para carga pesada (calzado, textiles, electrónicos), verificar si el producto requiere Registro de Conformidad o Certificado INEN antes de importar.
|
||||
- **Consolidación:** El sistema soportará la consolidación de múltiples paquetes de un mismo cliente en un solo envío para optimizar costos.
|
||||
- **API propia de bodega:** El sistema se diseñará con una capa de integración que permita conectarse al software propietario de gestión de bodega en NJ.
|
||||
- **Multi-idioma:** Español e Inglés (para operadores en NJ).
|
||||
- **Zona horaria:** EST (New Jersey) y ECT (Ecuador) — el sistema mostrará los tiempos en la zona horaria del usuario.
|
||||
- **Estructura de navegación:** La web estará dividida claramente en dos servicios para no mezclarlos: **Casillero** y **Carga Pesada/Pallets**.
|
||||
|
||||
---
|
||||
|
||||
## 10. Estructura de Navegación del Sitio Web
|
||||
|
||||
```
|
||||
main/
|
||||
├── / (Landing Page principal)
|
||||
│ ├── /como-funciona
|
||||
│ ├── /calculadora
|
||||
│ ├── /tarifas
|
||||
│ └── /quienes-somos (legalidad, RUC, permisos, representantes)
|
||||
│
|
||||
├── /casillero/ (Servicio B2C)
|
||||
│ ├── /casillero/como-usar
|
||||
│ ├── /casillero/calculadora
|
||||
│ └── /casillero/registro
|
||||
│
|
||||
├── /carga-pesada/ (Servicio B2B)
|
||||
│ ├── /carga-pesada/como-funciona
|
||||
│ ├── /carga-pesada/cotizacion ← Formulario de cotización especial
|
||||
│ └── /carga-pesada/inen ← Info sobre normas INEN
|
||||
│
|
||||
├── /tracking ← Búsqueda pública de paquete por Tracking ID
|
||||
│
|
||||
└── /portal/ (App autenticada)
|
||||
├── /portal/dashboard
|
||||
├── /portal/mi-casillero
|
||||
├── /portal/pre-alerta
|
||||
├── /portal/mis-paquetes
|
||||
├── /portal/calculadora
|
||||
└── /portal/perfil
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Documento generado el 6 de mayo de 2026 — Sujeto a revisión y validación con el cliente.*
|
||||
*Versión 1.1 — Merge con documento de objetivos sociales y funcionales de Moraworld Imports.*
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
Documento de Objetivos Sociales y Funcionales: Moraworld Imports S.A.S. y Mora Global Import LLC
|
||||
1. Misión del Proyecto Digital
|
||||
Establecer un ecosistema logístico transfronterizo que conecte físicamente la bodega en New Jersey con el mercado de Ecuador, ofreciendo soluciones automatizadas de casillero (B2C) y gestión de carga pesada (B2B).
|
||||
2. Objetivos Operativos (Acciones Requeridas)
|
||||
• Automatización de Identidad Logística: Al registrarse, el cliente debe recibir automáticamente su dirección personalizada: 150 N Day St, Suite [Número de Casillero], City of Orange, NJ 07050. Ejemplo!
|
||||
150 N Day St, Suite [Número de Casillero], City of Orange, NJ 07050.
|
||||
|
||||
o Transparencia Tributaria: Implementar una calculadora que desglose el costo de flete y los impuestos de aduana en Ecuador según las categorías del SENAE (Categoría B/4x4, C, D, etc.).
|
||||
o Gestión Documental (Pre-alerta): Permitir que los usuarios carguen sus facturas comerciales de tiendas como Amazon o eBay antes de que el paquete llegue a la bodega.
|
||||
o Sincronización de Inventarios: El sistema debe estar preparado para conectarse vía API con el software de bodega para actualizar estados de rastreo (Tracking) en tiempo real.
|
||||
3. Objetivos Sociales y de Marca
|
||||
o Generación de Confianza: Destacar que somos una empresa constituida bajo las leyes de Ecuador (S.A.S.) y EE. UU. (LLC), con representantes legales visibles (Cuenca y New Jersey).
|
||||
o Accesibilidad: Facilitar canales de soporte directo (WhatsApp integrado) para asesoría en compras internacionales y cumplimiento de normas INEN (especialmente para carga de calzado al por mayor).
|
||||
|
||||
2. Funcionalidades Clave
|
||||
La web incluya estos 4 módulos críticos:
|
||||
• Panel de Usuario (Casillero): Donde el cliente vea su "Suite" o número de casillero, suba sus facturas de compra y vea fotos de sus paquetes cuando lleguen a tu bodega en NJ.
|
||||
• Calculadora Dinámica: Una herramienta donde el usuario ponga el peso (libras) y el valor del producto, y la web le desglose automáticamente:
|
||||
o Costo del flete.
|
||||
o Impuestos estimados según la categoría de Aduana (4x4, Categoría C, etc.).
|
||||
• Sistema de Pre-alerta: Un formulario donde el cliente avise: "Compré unos zapatos en Amazon, el tracking es X y vale $100". Esto te permite a ti organizar tu bodega antes de que llegue el paquete.
|
||||
• Rastreo (Tracking) en Tiempo .Real: Un campo donde el cliente ponga su número de guía y vea si el paquete está "En Bodega NJ", "En Tránsito", "En Aduana Ecuador" o "Listo para entrega".
|
||||
3 La Integración con tu Software Propio
|
||||
Como planeas tener tu propio software, esto es lo más técnico que debes decirles:
|
||||
"La página web debe conectarse vía API con mi software de gestión de bodega. Necesito que el registro de la web cree un usuario en mi base de datos de logística de forma automática."
|
||||
|
||||
4. Estructura de Navegación
|
||||
La web debe estar dividida en dos grandes servicios para que no se mezclen:
|
||||
1. Servicio de Casillero: Para compras pequeñas (Amazon, eBay, ropa, tecnología).
|
||||
2. Servicio de Carga Pesada/Pallets: Para importadores mayoristas (como el caso de los 1,000 pares de zapatos). Aquí el flujo no es automático, sino un formulario de "Cotización Especial".
|
||||
5. Confianza y Contacto
|
||||
Para el mercado de Ecuador, la confianza es todo. Pide:
|
||||
• Botón de WhatsApp Flotante: Que dé la opción de hablar contigo en NJ o con tu padre (Representante Legal) en Cuenca.
|
||||
• Sección de Legalidad: Donde se muestre que son una empresa constituida (Moraworld Imports S.A.S.) con RUC y permisos de aduana.
|
||||
Reference in New Issue
Block a user