/** * ╔══════════════════════════════════════════════════════════════════════╗ * ║ KOBIA System — Script Maestro v5.7 NO setNumberFormat ║ * ║ Proyecto standalone en Google Drive ║ * ║ Un solo script para TODOS los clientes ║ * ║ ║ * ║ CAMBIOS EN v5.7: ║ * ║ • CRÍTICO: Eliminado setNumberFormat completamente ║ * ║ • Google Sheets auto-detecta el tipo de dato ║ * ║ • Simplifica guardado y elimina errores de formato ║ * ║ ║ * ║ CAMBIOS EN v5.6: ║ * ║ • Fix setNumberFormat en campos numéricos ║ * ║ • Solo aplicaba formato texto en campos NO numéricos ║ * ╚══════════════════════════════════════════════════════════════════════╝ */ // ── Configuración global ───────────────────────────────────────────── const CFG = { SHEET_PRODUCTOS: 'Productos', SHEET_CONFIG: 'Config', SHEET_MASTER: 'Clientes', ID_HOJA_MAESTRA: '1G30mdmJSpW5gzJTenjhHizjpB_iRfN7PkWDusJ3yrP8', ID_PUBLIC_DATA: '1uY80XwQ9Y4_uxh10iXfAHKxuA6orVIIpvAsaa7b0Spw', ID_PLANTILLA: '134WAp1lJzsZ3FiHuFqeUyma9CWbKQkZliWejAB7XQAU', ID_CARPETA_RAIZ: '1bqYdXug2jWrADsKgzGJDlj23RLQD2yPZ', // ← ID fijo, nunca cambia CARPETA_CLIENTES: 'Clientes', // ← subcarpeta dentro de la raíz // Credenciales de administrador KOBIA ADMIN_USER: 'kobia_admin', ADMIN_PASS: 'kobia2026', }; // ── Encabezados definitivos de la hoja Productos ───────────────────── const HDRS_PRODUCTOS = [ 'id', 'nombre', 'descripcion', 'precio', 'precio_oferta', 'categoria', 'subcategoria', 'imagen', 'stock', 'destacado', 'estado', 'etiqueta', 'variante' ]; // ── Encabezados definitivos de la hoja Config ──────────────────────── const HDRS_CONFIG = [ 'propiedad', 'valor' ]; // ── Campos Config definitivos ──────────────────────────────────────── const CAMPOS_CONFIG = [ 'nombre_marca', 'whatsapp', 'color_botones', 'color_fondos', 'color_detalles', 'color_titulos', 'color_nombre_producto', 'banner_portada', 'foto_perfil', 'frase_bienvenida', 'aviso_promocion', 'direccion_fisica', 'texto_pedido', 'instagram', 'tiktok', 'facebook', 'telefono', 'columnas_escritorio', 'columnas_movil', 'categorias', 'link_portafolio', 'tipo_plantilla', 'email' ]; // ============================================ // ============================================ // GENERACIÓN DE SLUG // ============================================ function generateSlug(nombreMarca) { if (!nombreMarca) { throw new Error('Nombre de marca requerido para generar slug'); } return nombreMarca .toLowerCase() .trim() .normalize('NFD').replace(/[\u0300-\u036f]/g, '') .replace(/ñ/g, 'n') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .substring(0, 50) .replace(/-+$/, ''); } function validateUniqueSlug(slug, masterSheetId) { const ss = SpreadsheetApp.openById(masterSheetId); const clientsSheet = ss.getSheetByName('Clientes'); if (!clientsSheet) { throw new Error('Hoja Clientes no encontrada'); } const data = clientsSheet.getDataRange().getValues(); const headers = data[0].map(h => h.toString().toLowerCase()); const slugIndex = headers.indexOf('slug'); if (slugIndex === -1) { Logger.log('⚠️ Columna SLUG no encontrada - se asume que no hay slugs existentes'); return slug; } const existingSlugs = data.slice(1) .map(row => row[slugIndex]?.toString().toLowerCase().trim()) .filter(s => s); let finalSlug = slug; let counter = 1; while (existingSlugs.includes(finalSlug.toLowerCase())) { finalSlug = `${slug}-${counter}`; counter++; } Logger.log('✅ Slug validado: ' + finalSlug); return finalSlug; } // ============================================ // EMAIL DE BIENVENIDA (HTML) // ============================================ function sendWelcomeEmail(clientData) { const { nombreMarca, email, whatsapp, idKobia, slug } = clientData; const subject = `¡Bienvenido a KOBIA, ${nombreMarca}!`; const htmlBody = `

Inteligencia Ancestral

¡Bienvenido a KOBIA, ${nombreMarca}!

Tu catálogo digital profesional está listo para usar. Compártelo con tus clientes y empieza a vender hoy mismo.

🔗 Tu catálogo público

https://kobia.com.co/${slug}

Comparte este link con tus clientes por WhatsApp, redes sociales o donde quieras.

👤 Panel de administración

https://kobia.com.co/catalogo/panel.html

Usuario: ${whatsapp}

Contraseña: ${idKobia}

Desde aquí puedes configurar tu catálogo, agregar productos, cambiar colores y mucho más.

Ir a mi panel

💡 Próximos pasos:

  1. Entra a tu panel de administración
  2. Personaliza los colores de tu marca
  3. Agrega tus productos
  4. ¡Comparte tu catálogo y empieza a vender!
`; try { GmailApp.sendEmail(email, subject, "", { name: "KOBIA", from: "soporte@kobia.com.co", htmlBody: htmlBody }); Logger.log('✅ Email enviado a: ' + email); } catch (error) { Logger.log('❌ Error enviando email: ' + error); } } // ============================================ // EMAIL DE VALIDACIÓN DE LEAD PENDIENTE (HTML) // ============================================ function sendLeadValidationEmail(email, nombreMarca) { const subject = `¡Solicitud Recibida! Preparando tu Catálogo KOBIA para ${nombreMarca}`; const htmlBody = `

¡Hola! Hemos recibido con éxito tu solicitud para ${nombreMarca}

Gracias por elegir a KOBIA para potenciar tus ventas por WhatsApp. Hemos registrado tu pedido en nuestro sistema central.

¿Qué sigue ahora?

Tu membresía y tu Panel de Administrador privado se encuentran en estado Pendiente de Activación.

En el transcurso del día, un asesor de KOBIA se pondrá en contacto contigo a tu WhatsApp para confirmar los últimos detalles, brindarte asesoría y enviarte tus contraseñas de acceso oficiales.

Prepárate para llevar tu negocio a otro nivel.
Nos hablamos pronto.

