diff --git a/__pycache__/deploy_dripzone_vps.cpython-313.pyc b/__pycache__/deploy_dripzone_vps.cpython-313.pyc
new file mode 100644
index 0000000..317467c
Binary files /dev/null and b/__pycache__/deploy_dripzone_vps.cpython-313.pyc differ
diff --git a/deploy_dripzone_vps.py b/deploy_dripzone_vps.py
index 9b85c5d..9479567 100644
--- a/deploy_dripzone_vps.py
+++ b/deploy_dripzone_vps.py
@@ -79,9 +79,12 @@ def deploy():
docker compose -f docker-compose.prod.yml down --remove-orphans || true
DOCKER_BUILDKIT=1 docker compose -f docker-compose.prod.yml up -d --build
sleep 3
+ docker exec dripzone-production chown -R nextjs:nodejs /app/data || true
docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true
docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/DripZone.apk || true
- docker exec dripzone-production chmod 644 /app/public/DripZone.apk /app/DripZone.apk || true
+ docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/dripzone.apk || true
+ docker cp {REMOTE_DIR}/public/DripZone.apk dripzone-production:/app/public/app-dripzone.apk || true
+ docker exec dripzone-production chmod 644 /app/public/DripZone.apk /app/DripZone.apk /app/public/dripzone.apk /app/public/app-dripzone.apk || true
docker ps | grep dripzone
"""
@@ -118,15 +121,14 @@ def send_apk_to_whatsapp():
"Content-Type": "application/json"
}
caption = (
- "📲 *¡DRIPZONE APK ACTUALIZADA!* 🚀👟\n"
+ "📲 *¡DRIPZONE ACTUALIZADA EN PRODUCCIÓN!* 🚀👟\n"
"━━━━━━━━━━━━━━━━━━━━━━\n"
"✨ *Novedades de esta versión:*\n"
- "• 📦 *Mis Pedidos*: Visualización de estado en tiempo real, desglose de calzado, flete, anticipo $10.000 y saldo contraentrega.\n"
- "• 👤 *Mi Cuenta*: Acceso seguro vía WhatsApp OTP exclusivo a la línea del cliente.\n"
- "• 💵 *Precios en Grid*: Corrección de visualización de precios en 1 sola línea sin recortes y botón 'Pedir' optimizado.\n"
- "• 🗄️ *Persistencia Total*: Configuración, parámetros y entidad almacenados de forma segura en Base de Datos.\n"
- "• 🔔 *Plantillas Dinámicas*: Notificaciones móviles y web emitidas con las plantillas registradas.\n\n"
- "🌐 *Descarga directa:*\n👉 https://drip-zone.online/DripZone.apk"
+ "• 📦 *Nueva Vista Dedicada 'Mis Pedidos'*: Interfaz completa y no modal en `/mis-pedidos` con pedidos agrupados por estado (Pendiente de anticipo, Anticipo aprobado, Despachados, Entregados, Cancelados), mostrando información clave (ID, fecha, total, anticipo Bold, contraentrega) y detalle desplegable con 1-clic.\n"
+ "• 🛒 *Selección Limpia de Producto*: Eliminado botón y banners de pedir por WhatsApp en la vitrina de producto, canalizando la compra de forma directa al carrito y checkout seguro.\n"
+ "• 📱 *Navegación Móvil Mejorada*: Reemplazado botón duplicado de Asesor por acceso directo a 'Mis Pedidos' en el dock inferior y en el menú superior.\n"
+ "• 💬 *Atención Comercial en WhatsApp & Pagos en Web/App*: Consultas por chat y procesamiento seguro de órdenes y pasarela Bold ($10k anticipo) en la app.\n\n"
+ "🌐 *Descarga directa del APK:*\n👉 https://drip-zone.online/DripZone.apk"
)
payload = {
"number": "573167455877",
@@ -138,7 +140,7 @@ def send_apk_to_whatsapp():
}
try:
req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers=headers)
- with urllib.request.urlopen(req, timeout=35) as resp:
+ with urllib.request.urlopen(req, timeout=180) as resp:
print(f"[OK] APK despachada con exito al WhatsApp de Carlos (HTTP {resp.status})")
except Exception as e:
print(f"[Aviso] No se pudo enviar archivo multimedia por Evolution: {e}. Enviando mensaje de texto...")
diff --git a/dripzone-app/Dockerfile b/dripzone-app/Dockerfile
index 8b68acd..89b3707 100644
--- a/dripzone-app/Dockerfile
+++ b/dripzone-app/Dockerfile
@@ -46,6 +46,7 @@ ENV HOSTNAME="0.0.0.0"
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
+RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data
# Copy standalone output & public static files
COPY --from=builder /app/public ./public
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt
index 1272ea3..072aa6e 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/api/ApiClient.kt
@@ -56,22 +56,25 @@ object ApiClient {
// 1. Try primary URL
val primaryResult = fetchFromUrl(getPrimaryProductsUrl())
if (primaryResult != null && primaryResult.isNotEmpty()) {
- saveCache(context, primaryResult)
- return@withContext primaryResult
+ val activeOnly = primaryResult.filter { it.isActive }
+ saveCache(context, activeOnly)
+ return@withContext activeOnly
}
// 2. Try fallback URL
val fallbackResult = fetchFromUrl(FALLBACK_URL)
if (fallbackResult != null && fallbackResult.isNotEmpty()) {
- saveCache(context, fallbackResult)
- return@withContext fallbackResult
+ val activeOnly = fallbackResult.filter { it.isActive }
+ saveCache(context, activeOnly)
+ return@withContext activeOnly
}
// 3. If offline or network failed, return cached products
val cached = loadCache(context)
if (cached.isNotEmpty()) {
- Log.i(TAG, "Serving ${cached.size} products from secure local cache")
- return@withContext cached
+ val activeOnly = cached.filter { it.isActive }
+ Log.i(TAG, "Serving ${activeOnly.size} active products from secure local cache")
+ return@withContext activeOnly
}
// 4. Return initial fallback sample if cache is empty
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/model/Product.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/Product.kt
index 2cc0908..3860c75 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/model/Product.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/Product.kt
@@ -45,6 +45,9 @@ data class Product(
@SerializedName("isFeatured")
val isFeatured: Boolean = false,
+ @SerializedName("isActive")
+ val isActive: Boolean = true,
+
@SerializedName("whatsappMessageId")
val whatsappMessageId: String? = null
) : Serializable {
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/model/UserManager.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/UserManager.kt
index 34bdc59..6800cd4 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/model/UserManager.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/model/UserManager.kt
@@ -33,6 +33,11 @@ object UserManager {
)
}
+ fun isAuthenticated(context: Context): Boolean {
+ val user = getUser(context)
+ return user.isPhoneVerified && user.phone.isNotBlank()
+ }
+
fun saveUser(
context: Context,
name: String,
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CustomOrderBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CustomOrderBottomSheet.kt
index 203b49b..bf999b7 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CustomOrderBottomSheet.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/CustomOrderBottomSheet.kt
@@ -50,12 +50,18 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
binding.btnCloseCustomModal.setOnClickListener { dismiss() }
- // Pre-fill user data if registered locally
val user = UserManager.getUser(requireContext())
+ val isAuth = UserManager.isAuthenticated(requireContext()) || (user.isPhoneVerified && user.phone.isNotBlank())
+
if (user.name.isNotBlank()) binding.etCustomCustomerName.setText(user.name)
if (user.phone.isNotBlank()) binding.etCustomCustomerPhone.setText(user.phone)
if (user.city.isNotBlank()) binding.etCustomCustomerCity.setText(user.city)
+ if (isAuth) {
+ binding.btnSubmitCustomOrder.text = "Registrar Encargo Directo 🚀"
+ binding.btnSubmitCustomOrder.setIconResource(R.drawable.ic_check)
+ }
+
if (initialModel.isNotBlank()) {
binding.etCustomModel.setText(initialModel)
}
@@ -130,7 +136,10 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
notes = rawNotes
val user = UserManager.getUser(requireContext())
- if (user.isPhoneVerified && user.phone == customerPhone) {
+ val isAuth = UserManager.isAuthenticated(requireContext()) || (user.isPhoneVerified && user.phone.isNotBlank())
+
+ // Si el usuario ya está autenticado en la app, NO hay necesidad de validar el usuario
+ if (isAuth || (user.isPhoneVerified && user.phone == customerPhone)) {
submitFinalCustomRequest()
} else {
sendCustomOtp()
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/OrderVerificationBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/OrderVerificationBottomSheet.kt
index 629ee7b..9fd4b7d 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/OrderVerificationBottomSheet.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/OrderVerificationBottomSheet.kt
@@ -67,10 +67,25 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
// Pre-fill user data if already registered locally
val user = UserManager.getUser(requireContext())
+ val isAuth = UserManager.isAuthenticated(requireContext()) || (user.isPhoneVerified && user.phone.isNotBlank())
+
if (user.name.isNotBlank()) binding.etCustomerName.setText(user.name)
if (user.phone.isNotBlank()) binding.etCustomerPhone.setText(user.phone)
if (user.address.isNotBlank()) binding.etCustomerAddress.setText(user.address)
+ if (isAuth) {
+ binding.layoutAuthUserBanner.visibility = View.VISIBLE
+ binding.tvAuthUserNotice.text = "Sesión activa (+57 ${user.phone}) • No requieres validación adicional ✅"
+ binding.tvOrderModalTitle.text = "CONFIRMAR PEDIDO"
+ binding.btnContinueToOtp.text = "Confirmar Pedido Contraentrega 🚀"
+ binding.btnContinueToOtp.setIconResource(R.drawable.ic_check)
+ } else {
+ binding.layoutAuthUserBanner.visibility = View.GONE
+ binding.tvOrderModalTitle.text = "DATOS DE ENTREGA"
+ binding.btnContinueToOtp.text = "Validar Celular y Registrar Pedido"
+ binding.btnContinueToOtp.setIconResource(R.drawable.ic_whatsapp)
+ }
+
// Setup economic calculations
val subtotal = if (isDirectProductOrder && directItem != null) {
directItem!!.itemTotal
@@ -140,8 +155,18 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
customerAddress = rawAddress
val user = UserManager.getUser(requireContext())
- // Si el usuario ya está verificado para este número, procede directo a la orden
- if (user.isPhoneVerified && user.phone == customerPhone) {
+ val isAuth = UserManager.isAuthenticated(requireContext()) || (user.isPhoneVerified && user.phone.isNotBlank())
+
+ // Si el usuario ya está autenticado en la app, NO hay necesidad de validar el usuario
+ if (isAuth || (user.isPhoneVerified && user.phone == customerPhone)) {
+ UserManager.saveUser(
+ context = requireContext(),
+ name = customerName,
+ phone = if (isAuth && user.phone.isNotBlank()) user.phone else customerPhone,
+ city = selectedCity.name,
+ address = customerAddress,
+ isVerified = true
+ )
submitFinalOrder()
} else {
sendOtpCode()
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
index 3023920..dd13ff0 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
@@ -78,22 +78,6 @@ class ProductDetailBottomSheet : BottomSheetDialogFragment() {
dismiss()
}
- // Order or Inquire directly via WhatsApp using registered product_direct_inquiry template
- binding.btnOrderWhatsAppDirect.setOnClickListener {
- val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "38-43"
- val variables = mapOf(
- "tienda_nombre" to "DripZone",
- "zapatilla_nombre" to p.name,
- "color_nombre" to "Estilo Oficial",
- "talla" to sizeToOrder,
- "precio" to p.formattedPrice(),
- "ciudad" to "Colombia",
- "marca" to p.brand
- )
- val message = co.dripzone.app.manager.TemplateManager.renderTemplate(requireContext(), "product_direct_inquiry", variables)
- SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
- }
-
// Custom order button inside bottom sheet
binding.btnCustomOrderSheet.setOnClickListener {
dismiss()
diff --git a/dripzone-app/android/app/src/main/res/drawable/ic_check.xml b/dripzone-app/android/app/src/main/res/drawable/ic_check.xml
new file mode 100644
index 0000000..4a0b8ff
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/ic_check.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml
index 7528cd0..76ae977 100644
--- a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml
@@ -62,6 +62,36 @@
android:textColor="@color/text_secondary"
android:textSize="12sp" />
+
+
+
+
+
+
+
+
-
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
index 50aa8ec..ad991dc 100644
--- a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
@@ -170,26 +170,6 @@
app:iconPadding="8dp"
app:iconSize="20dp" />
-
-
diff --git a/dripzone-app/docker-compose.prod.yml b/dripzone-app/docker-compose.prod.yml
index cedfc7d..c106900 100644
--- a/dripzone-app/docker-compose.prod.yml
+++ b/dripzone-app/docker-compose.prod.yml
@@ -29,6 +29,7 @@ services:
- BOLD_PROD_SECRET_KEY=Yyzm4vZjWe_N_I_5c1g7Dw
volumes:
- ./public/DripZone.apk:/app/public/DripZone.apk:ro
+ - ./data:/app/data
networks:
- coolify
labels:
diff --git a/dripzone-app/src/app/admin/catalogo-whatsapp/page.tsx b/dripzone-app/src/app/admin/catalogo-whatsapp/page.tsx
new file mode 100644
index 0000000..a087f82
--- /dev/null
+++ b/dripzone-app/src/app/admin/catalogo-whatsapp/page.tsx
@@ -0,0 +1,687 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import Image from 'next/image';
+import {
+ MessageCircle,
+ RefreshCw,
+ Copy,
+ Check,
+ ExternalLink,
+ ShieldCheck,
+ AlertCircle,
+ CheckCircle2,
+ Package,
+ Layers,
+ Sparkles,
+ Search,
+ Eye,
+ EyeOff,
+ Radio,
+ Sliders,
+ Zap,
+ Tag,
+ Link2,
+ Download,
+ Send,
+} from 'lucide-react';
+import { Product, StoreSettings } from '@/types';
+import { formatCOP } from '@/lib/pricing-engine';
+import { WhatsAppOrderLinkModal } from '@/components/admin/WhatsAppOrderLinkModal';
+
+export default function AdminWhatsAppCatalogPage() {
+ const [products, setProducts] = useState([]);
+ const [settings, setSettings] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSyncing, setIsSyncing] = useState(false);
+ const [syncMessage, setSyncMessage] = useState(null);
+ const [copiedFeed, setCopiedFeed] = useState<'xml' | 'csv' | null>(null);
+ const [search, setSearch] = useState('');
+ const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
+ const [savingSettings, setSavingSettings] = useState(false);
+
+ // Modal para Generación de Enlaces de Pedido WhatsApp para Asesores
+ const [selectedProductForLink, setSelectedProductForLink] = useState(null);
+ const [isOrderLinkModalOpen, setIsOrderLinkModalOpen] = useState(false);
+
+ // Formulario de Meta API
+ const [metaCatalogId, setMetaCatalogId] = useState('');
+ const [metaAccessToken, setMetaAccessToken] = useState('');
+ const [autoSyncEnabled, setAutoSyncEnabled] = useState(true);
+
+ const domain = typeof window !== 'undefined' ? window.location.origin : 'https://drip-zone.online';
+ const xmlFeedUrl = `${domain}/api/catalog/feed.xml`;
+ const csvFeedUrl = `${domain}/api/catalog/feed.csv`;
+
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const [resProd, resSettings] = await Promise.all([
+ fetch('/api/products?all=true'),
+ fetch('/api/settings'),
+ ]);
+
+ const dataProd = await resProd.json();
+ const dataSettings = await resSettings.json();
+
+ if (dataProd.success && Array.isArray(dataProd.products)) {
+ setProducts(dataProd.products);
+ }
+
+ if (dataSettings.success && dataSettings.settings) {
+ setSettings(dataSettings.settings);
+ setMetaCatalogId(dataSettings.settings.metaCatalogId || '');
+ setMetaAccessToken(dataSettings.settings.metaAccessToken || '');
+ setAutoSyncEnabled(dataSettings.settings.whatsappCatalogAutoSync !== false);
+ }
+ } catch (err) {
+ console.error('Error cargando datos del catálogo de WhatsApp:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const handleCopyUrl = (type: 'xml' | 'csv', url: string) => {
+ navigator.clipboard.writeText(url);
+ setCopiedFeed(type);
+ setTimeout(() => setCopiedFeed(null), 2500);
+ };
+
+ const handleSaveMetaSettings = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSavingSettings(true);
+ try {
+ const res = await fetch('/api/settings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ metaCatalogId: metaCatalogId.trim(),
+ metaAccessToken: metaAccessToken.trim(),
+ whatsappCatalogAutoSync: autoSyncEnabled,
+ }),
+ });
+
+ const data = await res.json();
+ if (data.success) {
+ setSyncMessage('✅ Configuración de Meta Commerce guardada correctamente.');
+ setTimeout(() => setSyncMessage(null), 4000);
+ } else {
+ setSyncMessage(`❌ Error: ${data.error || 'No se pudo guardar la configuración'}`);
+ }
+ } catch (err: any) {
+ setSyncMessage(`❌ Error de red: ${err.message}`);
+ } finally {
+ setSavingSettings(false);
+ }
+ };
+
+ const handleSyncAll = async () => {
+ setIsSyncing(true);
+ setSyncMessage(null);
+ try {
+ const res = await fetch('/api/catalog/sync', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ syncAll: true }),
+ });
+
+ const data = await res.json();
+ if (data.success) {
+ setSyncMessage(`🚀 ¡Catálogo sincronizado! ${data.message}`);
+ fetchData();
+ } else {
+ setSyncMessage(`⚠️ ${data.error || 'Error al sincronizar'}`);
+ }
+ } catch (err: any) {
+ setSyncMessage(`❌ Error: ${err.message}`);
+ } finally {
+ setIsSyncing(false);
+ setTimeout(() => setSyncMessage(null), 6000);
+ }
+ };
+
+ const handleToggleProductStatus = async (product: Product) => {
+ const newStatus = product.isActive === false ? true : false;
+
+ // Actualización optimista en UI
+ setProducts((prev) =>
+ prev.map((p) => (p.id === product.id ? { ...p, isActive: newStatus } : p))
+ );
+
+ try {
+ const res = await fetch('/api/products', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ id: product.id,
+ updates: { isActive: newStatus },
+ }),
+ });
+
+ const data = await res.json();
+ if (data.success) {
+ setSyncMessage(
+ newStatus
+ ? `🟢 "${product.name}" activado: visible en la app y disponible en WhatsApp Catalog.`
+ : `⚪ "${product.name}" desactivado: oculto de la app y del catálogo de WhatsApp.`
+ );
+ setTimeout(() => setSyncMessage(null), 4000);
+ }
+ } catch (e: any) {
+ console.error('Error toggling status:', e);
+ }
+ };
+
+ const handleSyncSingleProduct = async (product: Product) => {
+ try {
+ const res = await fetch('/api/catalog/sync', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ productId: product.id }),
+ });
+ const data = await res.json();
+ if (data.success) {
+ setSyncMessage(`✅ Producto "${product.name}" sincronizado con WhatsApp Catalog.`);
+ fetchData();
+ setTimeout(() => setSyncMessage(null), 4000);
+ }
+ } catch (err: any) {
+ setSyncMessage(`❌ Error sincronizando producto: ${err.message}`);
+ }
+ };
+
+ const activeProducts = products.filter((p) => p.isActive !== false);
+ const inactiveProducts = products.filter((p) => p.isActive === false);
+
+ const filteredProducts = products.filter((p) => {
+ const matchesSearch =
+ p.name.toLowerCase().includes(search.toLowerCase()) ||
+ (p.brand && p.brand.toLowerCase().includes(search.toLowerCase())) ||
+ (p.category && p.category.toLowerCase().includes(search.toLowerCase()));
+
+ if (statusFilter === 'active') return matchesSearch && p.isActive !== false;
+ if (statusFilter === 'inactive') return matchesSearch && p.isActive === false;
+ return matchesSearch;
+ });
+
+ return (
+
+ {/* Encabezado Principal */}
+
+
+
+
+
+
+
+ Catálogo Oficial de WhatsApp Business
+
+
+
+ Sincronización automática de calzado con foto HD, precio COP, descripción completa y tallas disponibles para la línea oficial DripZone (+57 310 639 0296).
+
+
+
+
+
+
+
+
+
+
+ {/* Banner de Orientación Comercial: Atención en WhatsApp / Cierre y Pago en Web & App */}
+
+
+
+
+
+
+
+ Flujo Comercial & Procesamiento de Pedidos DripZone
+
+ Oficial
+
+
+
+ El catálogo de WhatsApp permite a los usuarios consultar modelos y comunicarse para asesoría.
+ Todos los pedidos y pagos ($10.000 anticipo Bold + contraentrega) se procesan a través de la web o app móvil para asegurar factura y seguimiento.
+ Usa el botón "Cerrar Pedido por WhatsApp" en cada producto para enviar al cliente su enlace directo con talla lista.
+
+
+
+
+
+ {/* Toast Alert */}
+ {syncMessage && (
+
+
+ {syncMessage}
+
+ )}
+
+ {/* Métricas y Estado */}
+
+
+
+
+
+
+
{products.length}
+
Total Modelos
+
+
+
+
+
+
+
+
+
{activeProducts.length}
+
Activos en WhatsApp & App
+
+
+
+
+
+
+
+
+
{inactiveProducts.length}
+
Inactivos / Ocultos
+
+
+
+
+
+
+
+
+
Línea Oficial DripZone
+
+57 310 639 0296
+
Instancia Evolution: DripZone
+
+
+
+
+ {/* Sección 1: Enlaces del Feed Automático (Meta Commerce Manager) */}
+
+
+
+
+
+ Orígenes de Datos para Meta Commerce & WhatsApp Catalog
+
+
+ Conecta estos enlaces en tu Administrador de Ventas de Meta (Commerce Manager). Meta consultará este feed de forma automática y continua para mantener sincronizado el catálogo de WhatsApp.
+
+ Formato delimitado por comas compatible con la carga manual por archivo en Commerce Manager o herramientas de marketing.
+
+
+
+
+
+ {/* Sección 2: Configuración Meta Commerce Graph API (Opcional para sincronización en tiempo real) */}
+
+
+ {/* Sección 3: Explorador y Control de Productos del Catálogo */}
+
+
+
+
+
+ Productos en Vitrina y Control de Disponibilidad
+
+
+ Revisa la imagen, el valor de venta en COP, la descripción y las tallas disponibles. Activa o desactiva productos para mostrarlos u ocultarlos en la app y WhatsApp.
+
)}
+
+ {/* Modal para Generación de Enlace de Pedido para Asesores WhatsApp */}
+ setIsWaOrderLinkModalOpen(false)}
+ product={selectedProductForWaLink}
+ />
);
}
diff --git a/dripzone-app/src/app/api/catalog/feed.csv/route.ts b/dripzone-app/src/app/api/catalog/feed.csv/route.ts
new file mode 100644
index 0000000..03ef6c2
--- /dev/null
+++ b/dripzone-app/src/app/api/catalog/feed.csv/route.ts
@@ -0,0 +1,40 @@
+import { NextResponse } from 'next/server';
+import { dataService } from '@/lib/data-service';
+import { generateWhatsAppCatalogCsv } from '@/lib/whatsapp-catalog';
+import { getProductsFromAppwrite } from '@/lib/server-appwrite';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ let products = dataService.getProducts();
+ if (products.length === 0) {
+ try {
+ const appwriteProducts = await getProductsFromAppwrite();
+ if (appwriteProducts && appwriteProducts.length > 0) {
+ appwriteProducts.forEach((p) => dataService.addProduct(p));
+ products = dataService.getProducts();
+ }
+ } catch (e) {
+ console.warn('Error sincronizando productos con Appwrite para CSV Feed:', e);
+ }
+ }
+
+ const csv = generateWhatsAppCatalogCsv(products);
+
+ return new Response(csv, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'text/csv; charset=utf-8',
+ 'Content-Disposition': 'inline; filename="dripzone_whatsapp_catalog.csv"',
+ 'Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
+ },
+ });
+ } catch (error: any) {
+ console.error('[WhatsApp Catalog Feed CSV] Error:', error);
+ return new NextResponse('error,message\n500,"Error generating CSV catalog feed"', {
+ status: 500,
+ headers: { 'Content-Type': 'text/csv; charset=utf-8' },
+ });
+ }
+}
diff --git a/dripzone-app/src/app/api/catalog/feed.xml/route.ts b/dripzone-app/src/app/api/catalog/feed.xml/route.ts
new file mode 100644
index 0000000..74fe1f3
--- /dev/null
+++ b/dripzone-app/src/app/api/catalog/feed.xml/route.ts
@@ -0,0 +1,43 @@
+import { NextResponse } from 'next/server';
+import { dataService } from '@/lib/data-service';
+import { generateWhatsAppCatalogXml } from '@/lib/whatsapp-catalog';
+import { getProductsFromAppwrite } from '@/lib/server-appwrite';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ let products = dataService.getProducts();
+ if (products.length === 0) {
+ try {
+ const appwriteProducts = await getProductsFromAppwrite();
+ if (appwriteProducts && appwriteProducts.length > 0) {
+ appwriteProducts.forEach((p) => dataService.addProduct(p));
+ products = dataService.getProducts();
+ }
+ } catch (e) {
+ console.warn('Error sincronizando productos con Appwrite para XML Feed:', e);
+ }
+ }
+
+ const settings = dataService.getSettings();
+ const xml = generateWhatsAppCatalogXml(products, settings);
+
+ return new Response(xml, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/xml; charset=utf-8',
+ 'Cache-Control': 'public, max-age=300, stale-while-revalidate=600',
+ },
+ });
+ } catch (error: any) {
+ console.error('[WhatsApp Catalog Feed XML] Error:', error);
+ return new NextResponse(
+ `${error.message || 'Error generating feed'}`,
+ {
+ status: 500,
+ headers: { 'Content-Type': 'application/xml; charset=utf-8' },
+ }
+ );
+ }
+}
diff --git a/dripzone-app/src/app/api/catalog/sync/route.ts b/dripzone-app/src/app/api/catalog/sync/route.ts
new file mode 100644
index 0000000..cc1a720
--- /dev/null
+++ b/dripzone-app/src/app/api/catalog/sync/route.ts
@@ -0,0 +1,96 @@
+import { NextResponse } from 'next/server';
+import { dataService } from '@/lib/data-service';
+import {
+ formatProductForWhatsAppCatalog,
+ syncProductToMetaCatalogApi,
+ registerProductInWhatsAppCatalog,
+} from '@/lib/whatsapp-catalog';
+import { getProductsFromAppwrite } from '@/lib/server-appwrite';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ try {
+ let products = dataService.getProducts();
+ if (products.length === 0) {
+ try {
+ const appwriteProducts = await getProductsFromAppwrite();
+ if (appwriteProducts && appwriteProducts.length > 0) {
+ appwriteProducts.forEach((p) => dataService.addProduct(p));
+ products = dataService.getProducts();
+ }
+ } catch (e) {}
+ }
+ const settings = dataService.getSettings();
+ const activeProducts = products.filter((p) => p.isActive !== false);
+
+ const formattedSample = activeProducts.slice(0, 5).map((p) => formatProductForWhatsAppCatalog(p));
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ totalProducts: products.length,
+ activeProducts: activeProducts.length,
+ syncedCount: activeProducts.filter((p) => p.whatsappCatalogSynced).length,
+ feedXmlUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'https://drip-zone.online'}/api/catalog/feed.xml`,
+ feedCsvUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'https://drip-zone.online'}/api/catalog/feed.csv`,
+ metaCatalogConfigured: Boolean(settings?.metaCatalogId && settings?.metaAccessToken),
+ metaCatalogId: settings?.metaCatalogId || '',
+ autoSyncEnabled: settings?.whatsappCatalogAutoSync !== false,
+ sampleCatalogItems: formattedSample,
+ },
+ });
+ } catch (error: any) {
+ console.error('[WhatsApp Catalog Sync GET] Error:', error);
+ return NextResponse.json({ success: false, error: error.message }, { status: 500 });
+ }
+}
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json().catch(() => ({}));
+ const { productId, syncAll } = body;
+
+ const products = dataService.getProducts();
+ const settings = dataService.getSettings();
+
+ if (productId) {
+ const product = products.find((p) => p.id === productId);
+ if (!product) {
+ return NextResponse.json({ success: false, error: 'Producto no encontrado' }, { status: 404 });
+ }
+
+ const syncResult = await registerProductInWhatsAppCatalog(product);
+ return NextResponse.json({
+ success: true,
+ message: `Producto "${product.name}" sincronizado con el catálogo de WhatsApp`,
+ catalogItem: syncResult.catalogItem,
+ });
+ }
+
+ // Sincronizar todos los productos activos
+ const activeProducts = products.filter((p) => p.isActive !== false);
+ let syncedCount = 0;
+ const errors: string[] = [];
+
+ for (const product of activeProducts) {
+ try {
+ await registerProductInWhatsAppCatalog(product);
+ syncedCount++;
+ } catch (err: any) {
+ errors.push(`Error en producto ${product.name}: ${err.message}`);
+ }
+ }
+
+ return NextResponse.json({
+ success: true,
+ message: `${syncedCount} de ${activeProducts.length} productos registrados y actualizados para el catálogo de WhatsApp`,
+ totalActive: activeProducts.length,
+ syncedCount,
+ errors: errors.length > 0 ? errors : undefined,
+ });
+ } catch (error: any) {
+ console.error('[WhatsApp Catalog Sync POST] Error:', error);
+ return NextResponse.json({ success: false, error: error.message }, { status: 500 });
+ }
+}
diff --git a/dripzone-app/src/app/api/products/route.ts b/dripzone-app/src/app/api/products/route.ts
index 11b66d2..8667926 100644
--- a/dripzone-app/src/app/api/products/route.ts
+++ b/dripzone-app/src/app/api/products/route.ts
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service';
import { getProductSocialProof } from '@/lib/social-proof';
import { deleteProductFromAppwrite, saveProductToAppwrite, getProductsFromAppwrite } from '@/lib/server-appwrite';
+import { registerProductInWhatsAppCatalog } from '@/lib/whatsapp-catalog';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
@@ -62,6 +63,7 @@ export async function POST(request: Request) {
const updated = dataService.updateProduct(id, { isActive });
if (updated) {
saveProductToAppwrite(updated).catch(() => {});
+ registerProductInWhatsAppCatalog(updated).catch(() => {});
updatedCount++;
}
}
@@ -249,6 +251,9 @@ export async function POST(request: Request) {
isActive: body.isActive !== undefined ? body.isActive : true,
});
saveProductToAppwrite(newProduct).catch(() => {});
+ registerProductInWhatsAppCatalog(newProduct).catch((e) =>
+ console.warn('[WhatsApp Catalog Sync Error]:', e)
+ );
return NextResponse.json({ success: true, product: newProduct });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
@@ -269,6 +274,9 @@ export async function PUT(request: Request) {
}
saveProductToAppwrite(updated).catch(() => {});
+ registerProductInWhatsAppCatalog(updated).catch((e) =>
+ console.warn('[WhatsApp Catalog Sync Error]:', e)
+ );
return NextResponse.json({ success: true, product: updated });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
diff --git a/dripzone-app/src/app/api/webhook/whatsapp/route.ts b/dripzone-app/src/app/api/webhook/whatsapp/route.ts
index 0ef9139..735816a 100644
--- a/dripzone-app/src/app/api/webhook/whatsapp/route.ts
+++ b/dripzone-app/src/app/api/webhook/whatsapp/route.ts
@@ -4,7 +4,7 @@ import { parseSupplierMessage, detectCategory } from '@/lib/gemini-parser';
import { sanitizePublicationTitle } from '@/lib/sanitizer';
import { generateAttractiveProductCopy } from '@/lib/attractive-copy';
import { getProductSocialProof } from '@/lib/social-proof';
-import { notifyNewProductPublished } from '@/lib/evolution-api';
+import { notifyNewProductPublished, sendWhatsAppMessage, sendWhatsAppMedia } from '@/lib/evolution-api';
import {
getMonitoredGroupsFromAppwrite,
saveProductToAppwrite,
@@ -13,9 +13,13 @@ import {
getProductsFromAppwrite,
} from '@/lib/server-appwrite';
import { Product, SupplierPost } from '@/types';
+import { registerProductInWhatsAppCatalog } from '@/lib/whatsapp-catalog';
+import { formatCOP } from '@/lib/pricing-engine';
// Cache en memoria para evitar procesar mensajes duplicados de WhatsApp en ráfaga
const processedMessagesCache = new Map();
+// Cache en memoria para control de cadencia de respuestas comerciales automáticas a clientes
+const customerChatCache = new Map();
function isDuplicateMessage(msgId: string, text: string): boolean {
const key = `${msgId}:${(text || '').trim().substring(0, 60)}`;
@@ -89,11 +93,138 @@ export async function POST(request: Request) {
return NextResponse.json({ success: true, ignored: 'Bucle de mensaje propio del bot evitado' });
}
+ // =========================================================================
+ // CASO A: ATENCIÓN COMERCIAL VÍA MENSAJES DE WHATSAPP (Chat Directo 1 a 1)
+ // Cuando los clientes consultan el catálogo de WhatsApp y escriben por chat,
+ // se les orienta y se les envía el enlace directo para procesar el pedido
+ // y pago seguro ($10.000 anticipo Bold + contraentrega) en la web o app móvil.
+ // =========================================================================
+ if (!remoteJid.includes('g.us')) {
+ if (fromMe) {
+ return NextResponse.json({ success: true, ignored: 'Mensaje saliente de la tienda (fromMe: true)' });
+ }
+
+ const customerPhone = remoteJid.replace(/\D/g, '');
+ console.log(`[DripZone Asesor Comercial WhatsApp] Mensaje recibido de cliente +${customerPhone}: "${messageContent}"`);
+
+ // Control de duplicados o respuestas en ráfaga (máximo 1 respuesta automática cada 3 minutos por cliente salvo mención explícita)
+ const lastReplyTime = customerChatCache.get(customerPhone) || 0;
+ const now = Date.now();
+ const isQuickFollowup = (now - lastReplyTime) < 180000; // 3 minutos
+
+ const lowerText = (messageContent || '').toLowerCase().trim();
+ const allActiveProducts = dataService.getProducts().filter((p) => p.isActive !== false);
+
+ // 1. Buscar si el cliente pregunta por un modelo específico, ID o referencia del catálogo
+ let matchedProduct = allActiveProducts.find((p) => {
+ const prodName = (p.name || '').toLowerCase();
+ const model = (p.modelName || '').toLowerCase();
+ const brand = (p.brand || '').toLowerCase();
+ if (lowerText.includes(p.id.toLowerCase())) return true;
+ if (prodName.length > 5 && lowerText.includes(prodName)) return true;
+ const modelWords = model.split(/\s+/).filter((w) => w.length > 3);
+ return modelWords.length > 0 && modelWords.some((w) => lowerText.includes(w)) && (brand ? lowerText.includes(brand) : true);
+ });
+
+ if (!matchedProduct && lowerText.includes('snk-')) {
+ const matchId = lowerText.match(/snk-[a-z0-9-]+/i);
+ if (matchId) {
+ matchedProduct = allActiveProducts.find((p) => p.id === matchId[0]);
+ }
+ }
+
+ // Si se identificó el calzado solicitado por el cliente:
+ if (matchedProduct) {
+ customerChatCache.set(customerPhone, now);
+
+ const checkoutUrl = `https://drip-zone.online/checkout?productId=${encodeURIComponent(matchedProduct.id)}&source=whatsapp`;
+ const sizesText = Array.isArray(matchedProduct.sizes) && matchedProduct.sizes.length > 0
+ ? matchedProduct.sizes.join(', ')
+ : '37, 38, 39, 40, 41, 42, 43';
+ const priceFormatted = formatCOP(matchedProduct.retailPrice || 125000);
+
+ const responseText = (
+ `👟 *${matchedProduct.name}*\n` +
+ `━━━━━━━━━━━━━━━━━━━━━━\n` +
+ `📏 *Tallas Disponibles:* ${sizesText} (Horma Nacional)\n` +
+ `💵 *Precio Oficial:* *${priceFormatted} COP*\n` +
+ `🚚 *Pago Contraentrega:* Pagas un anticipo de *$10.000* para despacho garantizado y el resto al recibir.\n` +
+ `🛡️ *Garantía:* 1er cambio de talla gratuito.\n\n` +
+ `🛒 *¿Cómo procesar tu pedido y pago seguro?*\n` +
+ `Para tu seguridad, asignación de factura y reserva de tu talla, el pedido y pago del anticipo ($10.000 por Bold / PSE / Nequi / Tarjetas) se procesan a través de nuestra plataforma:\n\n` +
+ `👉 *Enlace directo de compra:* ${checkoutUrl}\n` +
+ `📱 *O pide en nuestra App Móvil:* https://drip-zone.online/app-dripzone.apk\n\n` +
+ `Diligencia tus datos de envío en el enlace y hoy mismo programamos tu despacho. Si necesitas ayuda con tu talla habitual, ¡aquí estamos para asesorarte!`
+ );
+
+ let sent = false;
+ const primaryImage = matchedProduct.images?.[0] || matchedProduct.colors?.[0]?.image;
+ if (primaryImage) {
+ const absImageUrl = primaryImage.startsWith('http')
+ ? primaryImage
+ : `https://drip-zone.online${primaryImage.startsWith('/') ? '' : '/'}${primaryImage}`;
+ sent = await sendWhatsAppMedia(customerPhone, absImageUrl, responseText);
+ }
+ if (!sent) {
+ await sendWhatsAppMessage(customerPhone, responseText);
+ }
+
+ return NextResponse.json({
+ success: true,
+ handled: 'product_inquiry',
+ productId: matchedProduct.id,
+ customerPhone,
+ });
+ }
+
+ // 2. Si es un saludo general o consulta sobre el catálogo, compra, pedidos o formas de pago:
+ const isCommercialGreeting = /hola|buenas|catalogo|catálogo|zapatillas|precio|precios|comprar|pedido|pedir|tallas|envio|envíos|contraentrega|bold|pagar|pago/i.test(
+ lowerText
+ );
+
+ if (isCommercialGreeting && !isQuickFollowup) {
+ customerChatCache.set(customerPhone, now);
+
+ const generalResponse = (
+ `¡Hola! 👋 Te damos la bienvenida a *DripZone Colombia* 🇨🇴\n` +
+ `Tu tienda oficial de sneakers, zapatillas y streetwear con horma nacional.\n` +
+ `━━━━━━━━━━━━━━━━━━━━━━\n\n` +
+ `👀 *Puedes consultar nuestro catálogo y modelos aquí en WhatsApp o en nuestra tienda online:*\n` +
+ `🌐 *Catálogo Completo:* https://drip-zone.online\n` +
+ `🛒 *Procesar Pedido:* https://drip-zone.online/checkout\n` +
+ `📱 *Descargar App Android:* https://drip-zone.online/app-dripzone.apk\n\n` +
+ `💡 *¿Cómo funciona tu compra?*\n` +
+ `1️⃣ Elige tu modelo y talla en el catálogo.\n` +
+ `2️⃣ Los pedidos y pagos se procesan en la web o app para asegurar tu factura y despacho inmediato.\n` +
+ `3️⃣ Realizas un anticipo de *$10.000* por Bold (PSE, tarjetas o Nequi) y pagas el saldo restante *contraentrega* al recibir en tu puerta.\n` +
+ `4️⃣ Tu primer cambio de talla es 100% gratuito.\n\n` +
+ `💬 *¿Te gustó algún modelo de nuestro catálogo de WhatsApp?* Indícanos el nombre o tu talla y con gusto te asesoramos y enviamos el enlace directo.`
+ );
+
+ await sendWhatsAppMessage(customerPhone, generalResponse);
+
+ return NextResponse.json({
+ success: true,
+ handled: 'general_commercial_greeting',
+ customerPhone,
+ });
+ }
+
+ return NextResponse.json({
+ success: true,
+ handled: 'advisor_conversation_active',
+ customerPhone,
+ });
+ }
+
+ // =========================================================================
+ // CASO B: GRUPOS MAYORISTAS DE PROVEEDORES (Monitoreo y Publicación Automática)
+ // =========================================================================
const groupSubject =
data.groupInfo?.subject ||
data.chatName ||
data.pushName ||
- (remoteJid.includes('g.us') ? 'Grupo Mayorista' : 'Chat Directo');
+ 'Grupo Mayorista';
// 1. VALIDAR SI EL GRUPO ESTÁ MONITOREADO
const [appwriteGroups, localGroups] = await Promise.all([
@@ -228,7 +359,7 @@ export async function POST(request: Request) {
salesCount: proof.salesCount,
originBadge: proof.badge,
supplierName: groupSubject,
- supplierPhone: data.key?.participant?.replace(/\D/g, '') || '573167455877',
+ supplierPhone: data.key?.participant?.replace(/\D/g, '') || dataService.getSettings()?.phone || '573106390296',
whatsappMessageId,
};
@@ -236,6 +367,11 @@ export async function POST(request: Request) {
const addedProduct = dataService.addProduct(newProduct);
await saveProductToAppwrite(addedProduct);
+ // Registrar automáticamente en el Catálogo de WhatsApp de la tienda (Meta Commerce)
+ await registerProductInWhatsAppCatalog(addedProduct).catch((e) =>
+ console.warn('[WhatsApp Catalog Auto-Sync from Webhook Error]:', e)
+ );
+
// Registrar también como post publicado en el feed interno
const newPost: SupplierPost = {
id: `post-${whatsappMessageId}`,
diff --git a/dripzone-app/src/app/api/whatsapp-feed/route.ts b/dripzone-app/src/app/api/whatsapp-feed/route.ts
index 2b2d249..b21d9a3 100644
--- a/dripzone-app/src/app/api/whatsapp-feed/route.ts
+++ b/dripzone-app/src/app/api/whatsapp-feed/route.ts
@@ -13,6 +13,7 @@ import {
getProductsFromAppwrite,
} from '@/lib/server-appwrite';
import { SupplierPost, MonitoredWhatsAppGroup } from '@/types';
+import { registerProductInWhatsAppCatalog } from '@/lib/whatsapp-catalog';
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
const EVOLUTION_API_KEY = process.env.EVOLUTION_FEED_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
@@ -424,12 +425,13 @@ export async function POST(request: Request) {
salesCount: proof.salesCount,
originBadge: proof.badge,
supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista',
- supplierPhone: post.supplierPhone || '573167455877',
+ supplierPhone: post.supplierPhone || dataService.getSettings()?.phone || '573106390296',
whatsappMessageId: whatsappMsgId,
};
const added = dataService.addProduct(newProduct as any);
await saveProductToAppwrite(added);
+ await registerProductInWhatsAppCatalog(added).catch(() => {});
await updateSupplierPostStatusInAppwrite(post.id, 'published');
dataService.updateSupplierPostStatus(post.id, 'published');
@@ -495,12 +497,13 @@ export async function POST(request: Request) {
salesCount: proof.salesCount,
originBadge: proof.badge,
supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista',
- supplierPhone: p.supplierPhone || '573167455877',
+ supplierPhone: p.supplierPhone || dataService.getSettings()?.phone || '573106390296',
whatsappMessageId: whatsappMsgId,
};
const added = dataService.addProduct(newProd as any);
await saveProductToAppwrite(added);
+ await registerProductInWhatsAppCatalog(added).catch(() => {});
await updateSupplierPostStatusInAppwrite(p.id, 'published');
dataService.updateSupplierPostStatus(p.id, 'published');
createdProducts.push(added);
diff --git a/dripzone-app/src/app/checkout/page.tsx b/dripzone-app/src/app/checkout/page.tsx
index b386fbe..d703df8 100644
--- a/dripzone-app/src/app/checkout/page.tsx
+++ b/dripzone-app/src/app/checkout/page.tsx
@@ -21,6 +21,7 @@ export default function CheckoutPage() {
const [address, setAddress] = useState('');
const [neighborhood, setNeighborhood] = useState('');
const [notes, setNotes] = useState('');
+ const [orderSource, setOrderSource] = useState('web');
const [cities, setCities] = useState(INITIAL_CITIES);
const [paymentMethods, setPaymentMethods] = useState(INITIAL_PAYMENT_METHODS);
@@ -28,7 +29,7 @@ export default function CheckoutPage() {
const [selectedPaymentId, setSelectedPaymentId] = useState('pay-deposit-10k');
const [storeSettings, setStoreSettings] = useState(null);
- const [checkoutItems] = useState>(
- INITIAL_PRODUCTS.length > 0
- ? [
- {
- productId: INITIAL_PRODUCTS[0].id,
- productName: INITIAL_PRODUCTS[0].name,
- brand: INITIAL_PRODUCTS[0].brand,
- size: '39',
- quantity: 1,
- price: INITIAL_PRODUCTS[0].retailPrice,
- image: INITIAL_PRODUCTS[0].images[0],
- },
- ]
- : []
- );
+ 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(null);
@@ -86,6 +87,104 @@ export default function CheckoutPage() {
.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(() => {
@@ -143,7 +242,8 @@ export default function CheckoutPage() {
balanceDue,
paymentMethodId: activePayment.id,
paymentMethodName: activePayment.name,
- notes,
+ notes: notes ? `${notes} (Origen: ${orderSource})` : `Origen: ${orderSource}`,
+ source: orderSource,
};
const res = await fetch('/api/orders', {
@@ -471,6 +571,38 @@ export default function CheckoutPage() {
+ {/* Banner de Pedido Originado en WhatsApp */}
+ {orderSource === 'whatsapp' && (
+
+
+
+
+
+
+
+
¡Pedido iniciado desde WhatsApp!
+
+ Atención Comercial
+
+
+
+ Tu calzado y talla seleccionada están reservados. Completa tu dirección y confirma el anticipo de $10.000 por Bold (PSE, tarjetas o Nequi) para despachar hoy. El saldo restante lo pagas contraentrega al recibir en tu puerta.
+