almeira-web/dripzone-app/src/app/checkout/page.tsx

1009 lines
48 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { ArrowLeft, CheckCircle2, ShieldCheck, Truck, Zap, QrCode, Phone, MapPin, User, MessageCircle, ExternalLink, CreditCard, AlertCircle } from 'lucide-react';
import { City, PaymentMethod, Order, StoreSettings } from '@/types';
import { INITIAL_CITIES, INITIAL_PAYMENT_METHODS, INITIAL_PRODUCTS } from '@/lib/data-service';
import { formatCOP } from '@/lib/pricing-engine';
import { useBrand } from '@/context/BrandContext';
import { useAuth } from '@/context/AuthContext';
import { renderClientTemplate } from '@/lib/templates-client';
import confetti from 'canvas-confetti';
import { OptimizedImage } from '@/components/ui/OptimizedImage';
export default function CheckoutPage() {
const { currentBrand } = useBrand();
const { user } = useAuth();
const [customerName, setCustomerName] = useState('');
const [customerPhone, setCustomerPhone] = useState('');
const [address, setAddress] = useState('');
const [neighborhood, setNeighborhood] = useState('');
const [notes, setNotes] = useState('');
const [orderSource, setOrderSource] = useState<string>('web');
const [cities, setCities] = useState<City[]>(INITIAL_CITIES);
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>(INITIAL_PAYMENT_METHODS);
const [selectedCityId, setSelectedCityId] = useState<string>('city-cali');
const [selectedPaymentId, setSelectedPaymentId] = useState<string>('pay-deposit-10k');
const [storeSettings, setStoreSettings] = useState<StoreSettings | null>(null);
const [checkoutItems, setCheckoutItems] = useState<Array<{
productId: string;
productName: string;
brand: string;
size: string;
quantity: number;
price: number;
image: string;
availableSizes?: string[];
}>>([]);
const handleUpdateItemSize = (index: number, newSize: string) => {
setCheckoutItems((prev) => {
const copy = [...prev];
if (copy[index]) {
copy[index] = { ...copy[index], size: newSize };
}
try {
localStorage.setItem('dripzone_cart', JSON.stringify(copy));
} catch (_) {}
return copy;
});
};
const [isSubmitting, setIsSubmitting] = useState(false);
const [orderCompleted, setOrderCompleted] = useState<Order | null>(null);
const [userRequiresDeposit, setUserRequiresDeposit] = useState<boolean>(true);
const [userDepositReason, setUserDepositReason] = useState<string>('Garantía de separación requerida para el primer pedido');
const [showOtpModal, setShowOtpModal] = useState<boolean>(false);
const [otpCode, setOtpCode] = useState<string>('');
const [otpLoading, setOtpLoading] = useState<boolean>(false);
const [otpError, setOtpError] = useState<string>('');
// Estados para pasarela Bold & QR Bre-B
const [boldPayment, setBoldPayment] = useState<{
paymentLink: string;
checkoutUrl: string;
qrImageUrl: string;
amount: number;
environment?: string;
} | null>(null);
const [boldPaid, setBoldPaid] = useState(false);
const [boldLoading, setBoldLoading] = useState(false);
useEffect(() => {
fetch('/api/cities')
.then((r) => r.json())
.then((d) => d.success && setCities(d.cities))
.catch(() => {});
fetch('/api/payment-methods')
.then((r) => r.json())
.then((d) => d.success && setPaymentMethods(d.paymentMethods))
.catch(() => {});
fetch('/api/settings')
.then((r) => r.json())
.then((d) => d.success && setStoreSettings(d.settings))
.catch(() => {});
// Carga dinámica de productos desde URL (WhatsApp Catalog / Asesor) o Carrito local
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
const urlProductId = params.get('productId') || params.get('id');
const urlSize = params.get('size');
const urlPrice = params.get('price') ? parseInt(params.get('price')!, 10) : undefined;
const urlName = params.get('name');
const urlSource = params.get('source');
if (urlSource === 'whatsapp' || urlProductId) {
setOrderSource('whatsapp');
}
if (urlProductId) {
fetch('/api/products?all=true')
.then((r) => r.json())
.then((d) => {
if (d.success && Array.isArray(d.products)) {
const matched = d.products.find((p: any) => p.id === urlProductId);
if (matched) {
const item = {
productId: matched.id,
productName: matched.name,
brand: matched.brand || 'Sneakers',
size: urlSize || (matched.sizes?.[0] || '39'),
quantity: 1,
price: urlPrice || matched.retailPrice || 125000,
image: matched.images?.[0] || matched.colors?.[0]?.image || '/splash.png',
availableSizes: matched.sizes || ['37', '38', '39', '40', '41', '42', '43'],
};
setCheckoutItems([item]);
localStorage.setItem('dripzone_cart', JSON.stringify([item]));
return;
}
}
})
.catch(() => {});
} else if (urlName) {
const item = {
productId: `custom-${Date.now()}`,
productName: decodeURIComponent(urlName),
brand: 'DripZone',
size: urlSize || '39',
quantity: 1,
price: urlPrice || 125000,
image: params.get('image') ? decodeURIComponent(params.get('image')!) : '/splash.png',
availableSizes: ['37', '38', '39', '40', '41', '42', '43'],
};
setCheckoutItems([item]);
return;
}
// Si no viene en URL, intentar cargar de localStorage
try {
const stored = localStorage.getItem('dripzone_cart');
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed) && parsed.length > 0) {
setCheckoutItems(
parsed.map((item: any) => ({
productId: item.productId || item.product?.id || `item-${Date.now()}`,
productName: item.productName || item.product?.name || 'Zapatillas DripZone',
brand: item.brand || item.product?.brand || 'Sneakers',
size: item.size || item.selectedSize || '39',
quantity: item.quantity || 1,
price: item.price || item.product?.retailPrice || 125000,
image: item.image || item.product?.images?.[0] || '/splash.png',
availableSizes: item.availableSizes || item.product?.sizes || ['37', '38', '39', '40', '41', '42', '43'],
}))
);
return;
}
}
} catch (_) {}
// Fallback a productos activos
fetch('/api/products')
.then((r) => r.json())
.then((d) => {
if (d.success && d.products?.length > 0) {
const first = d.products[0];
setCheckoutItems([
{
productId: first.id,
productName: first.name,
brand: first.brand || 'Sneakers',
size: first.sizes?.[0] || '39',
quantity: 1,
price: first.retailPrice || 125000,
image: first.images?.[0] || first.colors?.[0]?.image || '/splash.png',
availableSizes: first.sizes || ['37', '38', '39', '40', '41', '42', '43'],
},
]);
}
})
.catch(() => {});
}
}, []);
useEffect(() => {
if (user) {
setUserRequiresDeposit((user.totalOrders || 0) === 0 || user.requiresShippingDeposit === true);
setUserDepositReason(user.depositReason || '');
}
}, [user]);
useEffect(() => {
const clean = customerPhone.replace(/\D/g, '');
if (clean.length >= 10) {
fetch(`/api/users?phone=${clean}`)
.then((r) => r.json())
.then((data) => {
if (data.success) {
setUserRequiresDeposit(data.requiresShippingDeposit !== false);
setUserDepositReason(data.depositReason || '');
}
})
.catch(() => {});
}
}, [customerPhone]);
const activeCity = cities.find((c) => c.id === selectedCityId) || cities[0];
const activePayment = paymentMethods.find((p) => p.id === selectedPaymentId) || paymentMethods[0];
const subtotal = checkoutItems.reduce((acc, it) => acc + it.price * it.quantity, 0);
const shippingCost = activeCity.shippingCost;
const totalAmount = subtotal + shippingCost;
// Lógica de Cobro:
// - Si el método es Bold 100% en línea (PSE, Tarjetas, Nequi, etc. sin separación): cobra el totalAmount.
// - Si el método requiere separación (ej. $10.000 COP) o el usuario la tiene obligatoria: cobra el anticipo ($10.000 COP) y el saldo queda contraentrega.
// - Si es 100% contraentrega en efectivo (ej. Cali): depósito $0 y saldo total contraentrega.
const is100PercentOnline = activePayment?.type === 'bold' && !activePayment?.requiresDeposit;
const isSeparation = Boolean(activePayment?.requiresDeposit || userRequiresDeposit);
const isMandatoryDeposit = isSeparation;
const depositAmount = is100PercentOnline
? totalAmount
: isSeparation
? (activePayment?.depositAmount || 10000)
: 0;
const balanceDue = Math.max(0, totalAmount - depositAmount);
const officialWhatsapp = storeSettings?.whatsappNumber || '573106390296';
const boldLink = activePayment?.boldPaymentLink || storeSettings?.boldPaymentLink || 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C';
const boldEnv = storeSettings?.boldEnvironment || activePayment?.environment || 'production';
const processOrderSubmission = async () => {
setIsSubmitting(true);
try {
const orderPayload = {
customerName,
customerPhone,
address: `${address} - Barrio: ${neighborhood || 'Centro'}`,
cityId: activeCity.id,
cityName: activeCity.name,
items: checkoutItems,
subtotal,
shippingCost,
totalAmount,
depositAmount,
balanceDue,
paymentMethodId: activePayment.id,
paymentMethodName: activePayment.name,
notes: notes ? `${notes} (Origen: ${orderSource})` : `Origen: ${orderSource}`,
source: orderSource,
};
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderPayload),
});
const data = await res.json();
if (data.success) {
setOrderCompleted(data.order);
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
});
// Si la orden requiere cobro en línea vía Bold (anticipo de separación o pago total 100%)
if (depositAmount > 0) {
setBoldLoading(true);
fetch('/api/bold/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
orderId: data.order.id,
amount: depositAmount,
type: is100PercentOnline ? 'full' : 'deposit',
payerEmail: '',
redirectionUrl: window.location.href,
}),
})
.then((r) => r.json())
.then((bData) => {
if (bData.success) {
setBoldPayment(bData);
}
})
.catch((err) => console.error('Error generando cobro Bold:', err))
.finally(() => setBoldLoading(false));
}
} else {
alert(data.error || 'Error al registrar pedido.');
}
} catch (err) {
console.error('Error creando orden:', err);
alert('Error de conexión al registrar pedido.');
} finally {
setIsSubmitting(false);
}
};
// Escuchador en tiempo real del estado de pago en Bold
useEffect(() => {
if (!boldPayment?.paymentLink || boldPaid || !orderCompleted?.id) return;
const interval = setInterval(async () => {
try {
const res = await fetch(`/api/bold/status?linkId=${boldPayment.paymentLink}&orderId=${orderCompleted.id}`);
const data = await res.json();
if (data.success && data.isPaid) {
setBoldPaid(true);
confetti({
particleCount: 150,
spread: 90,
origin: { y: 0.5 },
});
}
} catch (_) {}
}, 3000);
return () => clearInterval(interval);
}, [boldPayment, boldPaid, orderCompleted]);
const handleSubmitOrder = async (e: React.FormEvent) => {
e.preventDefault();
if (!customerName || !customerPhone || !address) {
alert('Por favor completa tu nombre, celular y dirección para el despacho.');
return;
}
const cleanPhone = customerPhone.replace(/\D/g, '');
if (cleanPhone.length < 10) {
alert('Por favor ingresa un número de celular/WhatsApp válido a 10 dígitos.');
return;
}
if (user?.phoneVerified && user.phone === cleanPhone) {
await processOrderSubmission();
return;
}
// Solicitar OTP por WhatsApp
setOtpLoading(true);
setOtpError('');
try {
const res = await fetch('/api/auth/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: cleanPhone, name: customerName }),
});
const data = await res.json();
if (data.success) {
setShowOtpModal(true);
} else {
alert(data.error || 'No se pudo enviar el código OTP a WhatsApp.');
}
} catch (err) {
alert('Error al solicitar código OTP por WhatsApp.');
} finally {
setOtpLoading(false);
}
};
const handleVerifyOtpAndPlaceOrder = async () => {
if (otpCode.length < 6) {
setOtpError('Ingresa el código de 6 dígitos enviado a tu WhatsApp.');
return;
}
setOtpLoading(true);
setOtpError('');
try {
const cleanPhone = customerPhone.replace(/\D/g, '');
const res = await fetch('/api/auth/verify-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identifier: cleanPhone,
code: otpCode,
name: customerName,
city: activeCity.name,
address: `${address} - Barrio: ${neighborhood || 'Centro'}`,
}),
});
const data = await res.json();
if (data.success) {
setShowOtpModal(false);
await processOrderSubmission();
} else {
setOtpError(data.error || 'Código incorrecto o expirado.');
}
} catch (err) {
setOtpError('Error verificando código.');
} finally {
setOtpLoading(false);
}
};
const handleOpenWhatsAppOrder = (order: Order) => {
const itemsSummary = order.items
.map((i) => `• ${i.productName} (Talla ${i.size}) x${i.quantity} = ${formatCOP(i.price * i.quantity)}`)
.join('\n');
const paymentCondition = order.depositAmount > 0
? `⚡ *Separación:* ${formatCOP(order.depositAmount)}\n🤝 *Saldo Contraentrega:* ${formatCOP(order.balanceDue)}`
: '🤝 *Pago 100% Contraentrega.*';
const rawText = renderClientTemplate('order_checkout_wa', {
tienda_nombre: currentBrand.name,
orden_id: order.id,
cliente_nombre: order.customerName,
cliente_telefono: order.customerPhone,
cliente_direccion: order.address,
cliente_ciudad: order.cityName,
articulos_resumen: itemsSummary,
total: formatCOP(order.totalAmount),
condicion_pago_resumen: paymentCondition,
metodo_pago: order.paymentMethodName,
saldo_contraentrega: formatCOP(order.balanceDue),
});
window.open(`https://wa.me/${officialWhatsapp}?text=${encodeURIComponent(rawText)}`, '_blank');
};
if (orderCompleted) {
return (
<div className={`min-h-screen ${currentBrand.colors.bodyBg} flex items-center justify-center p-4`}>
<div className={`bg-neutral-900 border ${currentBrand.colors.border} rounded-3xl max-w-lg w-full p-6 sm:p-8 shadow-2xl text-center space-y-5 animate-in zoom-in-95 duration-300`}>
<div className={`w-16 h-16 rounded-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white flex items-center justify-center mx-auto shadow-md`}>
<CheckCircle2 className="w-10 h-10" />
</div>
<div>
<span className={`text-xs font-bold bg-white/10 text-white px-3 py-1 rounded-full uppercase tracking-wider border border-white/10`}>
¡Pedido Registrado con Éxito!
</span>
<h2 className="text-2xl font-black text-white mt-2">Orden #{orderCompleted.id}</h2>
<p className="text-xs text-neutral-400 mt-1">
Hemos recibido tu solicitud para entrega en <strong>{orderCompleted.cityName}</strong> con <strong>{currentBrand.name}</strong>.
</p>
</div>
{/* Bold.co QR Bre-B and Separation Box */}
{orderCompleted.depositAmount > 0 ? (
boldPaid ? (
<div className="bg-emerald-950/60 border border-emerald-500/50 rounded-2xl p-5 text-center space-y-3 shadow-lg animate-in zoom-in-95">
<div className="w-12 h-12 bg-emerald-500/20 text-emerald-400 rounded-full flex items-center justify-center mx-auto">
<CheckCircle2 className="w-7 h-7" />
</div>
<h4 className="text-base font-black text-emerald-300">¡Anticipo de $10.000 COP Confirmado con Éxito!</h4>
<p className="text-xs text-neutral-300 leading-relaxed">
Tu pago fue procesado correctamente mediante <strong>Bold</strong>. Tu orden ha sido autorizada para alistamiento prioritario en bodega y despacho inmediato.
</p>
<div className="bg-black/50 p-3 rounded-xl border border-emerald-500/30 text-xs flex justify-between items-center">
<span className="text-neutral-400">🤝 Saldo a Pagar Contraentrega:</span>
<strong className="text-emerald-400 font-mono text-sm">
{formatCOP(Math.max(0, orderCompleted.totalAmount - (orderCompleted.depositAmount || 10000)))} COP
</strong>
</div>
</div>
) : (
<div className="bg-neutral-950 border border-purple-500/40 rounded-2xl p-4 sm:p-5 text-left space-y-3.5 shadow-xl">
<div className="flex items-center justify-between border-b border-white/10 pb-2.5">
<div className="flex items-center space-x-2 text-white font-bold text-xs uppercase">
<QrCode className="w-4 h-4 text-purple-400" />
<span>
{orderCompleted.balanceDue === 0
? `Paso 2: Pago en Línea 100% Seguro (${formatCOP(orderCompleted.totalAmount)})`
: `Paso 2: Anticipo de Separación (${formatCOP(orderCompleted.depositAmount || 10000)})`}
</span>
</div>
<div className="flex items-center gap-2">
{boldPayment?.environment === 'sandbox' || boldEnv === 'sandbox' ? (
<span className="bg-amber-500/20 text-amber-300 border border-amber-500/40 text-[9px] px-2 py-0.5 rounded-full font-black flex items-center gap-1 shadow-xs">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-ping" />
SANDBOX (PRUEBAS)
</span>
) : (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 text-[9px] px-2 py-0.5 rounded-full font-black flex items-center gap-1 shadow-xs">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
PRODUCCIÓN (LIVE)
</span>
)}
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] px-2.5 py-0.5 rounded-full font-bold">
QR Bre-B / Bold
</span>
</div>
</div>
<p className="text-xs text-neutral-300 leading-relaxed">
Escanea el <strong>Código QR Bre-B</strong> o toca el botón para pagar en línea de forma 100% segura con <strong>Bold</strong>:
</p>
{boldPayment?.qrImageUrl ? (
<div className="flex flex-col items-center justify-center gap-2 bg-white p-4 rounded-2xl shadow-md border-2 border-purple-500/30">
<img
src={boldPayment.qrImageUrl}
alt="Código QR Bre-B Bold"
className="w-48 h-48 sm:w-56 sm:h-56 object-contain rounded-lg"
/>
<div className="text-center space-y-0.5">
<p className="text-xs font-black text-neutral-900">
Total a Pagar: {formatCOP(orderCompleted.depositAmount || (orderCompleted.balanceDue === 0 ? orderCompleted.totalAmount : 10000))} COP
</p>
<p className="text-[10px] font-semibold text-neutral-600">
⚡ Compatible con Bre-B e Interoperabilidad Bancaria
</p>
</div>
</div>
) : boldLoading ? (
<div className="py-8 text-center text-xs text-purple-400 font-bold animate-pulse">
Generando Código QR Bre-B con Bold...
</div>
) : null}
<div className="space-y-2">
{boldPayment?.checkoutUrl && (
<a
href={boldPayment.checkoutUrl}
target="_blank"
rel="noopener noreferrer"
className="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-black text-xs py-3 px-4 rounded-xl flex items-center justify-center gap-2 shadow-lg transition-all"
>
<CreditCard className="w-4 h-4" />
<span>
Pagar {formatCOP(orderCompleted.depositAmount || (orderCompleted.balanceDue === 0 ? orderCompleted.totalAmount : 10000))} con PSE, Tarjetas, Nequi o Bre-B
</span>
<ExternalLink className="w-3.5 h-3.5" />
</a>
)}
</div>
<div className="pt-2 border-t border-white/10 flex justify-between items-center text-xs">
<span className="text-neutral-400">🤝 Saldo restante contraentrega:</span>
<span className="text-emerald-400 font-bold font-mono">
{orderCompleted.balanceDue === 0
? '$0 COP (Despacho Prioritario)'
: `${formatCOP(orderCompleted.balanceDue || Math.max(0, orderCompleted.totalAmount - (orderCompleted.depositAmount || 10000)))} COP`}
</span>
</div>
<div className="flex items-center justify-center gap-1.5 text-[10px] text-purple-300/80 font-medium text-center">
<div className="w-2 h-2 rounded-full bg-purple-400 animate-ping" />
<span>Esperando confirmación en tiempo real...</span>
</div>
</div>
)
) : (
<div className="bg-neutral-950 border border-white/10 rounded-2xl p-4 text-left space-y-2">
<p className="text-xs text-neutral-300 font-semibold">
¡Cliente Frecuente! Tu pedido quedó programado con 100% Pago Contraentrega al recibir en tu puerta.
</p>
</div>
)}
<div className="flex flex-col gap-2 pt-2">
<button
onClick={() => handleOpenWhatsAppOrder(orderCompleted)}
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-sm py-3 px-4 rounded-xl flex items-center justify-center space-x-2 shadow-lg shadow-emerald-600/20 active:scale-95 transition-all cursor-pointer"
>
<MessageCircle className="w-4 h-4 fill-white" />
<span>Confirmar y Enviar Comprobante por WhatsApp</span>
</button>
<Link
href="/"
className="w-full bg-neutral-800 hover:bg-neutral-700 text-neutral-300 font-bold text-xs py-2.5 px-4 rounded-xl text-center transition-all border border-neutral-700"
>
Volver a la Tienda
</Link>
</div>
</div>
</div>
);
}
return (
<div className={`min-h-screen ${currentBrand.colors.bodyBg} py-8 px-4 font-sans`}>
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<Link
href="/"
className="flex items-center space-x-2 text-xs font-bold text-neutral-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span>Regresar a {currentBrand.name}</span>
</Link>
<div className="flex items-center space-x-1.5 text-xs font-bold text-white bg-white/10 border border-white/10 px-3 py-1 rounded-full shadow-sm">
<ShieldCheck className="w-4 h-4" />
<span>Compra Segura & Garantizada</span>
</div>
</div>
{/* Banner de Pedido Originado en WhatsApp */}
{orderSource === 'whatsapp' && (
<div className="p-4 sm:p-5 rounded-3xl bg-gradient-to-r from-emerald-950/70 via-neutral-900 to-neutral-950 border border-emerald-500/40 shadow-xl flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-start gap-3.5">
<div className="w-10 h-10 rounded-2xl bg-emerald-500/20 text-emerald-400 flex items-center justify-center shrink-0 border border-emerald-500/30 mt-0.5">
<MessageCircle className="w-5 h-5" />
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<h4 className="text-sm font-black text-white">¡Pedido iniciado desde WhatsApp!</h4>
<span className="text-[10px] bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 px-2 py-0.5 rounded-full font-bold uppercase tracking-wider">
Atención Comercial
</span>
</div>
<p className="text-xs text-neutral-300 leading-relaxed">
Tu calzado y talla seleccionada están reservados. Completa tu dirección y confirma el anticipo de <strong className="text-emerald-400">$10.000 en línea</strong> (PSE, Tarjetas Débito/Crédito, Nequi o Bre-B) para asegurar tu despacho hoy. El saldo restante lo pagas <strong>contraentrega (en efectivo o transferencia)</strong> al recibir en tu puerta.
</p>
</div>
</div>
<a
href="/app-dripzone.apk"
download="app-dripzone.apk"
className="px-3.5 py-2 bg-neutral-900 hover:bg-neutral-800 border border-neutral-700 hover:border-purple-500/50 text-neutral-300 hover:text-white rounded-xl text-xs font-bold flex items-center gap-1.5 shrink-0 transition-all self-start sm:self-auto"
>
<Zap className="w-3.5 h-3.5 text-purple-400" />
<span>O pedir en App Móvil</span>
<ExternalLink className="w-3 h-3 text-neutral-500" />
</a>
</div>
)}
<form onSubmit={handleSubmitOrder} className="grid grid-cols-1 md:grid-cols-12 gap-6">
{/* Left Column: Shipping & Payment Details (7 cols) */}
<div className="md:col-span-7 space-y-5">
{/* 1. Datos del Cliente */}
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-5 sm:p-6 shadow-sm space-y-4">
<h3 className="font-black text-white text-base flex items-center gap-2">
<User className="w-5 h-5 text-white" />
<span>1. Datos de Entrega</span>
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-bold text-neutral-300 mb-1">Nombre Completo *</label>
<input
type="text"
required
placeholder="Ej: Juan Pérez"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-sm text-white focus:border-purple-400 outline-hidden"
/>
</div>
<div>
<label className="block text-xs font-bold text-neutral-300 mb-1">Celular / WhatsApp *</label>
<input
type="tel"
required
placeholder="Ej: 3XX XXX XXXX"
value={customerPhone}
onChange={(e) => setCustomerPhone(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-sm text-white focus:border-purple-400 outline-hidden"
/>
</div>
</div>
{/* City Selection */}
<div>
<label className="block text-xs font-bold text-neutral-300 mb-1">Ciudad de Despacho *</label>
<select
value={selectedCityId}
onChange={(e) => setSelectedCityId(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2.5 text-sm text-white focus:border-purple-400 font-semibold outline-hidden"
>
{cities.map((city) => (
<option key={city.id} value={city.id} className="bg-neutral-900">
{city.name} — {city.shippingCost === 0 ? 'Envío Gratis' : formatCOP(city.shippingCost)} ({city.deliveryTime})
</option>
))}
</select>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-bold text-neutral-300 mb-1">Dirección de Entrega *</label>
<input
type="text"
required
placeholder="Ej: Calle 123 # 45 - 67, Apto 201"
value={address}
onChange={(e) => setAddress(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-sm text-white focus:border-purple-400 outline-hidden"
/>
</div>
<div>
<label className="block text-xs font-bold text-neutral-300 mb-1">Barrio / Sector</label>
<input
type="text"
placeholder="Ej: Barrio San Fernando / Casa blanca"
value={neighborhood}
onChange={(e) => setNeighborhood(e.target.value)}
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-sm text-white focus:border-purple-400 outline-hidden"
/>
</div>
</div>
</div>
{/* 2. Método de Pago */}
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-5 sm:p-6 shadow-sm space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<h3 className="font-black text-white text-base flex items-center gap-2">
<Zap className="w-5 h-5 text-purple-400" />
<span>2. Selecciona tu Método de Pago</span>
</h3>
{boldEnv === 'sandbox' ? (
<span className="bg-amber-500/20 text-amber-300 border border-amber-500/40 text-[10px] font-black px-2.5 py-1 rounded-full flex items-center gap-1.5 shadow-sm w-fit">
<span className="w-2 h-2 rounded-full bg-amber-400 animate-ping shrink-0" />
<span>MODO PRUEBAS (SANDBOX)</span>
</span>
) : (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 text-[10px] font-black px-2.5 py-1 rounded-full flex items-center gap-1.5 shadow-sm w-fit">
<span className="w-2 h-2 rounded-full bg-emerald-400 shrink-0" />
<span>PRODUCCIÓN (LIVE)</span>
</span>
)}
</div>
{boldEnv === 'sandbox' && (
<div className="bg-amber-950/40 border border-amber-500/30 rounded-2xl p-3 flex items-start gap-2.5 text-xs text-amber-200">
<AlertCircle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<div>
<p className="font-bold text-amber-300">Pasarela Bold en Modo Pruebas Activo</p>
<p className="text-[11px] text-amber-200/80 mt-0.5">Estás en entorno Sandbox. Los pagos generados con Bold son de prueba y no debitarán dinero real de tus cuentas.</p>
</div>
</div>
)}
<div className="space-y-2.5">
{paymentMethods.map((method) => {
const isSelected = selectedPaymentId === method.id;
const isBold = method.type === 'bold' || Boolean(method.boldChannel);
return (
<div
key={method.id}
onClick={() => setSelectedPaymentId(method.id)}
className={`p-4 rounded-2xl border transition-all cursor-pointer space-y-1.5 ${
isSelected
? `border-purple-400 bg-purple-950/20 shadow-md`
: 'border-neutral-800 hover:border-neutral-700 bg-neutral-950'
}`}
>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div className="flex items-center space-x-2">
<div
className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
isSelected ? 'border-purple-400' : 'border-neutral-600'
}`}
>
{isSelected && <div className="w-2 h-2 rounded-full bg-purple-400" />}
</div>
<span className="font-black text-white text-sm">{method.name}</span>
</div>
<div className="flex flex-wrap items-center gap-1.5 pl-6 sm:pl-0">
{/* IDENTIFICADOR DE ENTORNO EN CADA MÉTODO */}
{isBold && (
boldEnv === 'sandbox' ? (
<span className="bg-amber-500/20 text-amber-300 border border-amber-500/40 text-[9px] font-black px-2 py-0.5 rounded-full flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
PRUEBAS SANDBOX
</span>
) : (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 text-[9px] font-black px-2 py-0.5 rounded-full flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
EN VIVO LIVE
</span>
)
)}
{method.badge ? (
<span className="bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 text-[10px] font-black px-2 py-0.5 rounded-full uppercase">
{method.badge}
</span>
) : method.requiresDeposit ? (
<span className="bg-white/10 text-white border border-white/20 text-[10px] font-black px-2 py-0.5 rounded-full uppercase">
Separa con {formatCOP(method.depositAmount || 10000)}
</span>
) : method.type === 'bold' ? (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-[10px] font-bold px-2 py-0.5 rounded-full uppercase">
100% en Línea
</span>
) : null}
</div>
</div>
<p className="text-xs text-neutral-400 pl-6">{method.description}</p>
{isSelected && (method.accountInfo || (method.type === 'bold' && boldLink)) && (
<div className="ml-6 mt-2 bg-neutral-900 p-3 rounded-xl border border-white/10 text-xs space-y-2 text-white">
{method.accountInfo && <p className="font-semibold">{method.accountInfo}</p>}
{method.type === 'bold' && (method.boldPaymentLink || boldLink) && (
<a
href={method.boldPaymentLink || boldLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold px-3 py-1.5 rounded-lg shadow-sm"
>
<ExternalLink className="w-3.5 h-3.5" />
<span>Pagar en Línea con Bold (PSE, Tarjetas, Nequi o Bre-B)</span>
</a>
)}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
{/* Right Column: Summary (5 cols) */}
<div className="md:col-span-5 space-y-5">
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-5 sm:p-6 shadow-sm space-y-4 sticky top-20">
<h3 className="font-black text-white text-base">Resumen del Pedido</h3>
{/* Items */}
<div className="space-y-3 max-h-80 overflow-y-auto pr-1">
{checkoutItems.map((item, i) => (
<div key={i} className="flex flex-col gap-2.5 bg-neutral-950 p-3 rounded-2xl border border-neutral-800">
<div className="flex gap-3">
<OptimizedImage
src={item.image}
alt={item.productName}
width={120}
height={120}
quality={75}
className="w-14 h-14 rounded-xl object-cover bg-neutral-900 border border-neutral-700 shrink-0"
containerClassName="w-14 h-14 rounded-xl shrink-0"
/>
<div className="flex-1 text-xs">
<h4 className="font-bold text-white line-clamp-1">{item.productName}</h4>
<p className="text-neutral-400">Talla elegida: <strong className="text-white font-mono">{item.size}</strong></p>
<p className={`font-black mt-0.5 text-transparent bg-clip-text bg-gradient-to-r ${currentBrand.colors.primaryGradient}`}>
{formatCOP(item.price * item.quantity)}
</p>
</div>
</div>
{/* Selector de tallas interactivo para clientes desde WhatsApp */}
{item.availableSizes && item.availableSizes.length > 0 && (
<div className="pt-2 border-t border-neutral-900">
<span className="text-[10px] text-neutral-400 font-bold uppercase tracking-wider block mb-1.5">
📏 Selecciona o Cambia tu Talla:
</span>
<div className="flex flex-wrap gap-1.5">
{item.availableSizes.map((sz) => (
<button
type="button"
key={sz}
onClick={() => handleUpdateItemSize(i, sz)}
className={`px-2.5 py-1 rounded-lg text-xs font-bold transition-all cursor-pointer ${
item.size === sz
? 'bg-purple-600 text-white shadow-md shadow-purple-600/30 font-black scale-105 border border-purple-400'
: 'bg-neutral-900 hover:bg-neutral-800 text-neutral-300 border border-neutral-700'
}`}
>
{sz}
</button>
))}
</div>
</div>
)}
</div>
))}
</div>
{/* Breakdown */}
<div className="border-t border-neutral-800 pt-3 space-y-2 text-xs text-neutral-400">
<div className="flex justify-between">
<span>Subtotal:</span>
<span className="font-bold text-white">{formatCOP(subtotal)}</span>
</div>
<div className="flex justify-between">
<span>Envío ({activeCity.name}):</span>
<span className="font-bold text-white">
{shippingCost === 0 ? <span className="text-emerald-400 font-bold uppercase">Gratis</span> : formatCOP(shippingCost)}
</span>
</div>
<div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-neutral-800">
<span>Total de la Orden:</span>
<span className="font-bold text-white">
{formatCOP(totalAmount)}
</span>
</div>
{/* Discriminación de Separación y Saldo Contraentrega */}
<div className="bg-neutral-950 border border-white/10 rounded-2xl p-3.5 space-y-2 text-xs">
<div className="flex justify-between items-center text-xs">
<span className="text-purple-300 font-bold">
{isMandatoryDeposit ? '⚡ Valor Separación (1er Pedido):' : '⭐ Separación (Cliente Frecuente):'}
</span>
<span className="font-black text-white font-mono">
{isMandatoryDeposit ? formatCOP(depositAmount) : '$0 COP (Exento)'}
</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-white/10 text-xs">
<span className="text-emerald-400 font-black">🤝 Saldo a Pagar Contraentrega:</span>
<span className="font-mono font-black text-emerald-400 text-sm">{formatCOP(balanceDue)}</span>
</div>
<p className="text-[10px] text-neutral-400 pt-1 leading-relaxed">
{isMandatoryDeposit
? `Para tu primer pedido aseguras el despacho de tu orden (${checkoutItems.reduce((acc, it) => acc + it.quantity, 0)} items) con una separación fija única de $10.000 COP vía Bold y cancelas el saldo restante en efectivo al recibir tu paquete.`
: '¡Gracias por tu confianza! Como cliente frecuente, cancelas el 100% del pedido contraentrega al recibir.'}
</p>
</div>
<div className="bg-white/5 border border-white/10 rounded-xl p-2.5 text-xs text-neutral-300 space-y-1">
<div className="flex items-center space-x-1.5 font-bold text-white">
<ShieldCheck className={`w-3.5 h-3.5 ${activeCity.firstExchangeFree ? 'text-emerald-400' : 'text-purple-400'}`} />
<span>Garantía en {activeCity.name}:</span>
</div>
<p className="text-[11px] text-neutral-300">
{activeCity.firstExchangeFree
? '1er Cambio de Talla 100% GRATIS (cambios posteriores con flete estándar).'
: 'Cambios de talla 100% garantizados (los costos de envío son asumidos por el cliente).'}
</p>
</div>
</div>
<button
type="submit"
disabled={isSubmitting}
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} hover:opacity-95 text-white font-black text-sm py-3.5 px-4 rounded-2xl shadow-lg active:scale-95 transition-all cursor-pointer flex items-center justify-center space-x-2`}
>
{isSubmitting ? (
<span>Procesando Pedido...</span>
) : isMandatoryDeposit ? (
<>
<span>Pagar Separación ($10.000 COP)</span>
<CreditCard className="w-4 h-4" />
</>
) : (
<>
<span>Enviar Pedido (100% Contraentrega)</span>
<Truck className="w-4 h-4" />
</>
)}
</button>
<p className="text-[11px] text-neutral-500 text-center leading-tight">
Al confirmar, se guardará tu pedido y recibirás asistencia inmediata por WhatsApp oficial ({officialWhatsapp}) para el seguimiento de tu entrega en 24 horas.
</p>
</div>
</div>
</form>
{showOtpModal && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in">
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-md w-full p-6 text-white space-y-4 shadow-2xl">
<div className="flex justify-between items-center border-b border-neutral-800 pb-3">
<h3 className="text-base font-black flex items-center gap-2">
<ShieldCheck className="w-5 h-5 text-purple-400" />
Verificación de WhatsApp (OTP)
</h3>
<button
type="button"
onClick={() => setShowOtpModal(false)}
className="text-neutral-400 hover:text-white text-sm cursor-pointer p-1"
>
✕
</button>
</div>
<p className="text-xs text-neutral-300 leading-relaxed">
Para validar tu cuenta y registrar tu orden contraentrega en la plataforma, te enviamos un código de seguridad de 6 dígitos a tu WhatsApp <strong>+57 {customerPhone}</strong>:
</p>
<input
type="text"
maxLength={6}
value={otpCode}
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, ''))}
placeholder="• • • • • •"
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-4 py-3 text-center text-2xl font-mono tracking-widest text-white outline-none"
/>
{otpError && <p className="text-xs text-rose-400 text-center">{otpError}</p>}
<button
type="button"
disabled={otpLoading || otpCode.length < 6}
onClick={handleVerifyOtpAndPlaceOrder}
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-600 text-white font-bold py-3.5 rounded-xl hover:opacity-95 disabled:opacity-50 text-sm shadow-md transition-all cursor-pointer"
>
{otpLoading ? 'Verificando...' : 'Confirmar Código y Registrar Pedido'}
</button>
<p className="text-[11px] text-neutral-500 text-center">
El código expira en 5 minutos.
</p>
</div>
</div>
)}
</div>
</div>
);
}