`; try { GmailApp.sendEmail(email, subject, "", { name: "KOBIA", from: "soporte@kobia.com.co", htmlBody: htmlBody }); Logger.log('✅ Email validación Lead enviado a: ' + email); } catch (error) { Logger.log('❌ Error enviando email validación Lead: ' + error); } } // ══════════════════════════════════════════════════════════════════════ // PUNTO DE ENTRADA — GET // ══════════════════════════════════════════════════════════════════════ function doGet(e) { const params = (e && e.parameter) || {}; const action = params.action || 'check'; const sheetId = params.sheetId || ''; try { switch (action) { case 'check': { if (!sheetId) return R({ ok: false, error: 'Falta sheetId' }); const ss = SpreadsheetApp.openById(sheetId); const sheet = ss.getSheetByName(CFG.SHEET_PRODUCTOS); return R({ ok: true, nombre: ss.getName(), productos: sheet ? Math.max(0, sheet.getLastRow() - 1) : 0 }); } default: return R({ ok: false, error: 'Acción GET no reconocida: ' + action }); } } catch (err) { Logger.log('ERROR doGet [' + action + ']: ' + err.message); return R({ ok: false, error: err.message }); } } // ══════════════════════════════════════════════════════════════════════ // PUNTO DE ENTRADA — POST (VERSIÓN SUPER-DEFENSIVA v5.4.1) // ══════════════════════════════════════════════════════════════════════ function doPost(e) { // ═══ LOG 1: Verificar que la función se ejecuta ═══════════════════ Logger.log('═══════════════════════════════════════════════════'); Logger.log('[KOBIA v5.4.1] ⚡ doPost() INICIADO'); Logger.log('[KOBIA] Timestamp: ' + new Date().toISOString()); Logger.log('═══════════════════════════════════════════════════'); try { // ═══ LOG 2: Verificar que e existe ═══════════════════════════════ if (!e) { Logger.log('[KOBIA] ❌ ERROR: Parámetro "e" es null o undefined'); return R({ ok: false, error: 'Parámetro de request vacío' }); } Logger.log('[KOBIA] ✅ Parámetro "e" existe'); // ═══ LOG 3: Verificar que e.postData existe ═══════════════════════ if (!e.postData) { Logger.log('[KOBIA] ❌ ERROR: e.postData es null o undefined'); return R({ ok: false, error: 'No hay datos POST' }); } Logger.log('[KOBIA] ✅ e.postData existe'); // ═══ LOG 4: Verificar que e.postData.contents existe ═════════════ if (!e.postData.contents) { Logger.log('[KOBIA] ❌ ERROR: e.postData.contents es null o undefined'); return R({ ok: false, error: 'POST sin contenido' }); } const rawData = e.postData.contents; Logger.log('[KOBIA] ✅ Raw data recibido'); Logger.log('[KOBIA] 📦 Tamaño: ' + rawData.length + ' bytes'); Logger.log('[KOBIA] 📝 Primeros 200 chars: ' + rawData.substring(0, 200)); // ═══ LOG 5: Intentar parsear JSON ════════════════════════════════ let data; try { data = JSON.parse(rawData); Logger.log('[KOBIA] ✅ JSON parseado correctamente'); } catch(parseErr) { Logger.log('[KOBIA] ❌ ERROR parseando JSON: ' + parseErr.message); Logger.log('[KOBIA] 🔍 Raw data completo: ' + rawData); return R({ ok: false, error: 'JSON inválido: ' + parseErr.message }); } // ═══ LOG 6: Verificar action ═════════════════════════════════════ const action = data.action || ''; Logger.log('[KOBIA] 📋 Action recibida: ' + (action || '(vacía)')); Logger.log('[KOBIA] 📦 Keys en payload: ' + Object.keys(data).join(', ')); if (!action) { Logger.log('[KOBIA] ⚠️ ADVERTENCIA: Action está vacía'); } // Acciones del Panel Master autorizadas per se const MASTER_ACTIONS = ['clonar', 'createClient', 'suspender', 'activar', 'registrarPreCliente', 'listarClientes']; // ═══ LOG 7: Verificar tipo de acción ═════════════════════════════ const esMasterAction = MASTER_ACTIONS.includes(action); Logger.log('[KOBIA] 🔍 Es acción Master: ' + (esMasterAction ? 'SÍ' : 'NO')); // Si NO es una acción maestra, exigimos credenciales de auth if (!esMasterAction) { Logger.log('[KOBIA] 🔐 Requiere autenticación de cliente'); const whatsapp = data._auth_whatsapp || data.whatsapp || ''; const pin = data._auth_pin || data.pin || ''; Logger.log('[KOBIA] 📱 WhatsApp recibido: ' + (whatsapp || '(vacío)')); Logger.log('[KOBIA] 🔑 PIN recibido: ' + (pin || '(vacío)')); // ═══ LOG 8: Verificar credenciales ═══════════════════════════ if (!whatsapp || !pin) { Logger.log('[KOBIA] ❌ ERROR: Faltan credenciales'); Logger.log('[KOBIA] WhatsApp: ' + (whatsapp ? 'presente' : 'FALTA')); Logger.log('[KOBIA] PIN: ' + (pin ? 'presente' : 'FALTA')); return R({ ok: false, error: 'Autenticación requerida: Envíe _auth_whatsapp y _auth_pin en el JSON.' }); } Logger.log('[KOBIA] ✅ Credenciales presentes, iniciando autenticación...'); // ═══ LOG 9: Autenticar usuario ═══════════════════════════════ let sheetIdAutorizado; try { sheetIdAutorizado = autenticarUsuario(whatsapp, pin); Logger.log('[KOBIA] 🔍 Resultado autenticación: ' + (sheetIdAutorizado || 'FALLO')); } catch(authErr) { Logger.log('[KOBIA] ❌ ERROR en autenticarUsuario(): ' + authErr.message); Logger.log('[KOBIA] 🔍 Stack: ' + authErr.stack); return R({ ok: false, error: 'Error de autenticación: ' + authErr.message }); } if (!sheetIdAutorizado) { Logger.log('[KOBIA] ❌ Autenticación FALLIDA - Credenciales inválidas'); return R({ ok: false, error: 'Credenciales inválidas (WhatsApp o PIN incorrecto)' }); } Logger.log('[KOBIA] ✅ Autenticación EXITOSA'); Logger.log('[KOBIA] 🆔 SheetId autorizado: ' + sheetIdAutorizado); // ═══ LOG 10: Ejecutar acción ═════════════════════════════════ Logger.log('[KOBIA] 🚀 Ejecutando acción: ' + action); // Ejecutamos las modificaciones únicamente sobre la hoja autorizada switch (action) { case 'login': Logger.log('[KOBIA] → Llamando loginPanel()'); return loginPanel(whatsapp, pin, sheetIdAutorizado); case 'agregar': Logger.log('[KOBIA] → Llamando agregarProducto()'); return agregarProducto(sheetIdAutorizado, data); case 'editar': Logger.log('[KOBIA] → Llamando editarProducto()'); return editarProducto(sheetIdAutorizado, data); case 'desactivar': Logger.log('[KOBIA] → Llamando cambiarEstadoProducto(inactivo)'); return cambiarEstadoProducto(sheetIdAutorizado, data.id, 'inactivo'); case 'activar_producto': Logger.log('[KOBIA] → Llamando cambiarEstadoProducto(activo)'); return cambiarEstadoProducto(sheetIdAutorizado, data.id, 'activo'); case 'config': Logger.log('[KOBIA] → Llamando guardarConfig()'); return guardarConfig(sheetIdAutorizado, data); case 'diagnostico': Logger.log('[KOBIA] → Llamando diagnosticarConfig()'); return diagnosticarConfig(sheetIdAutorizado, data); default: Logger.log('[KOBIA] ❌ Acción no reconocida: ' + action); return R({ ok: false, error: 'Acción no reconocida: ' + action }); } } else { // ═══ ACCIONES DEL PANEL MASTER ═══════════════════════════════ Logger.log('[KOBIA] 👑 Ejecutando acción MASTER: ' + action); switch (action) { case 'clonar': case 'createClient': Logger.log('[KOBIA] → Llamando crearCliente()'); return crearCliente(data); case 'suspender': Logger.log('[KOBIA] → Llamando cambiarEstadoCliente(Suspendido)'); return cambiarEstadoCliente(data.idKobia, 'Suspendido'); case 'activar': Logger.log('[KOBIA] → Llamando cambiarEstadoCliente(Activo)'); return cambiarEstadoCliente(data.idKobia, 'Activo'); case 'registrarPreCliente': Logger.log('[KOBIA] → Llamando registrarLeadPendiente()'); return registrarLeadPendiente(data); case 'listarClientes': Logger.log('[KOBIA] → Llamando listarClientes()'); return listarClientes(data); default: Logger.log('[KOBIA] ❌ Acción MAESTRA no reconocida: ' + action); return R({ ok: false, error: 'Acción MAESTRA no reconocida: ' + action }); } } } catch (err) { Logger.log('═══════════════════════════════════════════════════'); Logger.log('[KOBIA] ❌❌❌ ERROR CRÍTICO EN doPost() ❌❌❌'); Logger.log('[KOBIA] Error: ' + err.message); Logger.log('[KOBIA] Stack completo:'); Logger.log(err.stack); Logger.log('═══════════════════════════════════════════════════'); return R({ ok: false, error: err.message }); } } // ══════════════════════════════════════════════════════════════════════ // SISTEMA CENTRAL DE AUTENTICACIÓN // ══════════════════════════════════════════════════════════════════════ function autenticarUsuario(whatsappEnviado, pinEnviado) { const wsMaster = SpreadsheetApp.openById(CFG.ID_HOJA_MAESTRA); const hojaClientes = wsMaster.getSheetByName(CFG.SHEET_MASTER); const data = hojaClientes.getDataRange().getValues(); Logger.log('[AUTH] Filas en hoja Clientes: ' + data.length); Logger.log('[AUTH] Encabezados: ' + JSON.stringify(data[0])); if (data.length < 2) return false; const headers = data[0].map(h => String(h).toLowerCase().replace(/[\s_\-]+/g, '')); Logger.log('[AUTH] Headers normalizados: ' + JSON.stringify(headers)); const whatsappColIdx = headers.findIndex(h => h === 'whatsappid' || h === 'whatsapp'); const sheetIdColIdx = headers.findIndex(h => h === 'idsheetcliente' || h === 'idsheet'); const idKobiaColIdx = 0; Logger.log('[AUTH] Col whatsapp: ' + whatsappColIdx + ' | Col sheetId: ' + sheetIdColIdx); if (whatsappColIdx === -1 || sheetIdColIdx === -1) { throw new Error('Validación corrompida: Faltan las columnas base whatsapp_id o id_sheet_cliente'); } const targetWhatsapp = String(whatsappEnviado).replace(/[^0-9]/g, '').slice(-10); const targetPin = String(pinEnviado).trim(); Logger.log('[AUTH] Buscando whatsapp (10 dígitos): ' + targetWhatsapp + ' | pin: ' + targetPin); for (let i = 1; i < data.length; i++) { let dbWhatsapp = String(data[i][whatsappColIdx]).replace(/[^0-9]/g, '').slice(-10); let dbPin = String(data[i][idKobiaColIdx]).trim(); Logger.log('[AUTH] Fila ' + i + ': whatsapp=' + dbWhatsapp + ' pin=' + dbPin); if (dbWhatsapp === targetWhatsapp && dbPin === targetPin) { Logger.log('[AUTH] ✅ Cliente encontrado. SheetId: ' + data[i][sheetIdColIdx]); return String(data[i][sheetIdColIdx]).trim(); } } Logger.log('[AUTH] ❌ Cliente no encontrado'); return false; } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: CREAR CLIENTE (MASTER) // Estructura Drive: // [ID_CARPETA_RAIZ]/ // └── Clientes/ // └── YYMM-NNN-CAT-MARCA/ ← carpeta cliente // ├── YYMM-NNN-CAT-MARCA ← Sheet // └── YYMM-NNN-IMG/ ← imágenes // ══════════════════════════════════════════════════════════════════════ function crearCliente(data) { Logger.log('═══════════════════════════════════════════════════'); Logger.log('👤 CREANDO NUEVO CLIENTE'); Logger.log('═══════════════════════════════════════════════════'); Logger.log('📦 Datos recibidos desde master.html:'); Logger.log(' nombreMarca: ' + (data.nombreMarca || '(vacío)')); Logger.log(' whatsappId: ' + (data.whatsappId || '(vacío)')); Logger.log(' whatsapp: ' + (data.whatsapp || '(vacío)')); Logger.log(' email: ' + (data.email || '(vacío)')); Logger.log(' categoria: ' + (data.categoria || '(vacío)')); Logger.log(' plan: ' + (data.plan || '(vacío)')); Logger.log(' meses: ' + (data.meses || '(vacío)')); Logger.log('═══════════════════════════════════════════════════'); const ss = SpreadsheetApp.openById(CFG.ID_HOJA_MAESTRA); const clientsSheet = ss.getSheetByName(CFG.SHEET_MASTER); if (!clientsSheet) throw new Error('Hoja Clientes no encontrada'); // ── 1. Generar ID_KOBIA (YYMM-NNN) ─────────────────────────────── const yymm = Utilities.formatDate(new Date(), 'GMT-5', 'yyMM'); const allIds = clientsSheet.getLastRow() > 1 ? clientsSheet.getRange(2, 1, clientsSheet.getLastRow() - 1, 1).getValues() .map(r => r[0].toString()) .filter(id => !id.startsWith('LEAD-') && id.includes('-')) .map(id => parseInt(id.split('-').pop()) || 0) : []; const nextNum = allIds.length > 0 ? Math.max(...allIds) + 1 : 1; const idKobia = `${yymm}-${String(nextNum).padStart(3, '0')}`; const catUp = (data.categoria || 'VAR').toString().toUpperCase().trim(); const marcaUp = (data.nombreMarca || '').toString().toUpperCase().replace(/\s+/g, '-').trim(); // Nombres normalizados siguiendo sistema de codificación const nombreCarpetaCliente = `${idKobia}-${catUp}-${marcaUp}`; // 2603-001-MOD-VALENTINA const nombreSheet = nombreCarpetaCliente; // mismo nombre para el Sheet const nombreCarpetaImg = `${idKobia}-IMG`; // 2603-001-IMG Logger.log('🆔 ID_KOBIA: ' + idKobia); Logger.log('📁 Carpeta cliente: ' + nombreCarpetaCliente); Logger.log('🖼️ Carpeta imágenes: ' + nombreCarpetaImg); // ── 2. Generar slug único ───────────────────────────────────────── const baseSlug = generateSlug(data.nombreMarca); const slug = validateUniqueSlug(baseSlug, CFG.ID_HOJA_MAESTRA); Logger.log('🔗 SLUG: ' + slug); // ── 3. Crear estructura de carpetas en Drive ────────────────────── // Raíz fija por ID — nunca busca por nombre const carpetaRaiz = DriveApp.getFolderById(CFG.ID_CARPETA_RAIZ); // Subcarpeta "Clientes" dentro de la raíz const iterClientes = carpetaRaiz.getFoldersByName(CFG.CARPETA_CLIENTES); const carpetaClientes = iterClientes.hasNext() ? iterClientes.next() : carpetaRaiz.createFolder(CFG.CARPETA_CLIENTES); // Carpeta individual del cliente const carpetaCliente = carpetaClientes.createFolder(nombreCarpetaCliente); // Carpeta de imágenes con ID único const carpetaImagenes = carpetaCliente.createFolder(nombreCarpetaImg); Logger.log('✅ Estructura de carpetas creada'); // ── 4. Clonar plantilla dentro de la carpeta del cliente ────────── const plantilla = DriveApp.getFileById(CFG.ID_PLANTILLA); const nuevoSheet = plantilla.makeCopy(nombreSheet, carpetaCliente); const newSheetId = nuevoSheet.getId(); // Permisos públicos de lectura (necesario para CSV) try { nuevoSheet.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); } catch(e) { Logger.log('⚠️ No se pudieron configurar permisos: ' + e.message); } Logger.log('✅ Sheet clonado: ' + newSheetId); // ── 5. Registrar en hoja Clientes (privada) ─────────────────────── const fechaInicio = new Date(); const meses = parseInt(data.meses) || 1; const fechaFin = new Date(fechaInicio); fechaFin.setMonth(fechaFin.getMonth() + meses); const fmt = (d) => { return String(d.getDate()).padStart(2,'0') + '/' + String(d.getMonth()+1).padStart(2,'0') + '/' + d.getFullYear(); }; const tipoPlan = (data.tipo_plantilla || data.tipoPlantilla || 'catalogo').toLowerCase().trim(); clientsSheet.appendRow([ idKobia, // A — ID_KOBIA data.categoria, // B — CATEGORIA data.nombreMarca, // C — NOMBRE_MARCA data.whatsappId || data.whatsapp || '', // D — WHATSAPP_ID (campo de autenticación) data.email, // E — EMAIL newSheetId, // F — ID_SHEET_CLIENTE 'Activo', // G — ESTADO data.plan || 'base', // H — PLAN fmt(fechaInicio), // I — FECHA_INICIO meses, // J — MESES fmt(fechaFin), // K — FECHA_FIN slug, // L — SLUG tipoPlan // M — TIPO_PLANTILLA ]); Logger.log('✅ Cliente registrado en hoja Clientes'); // ── 6. Escribir tipo_plantilla en Config del cliente nuevo ─────── // Esto activa el redirect automático en catalogo.html sin acción del admin if (tipoPlan === 'portafolio') { try { const newSs = SpreadsheetApp.openById(newSheetId); const cfgSheet = obtenerOCrearHoja(newSs, CFG.SHEET_CONFIG, HDRS_CONFIG); const cfgData = cfgSheet.getDataRange().getValues(); const tipoExiste = cfgData.some(row => norm(String(row[0])) === 'tipoplantilla'); if (!tipoExiste) { cfgSheet.appendRow(['tipo_plantilla', 'portafolio']); Logger.log('✅ tipo_plantilla=portafolio escrito en Config del cliente'); } } catch(e) { Logger.log('⚠️ No se pudo escribir tipo_plantilla en Config: ' + e.message); } } // ── 7. Eliminar lead temporal si viene de conversión ───────────── if (data.leadIdToDelete) { const filas = clientsSheet.getDataRange().getValues(); const rowIdx = filas.findIndex((r, i) => i > 0 && r[0] === data.leadIdToDelete); if (rowIdx >= 0) { clientsSheet.deleteRow(rowIdx + 1); Logger.log('🗑️ Lead temporal eliminado: ' + data.leadIdToDelete); } } // ── 8. Registrar slug en archivo público de Slugs ───────────────── try { const publicSs = SpreadsheetApp.openById(CFG.ID_PUBLIC_DATA); let slugsSheet = publicSs.getSheetByName('Slugs'); if (!slugsSheet) { slugsSheet = publicSs.insertSheet('Slugs'); slugsSheet.appendRow(['slug', 'id_sheet']); } slugsSheet.appendRow([slug, newSheetId]); Logger.log('✅ Slug registrado en archivo público'); } catch(e) { Logger.log('⚠️ Error registrando slug público: ' + e.message); } // ── 9. Email de bienvenida ──────────────────────────────────────── const whatsappParaEmail = data.whatsappId || data.whatsapp || ''; Logger.log('📧 Preparando email de bienvenida...'); Logger.log(' Destinatario: ' + data.email); Logger.log(' WHATSAPP_ID (usuario): ' + whatsappParaEmail); Logger.log(' ID_KOBIA (contraseña): ' + idKobia); Logger.log(' Slug: ' + slug); sendWelcomeEmail({ nombreMarca: data.nombreMarca, email: data.email, whatsapp: whatsappParaEmail, // WHATSAPP_ID (usuario de autenticación) idKobia: idKobia, slug: slug }); Logger.log('✅ Email de bienvenida enviado'); Logger.log('✅ Cliente creado exitosamente: ' + idKobia); return R({ ok: true, idKobia: idKobia, sheetId: newSheetId, slug: slug, nombreCarpeta: nombreCarpetaCliente, nombreCarpetaImg: nombreCarpetaImg, catalogUrl: `https://kobia.com.co/${slug}`, panelUrl: 'https://kobia.com.co/catalogo/panel.html' }); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: AGREGAR PRODUCTO // ══════════════════════════════════════════════════════════════════════ function agregarProducto(sheetId, data) { const ss = SpreadsheetApp.openById(sheetId); const sheet = obtenerOCrearHoja(ss, CFG.SHEET_PRODUCTOS, HDRS_PRODUCTOS); const lastRow = sheet.getLastRow(); let nextId = 1; if (lastRow >= 2) { const ids = sheet.getRange(2, 1, lastRow - 1, 1).getValues() .map(r => parseInt(r[0]) || 0); nextId = Math.max(...ids) + 1; } let imageUrl = data.imagenUrl || ''; if (data.imagen && data.imagen.indexOf('base64,') > -1) { const carpeta = obtenerCarpetaCliente(sheetId); imageUrl = subirImagen(data.imagen, data.imagenTipo, 'prod_' + nextId, carpeta); } const fila = construirFila(HDRS_PRODUCTOS, data, imageUrl, nextId, 'activo'); sheet.appendRow(fila); Logger.log('[KOBIA] Producto agregado — sheet:' + sheetId + ' id:' + nextId); return R({ ok: true, id: nextId, imageUrl }); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: EDITAR PRODUCTO // ══════════════════════════════════════════════════════════════════════ function editarProducto(sheetId, data) { const ss = SpreadsheetApp.openById(sheetId); const sheet = ss.getSheetByName(CFG.SHEET_PRODUCTOS); if (!sheet) throw new Error('No existe la pestaña Productos'); const id = parseInt(data.id); if (!id) throw new Error('Se requiere el id del producto'); const valores = sheet.getDataRange().getValues(); const rowIndex = valores.findIndex((row, i) => i > 0 && parseInt(row[0]) === id); if (rowIndex < 0) throw new Error('Producto no encontrado: id=' + id); const hdrsNorm = valores[0].map(norm); const idxImg = hdrsNorm.indexOf('imagen'); let imageUrl = data.imagenUrl || (idxImg >= 0 ? valores[rowIndex][idxImg] : '') || ''; if (data.imagen && data.imagen.indexOf('base64,') > -1) { const carpeta = obtenerCarpetaCliente(sheetId); imageUrl = subirImagen(data.imagen, data.imagenTipo, 'prod_' + id, carpeta); } const fila = construirFila(valores[0], data, imageUrl, id, data.estado || 'activo'); sheet.getRange(rowIndex + 1, 1, 1, fila.length).setValues([fila]); Logger.log('[KOBIA] Producto editado — sheet:' + sheetId + ' id:' + id); return R({ ok: true, id, imageUrl }); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: CAMBIAR ESTADO PRODUCTO (activo / inactivo) // ══════════════════════════════════════════════════════════════════════ function cambiarEstadoProducto(sheetId, id, estado) { const ss = SpreadsheetApp.openById(sheetId); const sheet = ss.getSheetByName(CFG.SHEET_PRODUCTOS); if (!sheet) throw new Error('No existe la pestaña Productos'); const idNum = parseInt(id); const valores = sheet.getDataRange().getValues(); const hdrs = valores[0].map(norm); const idxEstado = hdrs.indexOf('estado'); const rowIndex = valores.findIndex((row, i) => i > 0 && parseInt(row[0]) === idNum); if (rowIndex < 0) throw new Error('Producto no encontrado: id=' + id); if (idxEstado < 0) { const newCol = sheet.getLastColumn() + 1; sheet.getRange(1, newCol).setValue('estado'); sheet.getRange(rowIndex + 1, newCol).setValue(estado); } else { sheet.getRange(rowIndex + 1, idxEstado + 1).setValue(estado); } Logger.log('[KOBIA] Estado producto ' + id + ' → ' + estado); return R({ ok: true, id: idNum, estado }); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: DIAGNOSTICAR CONFIG (v5.4 DEBUG) // ══════════════════════════════════════════════════════════════════════ function diagnosticarConfig(sheetId, data) { const resultado = { timestamp: new Date().toISOString(), sheetId: sheetId, diagnostico: {} }; Logger.log('═══════════════════════════════════════════════════'); Logger.log('[DIAGNÓSTICO] Iniciando diagnóstico completo'); Logger.log('═══════════════════════════════════════════════════'); try { // 1. Verificar Sheet resultado.diagnostico.sheet = { id: sheetId, existe: false, accesible: false }; try { const ss = SpreadsheetApp.openById(sheetId); resultado.diagnostico.sheet.existe = true; resultado.diagnostico.sheet.nombre = ss.getName(); resultado.diagnostico.sheet.accesible = true; Logger.log('[DIAGNÓSTICO] ✅ Sheet accesible: ' + ss.getName()); } catch(e) { resultado.diagnostico.sheet.error = e.message; Logger.log('[DIAGNÓSTICO] ❌ Error abriendo Sheet: ' + e.message); return R(resultado); } // 2. Verificar hoja Config resultado.diagnostico.config = { hojaExiste: false, headers: [], filas: 0 }; try { const ss = SpreadsheetApp.openById(sheetId); const sheet = ss.getSheetByName(CFG.SHEET_CONFIG); if (!sheet) { resultado.diagnostico.config.error = 'Hoja Config no existe'; Logger.log('[DIAGNÓSTICO] ❌ Hoja Config no existe'); } else { resultado.diagnostico.config.hojaExiste = true; const valores = sheet.getDataRange().getValues(); resultado.diagnostico.config.filas = valores.length - 1; if (valores.length > 0) { resultado.diagnostico.config.headers = valores[0]; } // Listar propiedades existentes resultado.diagnostico.config.propiedadesExistentes = []; for (let i = 1; i < Math.min(valores.length, 25); i++) { // Máximo 24 propiedades if (valores[i][0]) { resultado.diagnostico.config.propiedadesExistentes.push({ fila: i + 1, propiedad: valores[i][0], valor: valores[i][1] ? String(valores[i][1]).substring(0, 50) : '(vacío)' }); } } Logger.log('[DIAGNÓSTICO] ✅ Hoja Config existe con ' + resultado.diagnostico.config.filas + ' filas'); } } catch(e) { resultado.diagnostico.config.error = e.message; Logger.log('[DIAGNÓSTICO] ❌ Error leyendo Config: ' + e.message); } // 3. Verificar payload recibido resultado.diagnostico.payload = { camposRecibidos: Object.keys(data).filter(k => !k.startsWith('_') && k !== 'action'), camposEsperados: CAMPOS_CONFIG, camposFaltantes: [], camposExtra: [], valoresPorCampo: {} }; // Campos faltantes CAMPOS_CONFIG.forEach(campo => { const valor = data[campo]; if (valor === undefined || valor === null || valor === '') { resultado.diagnostico.payload.camposFaltantes.push(campo); } else { // Guardar primeros 50 caracteres del valor resultado.diagnostico.payload.valoresPorCampo[campo] = String(valor).substring(0, 50); } }); // Campos extra (no esperados) Object.keys(data).forEach(campo => { if (!campo.startsWith('_') && campo !== 'action' && !CAMPOS_CONFIG.includes(campo)) { resultado.diagnostico.payload.camposExtra.push(campo); } }); Logger.log('[DIAGNÓSTICO] 📦 Campos recibidos: ' + resultado.diagnostico.payload.camposRecibidos.length); Logger.log('[DIAGNÓSTICO] ⏭️ Campos faltantes: ' + resultado.diagnostico.payload.camposFaltantes.length); // 4. Verificar permisos de escritura resultado.diagnostico.permisos = { puedeEscribir: false }; try { const ss = SpreadsheetApp.openById(sheetId); const sheet = ss.getSheetByName(CFG.SHEET_CONFIG); if (sheet) { // Intentar escribir en una celda temporal const testRow = sheet.getLastRow() + 1; sheet.getRange(testRow, 1).setValue('__TEST_KOBIA__'); // Verificar que se escribió const testVal = sheet.getRange(testRow, 1).getValue(); if (testVal === '__TEST_KOBIA__') { resultado.diagnostico.permisos.puedeEscribir = true; // Limpiar celda de prueba sheet.deleteRow(testRow); Logger.log('[DIAGNÓSTICO] ✅ Permisos de escritura OK'); } } } catch(e) { resultado.diagnostico.permisos.error = e.message; Logger.log('[DIAGNÓSTICO] ❌ Error verificando permisos: ' + e.message); } // 5. Resumen resultado.resumen = { sheetAccesible: resultado.diagnostico.sheet.accesible, configExiste: resultado.diagnostico.config.hojaExiste, filasConfig: resultado.diagnostico.config.filas, camposRecibidos: resultado.diagnostico.payload.camposRecibidos.length, camposFaltantes: resultado.diagnostico.payload.camposFaltantes.length, puedeEscribir: resultado.diagnostico.permisos.puedeEscribir, diagnosticoCompleto: true }; } catch(err) { resultado.error = err.message; resultado.stack = err.stack; Logger.log('[DIAGNÓSTICO] ❌ Error general: ' + err.message); } Logger.log('═══════════════════════════════════════════════════'); Logger.log('[DIAGNÓSTICO] RESULTADO:'); Logger.log(JSON.stringify(resultado.resumen, null, 2)); Logger.log('═══════════════════════════════════════════════════'); return R(resultado); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: GUARDAR CONFIG // ══════════════════════════════════════════════════════════════════════ function guardarConfig(sheetId, data) { Logger.log('═══════════════════════════════════════════════════'); Logger.log('[CONFIG v5.4] ⚙️ INICIANDO GUARDADO'); Logger.log('[CONFIG] 🆔 SheetId: ' + sheetId); Logger.log('[CONFIG] 📦 Total campos en payload: ' + Object.keys(data).length); Logger.log('[CONFIG] 📋 Campos recibidos: ' + Object.keys(data).filter(k => !k.startsWith('_')).join(', ')); Logger.log('═══════════════════════════════════════════════════'); // ── VALIDACIÓN 1: SheetId válido ────────────────────────────── if (!sheetId || sheetId === 'null' || sheetId === 'undefined') { Logger.log('[CONFIG] ❌ ERROR CRÍTICO: sheetId inválido: ' + sheetId); throw new Error('SheetId inválido: ' + sheetId); } // ── VALIDACIÓN 2: Abrir Sheet ───────────────────────────────── let ss; try { ss = SpreadsheetApp.openById(sheetId); Logger.log('[CONFIG] ✅ Sheet abierto correctamente: ' + ss.getName()); } catch(e) { Logger.log('[CONFIG] ❌ ERROR: No se pudo abrir el Sheet'); Logger.log('[CONFIG] 🔍 Error: ' + e.message); throw new Error('No se pudo abrir Sheet ' + sheetId + ': ' + e.message); } // ── VALIDACIÓN 3: Obtener/crear hoja Config ─────────────────── let sheet; try { sheet = obtenerOCrearHoja(ss, CFG.SHEET_CONFIG, HDRS_CONFIG); Logger.log('[CONFIG] ✅ Hoja Config obtenida/creada correctamente'); } catch(e) { Logger.log('[CONFIG] ❌ ERROR: No se pudo obtener/crear hoja Config'); Logger.log('[CONFIG] 🔍 Error: ' + e.message); throw new Error('Error obteniendo hoja Config: ' + e.message); } // ── ANÁLISIS: Estructura actual de la hoja ──────────────────── const valores = sheet.getDataRange().getValues(); Logger.log('[CONFIG] 📊 Filas totales en Config: ' + valores.length); Logger.log('[CONFIG] 📊 Filas de datos (sin header): ' + (valores.length - 1)); if (valores.length > 0) { Logger.log('[CONFIG] 📋 Headers encontrados: ' + valores[0].join(', ')); } // Mapeo de propiedades existentes const mapeo = {}; for (let i = 1; i < valores.length; i++) { const prop = norm(valores[i][0]); if (prop) { mapeo[prop] = i + 1; Logger.log('[CONFIG] 🗺️ Fila ' + (i + 1) + ': ' + valores[i][0] + ' (norm: ' + prop + ')'); } } Logger.log('[CONFIG] 📊 Total propiedades mapeadas: ' + Object.keys(mapeo).length); // ── PROCESAMIENTO: Imágenes Base64 ──────────────────────────── Logger.log('[CONFIG] 🖼️ Verificando imágenes Base64...'); try { const carpeta = obtenerCarpetaCliente(sheetId); Logger.log('[CONFIG] 📁 Carpeta del cliente obtenida'); if (data.foto_perfilBase64) { Logger.log('[CONFIG] 📸 Procesando foto_perfil...'); const urlLogo = subirImagen(data.foto_perfilBase64, data.foto_perfilTipo, 'logo', carpeta); data.foto_perfil = urlLogo; Logger.log('[CONFIG] ✅ foto_perfil subida: ' + urlLogo.substring(0, 80) + '...'); } else { Logger.log('[CONFIG] ⏭️ No hay foto_perfilBase64 en payload'); } if (data.banner_portadaBase64) { Logger.log('[CONFIG] 🖼️ Procesando banner_portada...'); const urlBanner = subirImagen(data.banner_portadaBase64, data.banner_portadaTipo, 'banner', carpeta); data.banner_portada = urlBanner; Logger.log('[CONFIG] ✅ banner_portada subido: ' + urlBanner.substring(0, 80) + '...'); } else { Logger.log('[CONFIG] ⏭️ No hay banner_portadaBase64 en payload'); } } catch(e) { Logger.log('[CONFIG] ⚠️ Error procesando imágenes: ' + e.message); Logger.log('[CONFIG] 🔍 Stack: ' + e.stack); } // ── PROCESAMIENTO: Campos de configuración ──────────────────── Logger.log('[CONFIG] 📝 Iniciando guardado de campos...'); Logger.log('[CONFIG] 📋 Campos esperados (CAMPOS_CONFIG): ' + CAMPOS_CONFIG.length); const CAMPOS_NUMERICOS = ['columnasescritorio', 'columnasmovil']; let camposGuardados = 0; let camposOmitidos = 0; let camposCreados = 0; let camposActualizados = 0; let erroresGuardado = 0; CAMPOS_CONFIG.forEach(campo => { const valor = data[campo]; const campoNorm = norm(campo); // Debug: mostrar cada campo procesado Logger.log('[CONFIG] ─────────────────────────────────────────'); Logger.log('[CONFIG] 🔍 Procesando campo: ' + campo); Logger.log('[CONFIG] Normalizado: ' + campoNorm); Logger.log('[CONFIG] Valor recibido: ' + (valor !== undefined && valor !== null ? String(valor).substring(0, 100) : '(vacío)')); Logger.log('[CONFIG] Tipo: ' + typeof valor); if (valor === undefined || valor === null || valor === '') { Logger.log('[CONFIG] ⏭️ OMITIDO (valor vacío)'); camposOmitidos++; return; } let valorFinal; if (CAMPOS_NUMERICOS.includes(campoNorm)) { valorFinal = parseInt(valor) || 0; Logger.log('[CONFIG] 🔢 Campo numérico: ' + valorFinal); } else { valorFinal = String(valor); Logger.log('[CONFIG] 📝 Campo texto: ' + valorFinal.substring(0, 50) + (valorFinal.length > 50 ? '...' : '')); } try { if (mapeo[campoNorm]) { // ── Actualizar fila existente ────────────────────────── const fila = mapeo[campoNorm]; Logger.log('[CONFIG] 🔄 ACTUALIZANDO fila existente: ' + fila); const rango = sheet.getRange(fila, 2); rango.setValue(valorFinal); // Verificación: leer el valor guardado const valorGuardado = sheet.getRange(fila, 2).getValue(); if (String(valorGuardado) !== String(valorFinal)) { Logger.log('[CONFIG] ⚠️ ADVERTENCIA: Valor guardado difiere'); Logger.log('[CONFIG] Esperado: ' + valorFinal); Logger.log('[CONFIG] Guardado: ' + valorGuardado); } else { Logger.log('[CONFIG] ✅ Verificado: valor guardado correctamente'); } camposActualizados++; } else { // ── Crear nueva fila ─────────────────────────────────── const nuevaFila = sheet.getLastRow() + 1; Logger.log('[CONFIG] ➕ CREANDO nueva fila: ' + nuevaFila); sheet.getRange(nuevaFila, 1).setValue(campo); sheet.getRange(nuevaFila, 2).setValue(valorFinal); Logger.log('[CONFIG] ✅ Fila creada exitosamente'); camposCreados++; } camposGuardados++; } catch(e) { Logger.log('[CONFIG] ❌ ERROR guardando campo: ' + e.message); Logger.log('[CONFIG] 🔍 Stack: ' + e.stack); erroresGuardado++; } }); // ── RESUMEN FINAL ────────────────────────────────────────────── Logger.log('═══════════════════════════════════════════════════'); Logger.log('[CONFIG] 📊 RESUMEN DEL GUARDADO:'); Logger.log('[CONFIG] ✅ Campos guardados: ' + camposGuardados); Logger.log('[CONFIG] ├─ Actualizados: ' + camposActualizados); Logger.log('[CONFIG] └─ Creados: ' + camposCreados); Logger.log('[CONFIG] ⏭️ Campos omitidos (vacíos): ' + camposOmitidos); Logger.log('[CONFIG] ❌ Errores: ' + erroresGuardado); Logger.log('[CONFIG] ✅ GUARDADO COMPLETADO — sheet: ' + sheetId); Logger.log('═══════════════════════════════════════════════════'); return R({ ok: true, guardados: camposGuardados, actualizados: camposActualizados, creados: camposCreados, omitidos: camposOmitidos, errores: erroresGuardado }); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: CAMBIAR ESTADO CLIENTE (Activo / Suspendido) // ══════════════════════════════════════════════════════════════════════ function cambiarEstadoCliente(idKobia, nuevoEstado) { const ss = SpreadsheetApp.openById(CFG.ID_HOJA_MAESTRA); const sheet = ss.getSheetByName(CFG.SHEET_MASTER); if (!sheet) throw new Error('Hoja Clientes no encontrada'); const valores = sheet.getDataRange().getValues(); const rowIndex = valores.findIndex((row, i) => i > 0 && row[0] === idKobia); if (rowIndex < 0) throw new Error('Cliente no encontrado: ' + idKobia); sheet.getRange(rowIndex + 1, 7).setValue(nuevoEstado); Logger.log('[KOBIA] Estado cliente ' + idKobia + ' → ' + nuevoEstado); return R({ ok: true, idKobia, estado: nuevoEstado }); } // ══════════════════════════════════════════════════════════════════════ // UTILIDADES // ══════════════════════════════════════════════════════════════════════ function norm(s) { if (s === null || s === undefined) return ''; return s.toString().toLowerCase().trim() .replace(/[\s_\-]+/g, '') .replace(/[áàä]/g,'a').replace(/[éèë]/g,'e') .replace(/[íìï]/g,'i').replace(/[óòö]/g,'o') .replace(/[úùü]/g,'u').replace(/[ñ]/g,'n'); } function construirFila(hdrs, data, imageUrl, id, estado) { return hdrs.map(function(h) { switch (norm(h)) { case 'id': return id; case 'nombre': return (data.nombre || '').trim(); case 'descripcion': return (data.descripcion || '').trim(); case 'precio': return parseFloat(data.precio) || 0; case 'preciooferta': case 'precio_oferta': return parseFloat(data.precio_oferta) || ''; case 'categoria': return (data.categoria || '').trim(); case 'subcategoria': return (data.subcategoria || '').trim(); case 'imagen': return imageUrl || ''; case 'stock': return parseInt(data.stock) || 1; case 'destacado': return (data.destacado || '').trim(); case 'estado': return estado || 'activo'; case 'etiqueta': return (data.etiqueta || '').trim(); case 'variante': return (data.variante || '').trim(); default: return ''; } }); } function subirImagen(base64, mimeType, nombreBase, carpeta) { try { const tipo = mimeType || 'image/jpeg'; const ext = tipo.split('/')[1] || 'jpg'; const nombre = nombreBase + '_' + Date.now() + '.' + ext; const base64Puro = base64.indexOf('base64,') > -1 ? base64.split('base64,')[1] : base64; const blob = Utilities.newBlob(Utilities.base64Decode(base64Puro), tipo, nombre); const file = carpeta.createFile(blob); // El cambio de permisos a veces falla en cuentas G-Suite/Education, usamos try-catch try { file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); } catch (e) { Logger.log('[KOBIA] No se pudo cambiar permisos a "Público", pero el archivo se creó: ' + e.message); } const url = 'https://drive.google.com/thumbnail?id=' + file.getId() + '&sz=w1200'; Logger.log('[KOBIA] Imagen subida exitosamente: ' + url); return url; } catch (err) { Logger.log('[KOBIA] ERROR FATAL en subirImagen: ' + err.message); throw err; } } function obtenerCarpetaCliente(sheetId) { try { const archivo = DriveApp.getFileById(sheetId); const padres = archivo.getParents(); if (padres.hasNext()) { const carpetaCliente = padres.next(); // Buscar carpeta de imágenes que termine en -IMG const subCarpetas = carpetaCliente.getFolders(); while (subCarpetas.hasNext()) { const sub = subCarpetas.next(); if (sub.getName().endsWith('-IMG')) return sub; } // Si no existe aún, extraer el ID del cliente del nombre del Sheet // y crear carpeta con nomenclatura correcta YYMM-NNN-IMG const nombreSheet = archivo.getName(); // ej: 2603-001-MOD-VALENTINA const partes = nombreSheet.split('-'); const idCliente = partes.length >= 2 ? `${partes[0]}-${partes[1]}` : 'KOBIA'; return carpetaCliente.createFolder(`${idCliente}-IMG`); } } catch(e) { Logger.log('[KOBIA] No se pudo encontrar carpeta del cliente: ' + e.message); } // Fallback: carpeta Clientes dentro de la raíz fija try { const raiz = DriveApp.getFolderById(CFG.ID_CARPETA_RAIZ); const iterCl = raiz.getFoldersByName(CFG.CARPETA_CLIENTES); return iterCl.hasNext() ? iterCl.next() : raiz.createFolder(CFG.CARPETA_CLIENTES); } catch(e) { Logger.log('[KOBIA] Error en fallback de carpeta: ' + e.message); throw e; } } function obtenerOCrearCarpeta(nombre) { const iter = DriveApp.getFoldersByName(nombre); return iter.hasNext() ? iter.next() : DriveApp.createFolder(nombre); } function obtenerOCrearHoja(ss, nombre, encabezados) { let sheet = ss.getSheetByName(nombre); if (!sheet) { sheet = ss.insertSheet(nombre); sheet.getRange(1, 1, 1, encabezados.length).setValues([encabezados]); sheet.setFrozenRows(1); Logger.log('[KOBIA] Hoja creada: ' + nombre); } return sheet; } function R(obj) { return ContentService .createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: REGISTRAR LEAD PENDIENTE (ONBOARDING LANDING PAGE) // ══════════════════════════════════════════════════════════════════════ function registrarLeadPendiente(data) { Logger.log('═══════════════════════════════════════════════════'); Logger.log('👤 REGISTRANDO NUEVO LEAD (PRE-CLIENTE)'); Logger.log('═══════════════════════════════════════════════════'); Logger.log('📦 Datos recibidos desde landing:'); Logger.log(JSON.stringify(data, null, 2)); const masterSheetId = CFG.ID_HOJA_MAESTRA; const ss = SpreadsheetApp.openById(masterSheetId); const clientsSheet = ss.getSheetByName('Clientes'); // ═══ NORMALIZACIÓN DE CAMPOS ═══════════════════════════════════ // La landing puede enviar campos con diferentes nombres // Necesitamos ser flexibles y aceptar múltiples variaciones // CATEGORÍA: acepta "categoria", "category", "tipo", "supercategoria" let categoriaRaw = data.categoria || data.category || data.tipo || data.supercategoria || data.tipoNegocio || data.tipo_negocio || ''; // MAPEO: Convertir categorías de la landing a supercategorías KOBIA const MAPEO_CATEGORIAS = { // Moda 'moda y ropa': 'MOD', 'moda': 'MOD', 'ropa': 'MOD', 'fashion': 'MOD', // Gastronomía 'comida y restaurante': 'GAS', 'comida': 'GAS', 'restaurante': 'GAS', 'gastronomía': 'GAS', 'gastronomia': 'GAS', 'food': 'GAS', // Tecnología 'electrónica y tecnología': 'TEC', 'electronica y tecnologia': 'TEC', 'tecnología': 'TEC', 'tecnologia': 'TEC', 'electrónica': 'TEC', 'electronica': 'TEC', 'tech': 'TEC', // Salud y Belleza 'belleza y salud': 'SAL', 'belleza': 'SAL', 'salud': 'SAL', 'beauty': 'SAL', 'health': 'SAL', // Mercado 'mercado': 'MER', 'supermercado': 'MER', 'market': 'MER', // Hogar 'hogar': 'HOG', 'home': 'HOG', 'decoración': 'HOG', 'decoracion': 'HOG', // Servicios y Variado 'servicios': 'VAR', 'services': 'VAR', 'otro': 'VAR', 'otros': 'VAR', 'other': 'VAR', 'variado': 'VAR' }; // Normalizar y mapear const categoriaKey = categoriaRaw.toLowerCase().trim(); const categoria = MAPEO_CATEGORIAS[categoriaKey] || 'VAR'; // Default: VAR Logger.log('📋 Categoría recibida: "' + categoriaRaw + '" → Mapeada a: "' + categoria + '"'); // NOMBRE MARCA: acepta "nombreMarca", "nombre", "marca", "name" const nombreMarca = data.nombreMarca || data.nombre_marca || data.nombre || data.marca || data.name || data.business_name || ''; Logger.log('🏷️ Nombre marca extraído: ' + (nombreMarca || '(vacío)')); // WHATSAPP: acepta "whatsapp", "telefono", "phone", "celular" const whatsapp = (data.whatsapp || data.whatsappId || data.whatsapp_id || data.telefono || data.phone || data.celular || data.mobile || '').replace(/[^0-9]/g, ''); // Limpia todo lo que no sea número Logger.log('📱 WhatsApp extraído: ' + (whatsapp || '(vacío)')); // EMAIL: acepta "email", "correo", "mail" const email = data.email || data.correo || data.mail || ''; Logger.log('📧 Email extraído: ' + (email || '(vacío)')); // PLAN: acepta "plan", "paquete", "package" const plan = data.plan || data.paquete || data.package || 'base'; Logger.log('📦 Plan extraído: ' + plan); // TIPO PLANTILLA: 'catalogo' o 'portafolio' const tipoPlantilla = (data.tipo_plantilla || data.tipoPlantilla || 'catalogo').toLowerCase().trim(); Logger.log('🎨 Tipo plantilla: ' + tipoPlantilla); // ═══ VALIDACIONES ═══════════════════════════════════════════════ const errores = []; if (!nombreMarca) { errores.push('❌ Nombre de marca vacío'); } if (!email || !email.includes('@')) { errores.push('❌ Email inválido o vacío: ' + email); } if (!whatsapp || whatsapp.length < 10) { errores.push('⚠️ WhatsApp vacío o incompleto: ' + whatsapp); } if (!categoria) { errores.push('⚠️ Categoría vacía'); } if (errores.length > 0) { Logger.log('⚠️ ADVERTENCIAS EN DATOS:'); errores.forEach(e => Logger.log(' ' + e)); } // Asignar ID temporal Lead const idKobiaTemp = 'LEAD-' + Date.now().toString().slice(-4); const fechaHoy = new Date(); const formatDate = (date) => { const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); return `${day}/${month}/${year}`; }; // ═══ CONSTRUCCIÓN DE FILA ═══════════════════════════════════════ const newRow = [ idKobiaTemp, // A — ID_KOBIA (temporal) categoria, // B — CATEGORIA nombreMarca, // C — NOMBRE_MARCA whatsapp, // D — WHATSAPP_ID email, // E — EMAIL '', // F — ID_SHEET_CLIENTE (vacío hasta convertir) 'Pendiente', // G — ESTADO plan, // H — PLAN formatDate(fechaHoy), // I — FECHA_INICIO 1, // J — MESES '', // K — FECHA_FIN (vacío hasta convertir) '', // L — SLUG (vacío hasta convertir) tipoPlantilla // M — TIPO_PLANTILLA ]; Logger.log('📊 Fila a insertar:'); Logger.log(' A (ID): ' + newRow[0]); Logger.log(' B (Categoría): ' + newRow[1]); Logger.log(' C (Nombre Marca): ' + newRow[2]); Logger.log(' D (WhatsApp): ' + newRow[3]); Logger.log(' E (Email): ' + newRow[4]); Logger.log(' G (Estado): ' + newRow[6]); // ═══ INSERTAR EN HOJA ═══════════════════════════════════════════ try { clientsSheet.appendRow(newRow); Logger.log('✅ Lead registrado exitosamente en hoja Clientes'); } catch(e) { Logger.log('❌ ERROR insertando lead: ' + e.message); throw e; } // ═══ ENVIAR EMAIL DE VALIDACIÓN ═════════════════════════════════ if (email && email.includes('@')) { Logger.log('📧 Enviando email de validación...'); try { sendLeadValidationEmail(email, nombreMarca || 'tu negocio'); Logger.log('✅ Email enviado'); } catch(e) { Logger.log('⚠️ Error enviando email: ' + e.message); } } Logger.log('═══════════════════════════════════════════════════'); Logger.log('✅ PRE-CLIENTE REGISTRADO COMO PENDIENTE'); Logger.log(' ID: ' + idKobiaTemp); Logger.log(' Nombre: ' + nombreMarca); Logger.log(' WhatsApp: ' + whatsapp); Logger.log(' Email: ' + email); Logger.log(' Categoría: ' + categoria); Logger.log('═══════════════════════════════════════════════════'); return R({ ok: true, mensaje: 'Pre-cliente registrado con estado Pendiente.', idTemp: idKobiaTemp, datos: { nombreMarca, whatsapp, email, categoria, tipoPlantilla } }); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: LOGIN DE CLIENTES EN PANEL // ══════════════════════════════════════════════════════════════════════ function loginPanel(whatsapp, pin, sheetIdAutorizado) { const wsMaster = SpreadsheetApp.openById(CFG.ID_HOJA_MAESTRA); const hojaClientes = wsMaster.getSheetByName(CFG.SHEET_MASTER); const data = hojaClientes.getDataRange().getValues(); const headers = data[0].map(h => String(h).toLowerCase().replace(/[\s_\-]+/g, '')); const targetWhatsapp = String(whatsapp).replace(/[^0-9]/g, '').slice(-10); const targetPin = String(pin).trim(); let nombreMarca = 'Mi Tienda'; let estado = 'inactivo'; let scriptUrl = ''; let slug = ''; const wIdx = headers.findIndex(h => h === 'whatsappid' || h === 'whatsapp'); const nIdx = headers.findIndex(h => h === 'nombremarca' || h === 'marca' || h === 'nombre'); const eIdx = headers.findIndex(h => h === 'estado' || h === 'status'); const slIdx = headers.findIndex(h => h === 'slug'); for (let i = 1; i < data.length; i++) { let dbWhatsapp = String(data[i][wIdx]).replace(/[^0-9]/g, '').slice(-10); let dbPin = String(data[i][0]).trim(); if (dbWhatsapp === targetWhatsapp && dbPin === targetPin) { if (nIdx >= 0) nombreMarca = data[i][nIdx]; if (eIdx >= 0) estado = data[i][eIdx]; if (slIdx >= 0) slug = data[i][slIdx]; break; } } const estadoVal = String(estado).toLowerCase().trim(); if (!['si','sí','yes','1','true','activo'].includes(estadoVal)) { return R({ ok: false, error: 'Tu servicio está suspendido. Contacta a KOBIA.' }); } return R({ ok: true, sheetId: sheetIdAutorizado, nombreMarca: nombreMarca, slug: slug, scriptUrl: scriptUrl || 'https://script.google.com/macros/s/AKfycbxQVl-5dj2_WRRw8J-Yj76isaYYS-ObaXYBGESBglPgwyW0LchzyTgzzfJJP__KMlti0g/exec' }); } // ══════════════════════════════════════════════════════════════════════ // ACCIÓN: LISTAR CLIENTES (SOLO PARA PANEL MASTER CON AUTH) // ══════════════════════════════════════════════════════════════════════ function listarClientes(data) { Logger.log('═══════════════════════════════════════════════════'); Logger.log('[KOBIA] 🔐 LISTAR CLIENTES - Autenticación Admin'); Logger.log('═══════════════════════════════════════════════════'); // ═══ VALIDAR CREDENCIALES DE ADMINISTRADOR ═════════════════════ const adminUser = data.adminUser || data.user || ''; const adminPass = data.adminPass || data.pass || ''; Logger.log('[ADMIN] Usuario recibido: ' + (adminUser || '(vacío)')); Logger.log('[ADMIN] Password recibido: ' + (adminPass ? '***' : '(vacío)')); if (!adminUser || !adminPass) { Logger.log('[ADMIN] ❌ ERROR: Credenciales vacías'); return R({ ok: false, error: 'Se requieren credenciales de administrador', needsAuth: true }); } if (adminUser !== CFG.ADMIN_USER || adminPass !== CFG.ADMIN_PASS) { Logger.log('[ADMIN] ❌ ERROR: Credenciales incorrectas'); Logger.log('[ADMIN] Esperado user: ' + CFG.ADMIN_USER); Logger.log('[ADMIN] Recibido user: ' + adminUser); return R({ ok: false, error: 'Credenciales de administrador incorrectas', needsAuth: true }); } Logger.log('[ADMIN] ✅ Autenticación exitosa'); // ═══ LEER HOJA CLIENTES (PRIVADA) ══════════════════════════════ try { const ss = SpreadsheetApp.openById(CFG.ID_HOJA_MAESTRA); const sheet = ss.getSheetByName(CFG.SHEET_MASTER); if (!sheet) { Logger.log('[ADMIN] ❌ ERROR: Hoja Clientes no encontrada'); return R({ ok: false, error: 'Hoja Clientes no encontrada' }); } Logger.log('[ADMIN] ✅ Hoja Clientes abierta'); // Leer todos los datos const valores = sheet.getDataRange().getValues(); if (valores.length < 2) { Logger.log('[ADMIN] ⚠️ Hoja vacía, solo headers'); return R({ ok: true, clientes: [] }); } // Headers normalizados const headers = valores[0].map(h => String(h).toLowerCase().trim().replace(/[\s_\-]+/g, '') ); Logger.log('[ADMIN] 📋 Headers: ' + headers.join(', ')); // Convertir filas a objetos const clientes = []; for (let i = 1; i < valores.length; i++) { const row = valores[i]; const cliente = {}; headers.forEach((h, idx) => { cliente[h] = row[idx] || ''; }); // Solo agregar si tiene ID_KOBIA if (cliente.idkobia || cliente.id) { clientes.push(cliente); } } Logger.log('[ADMIN] ✅ Clientes procesados: ' + clientes.length); Logger.log('═══════════════════════════════════════════════════'); return R({ ok: true, clientes: clientes, total: clientes.length }); } catch(e) { Logger.log('[ADMIN] ❌ ERROR leyendo hoja: ' + e.message); Logger.log('[ADMIN] 🔍 Stack: ' + e.stack); return R({ ok: false, error: 'Error al leer datos: ' + e.message }); } } // ══════════════════════════════════════════════════════════════════════ // FUNCIÓN DE PRUEBA: EJECUTA DESDE EL EDITOR DE APPS SCRIPT // ══════════════════════════════════════════════════════════════════════ function testKobiaEmail() { const testEmail = "soporte.kobia.com.co@gmail.com"; // O tu correo personal para probar Logger.log("📧 Iniciando envío de prueba a: " + testEmail); try { sendLeadValidationEmail(testEmail, "Tienda de Prueba KOBIA"); Logger.log("✅ Prueba completada. Revisa tu bandeja de entrada y spam."); } catch (error) { Logger.log("❌ Error en prueba: " + error); } }