Files
moraworld/apps/web/src/app/_components/whatsapp-float.tsx
T
Lizandro Guarnizo 5565eef554 feat: payments module, notification templates, WA float, route fixes
- Add PaymentsModule: POST /payments/intent, POST /:id/confirm, GET /payments, GET /payments/package/:id, GET /payments/track/:trackingId
- Add Payment model to Prisma schema (PaymentStatus enum, Payment table)
- Add NotificationTemplate model + NotificationsController (GET/PUT /notification-templates, POST /notification-templates/seed)
- Update NotificationsService: DB-backed templates with variable interpolation {{trackingId}} {{firstName}} {{status}} {{suiteCode}}
- Fix /bodega/paquetes: replace 11 wrong status strings with correct §08 enum values
- Fix /admin/reportes: replace EN_CAMINO_A_ECUADOR with correct §08 statuses, rewrite report page with proper KPIs and bar charts
- Fix /portal/mis-paquetes: correct §08 statuses, add 'Pagar envío' button for VERIFICADO/DECLARACION_ADUANERA packages
- Add WhatsApp float component (_components/whatsapp-float.tsx, 2 contacts: NJ ops + Cuenca aduana)
- Add /casillero/calculadora and /casillero/registro redirects (§20)
- Add /portal/pago payment page with cost breakdown (§09/§14/§15)
- Add /admin/notificaciones page: view/edit/toggle templates per event×channel
- Admin nav: add Notificaciones link
- api.ts: add notificationTemplates.* and payments.* client methods
- schema.prisma v0.4: PaymentStatus enum, Payment model, NotificationTemplate model
- db push applied to remote DB (46.202.93.92)
- All builds pass (API nest build + Next.js build)
2026-06-01 17:09:05 -05:00

94 lines
4.1 KiB
TypeScript

"use client";
import { useState } from "react";
// §07 + §21 — Botón flotante WhatsApp con dos contactos
// Reemplaza los números de teléfono con los reales antes de producción.
const CONTACTS = [
{
label: "Operaciones NJ",
sub: "150 N Day St · New Jersey",
phone: "12015550100", // ← reemplazar con número real
msg: "Hola, tengo una consulta sobre mi paquete en New Jersey.",
},
{
label: "Aduana · Cuenca",
sub: "Moraworld Imports S.A.S.",
phone: "593987654321", // ← reemplazar con número real
msg: "Hola, necesito información sobre trámites aduaneros en Ecuador.",
},
];
export function WhatsAppFloat() {
const [open, setOpen] = useState(false);
return (
<div
style={{
position: "fixed", bottom: "1.5rem", right: "1.5rem",
zIndex: 9999, display: "flex", flexDirection: "column",
alignItems: "flex-end", gap: ".75rem",
}}
>
{/* Opciones desplegables */}
{open && (
<div style={{ display: "flex", flexDirection: "column", gap: ".5rem" }}>
{CONTACTS.map(c => (
<a
key={c.phone}
href={`https://wa.me/${c.phone}?text=${encodeURIComponent(c.msg)}`}
target="_blank"
rel="noopener noreferrer"
style={{
display: "flex", alignItems: "center", gap: ".75rem",
background: "#fff", borderRadius: 12, padding: ".75rem 1rem",
boxShadow: "0 4px 20px rgba(0,0,0,.15)",
textDecoration: "none", color: "#111827",
minWidth: 220, transition: "transform .15s",
}}
onMouseEnter={e => { e.currentTarget.style.transform = "translateX(-4px)"; }}
onMouseLeave={e => { e.currentTarget.style.transform = "translateX(0)"; }}
>
<span style={{ fontSize: "1.4rem", lineHeight: 1 }}>💬</span>
<div>
<div style={{ fontWeight: 700, fontSize: ".88rem" }}>{c.label}</div>
<div style={{ fontSize: ".75rem", color: "#6B7280" }}>{c.sub}</div>
</div>
</a>
))}
</div>
)}
{/* Botón principal */}
<button
onClick={() => setOpen(o => !o)}
aria-label="Contactar por WhatsApp"
style={{
width: 56, height: 56, borderRadius: "50%",
background: "#25D366", border: "none", cursor: "pointer",
display: "flex", alignItems: "center", justifyContent: "center",
boxShadow: "0 4px 16px rgba(37,211,102,.45)",
transition: "transform .2s, box-shadow .2s",
transform: open ? "rotate(45deg)" : "rotate(0deg)",
}}
onMouseEnter={e => {
e.currentTarget.style.transform = open ? "rotate(45deg) scale(1.1)" : "scale(1.1)";
}}
onMouseLeave={e => {
e.currentTarget.style.transform = open ? "rotate(45deg)" : "scale(1)";
}}
>
{open ? (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="white" strokeWidth="2.5" strokeLinecap="round" />
</svg>
) : (
<svg width="26" height="26" viewBox="0 0 24 24" fill="white">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z" />
<path d="M12 2C6.477 2 2 6.477 2 12c0 1.89.525 3.66 1.438 5.168L2 22l4.948-1.42A9.956 9.956 0 0012 22c5.523 0 10-4.477 10-10S17.523 2 12 2z" />
</svg>
)}
</button>
</div>
);
}