========================================================================== RL TRUCK SALES - GUÍA DE INTEGRACIÓN DE GOOGLE SHEETS & DRIVE ========================================================================== Sigue estos sencillos pasos para conectar tu panel de administración y el catálogo de tu sitio web a una Hoja de Cálculo de Google (como Base de Datos) y a una carpeta de Google Drive (para almacenar las imágenes de forma pública y automática). -------------------------------------------------------------------------- PASO 1: CREAR LA HOJA DE CÁLCULO -------------------------------------------------------------------------- 1. Entra a tu cuenta de Google Drive o Google Sheets. 2. Crea una nueva Hoja de Cálculo de Google y nómbrala: "Base de Datos RL TRUCK SALES". 3. Por defecto, verás una pestaña llamada "Hoja 1". No necesitas cambiarle de nombre ni escribir las columnas manualmente; el nuevo código de Apps Script la inicializará automáticamente con el nombre de "equipos" y todas sus cabeceras. 4. El script también creará automáticamente una segunda pestaña llamada "usuarios" e insertará una cuenta de administración por defecto con los siguientes datos: - Usuario: admin - Contraseña: admin123 - Rol: Administrador (Puedes cambiar esta contraseña o dar de alta/baja cuentas directamente desde el nuevo panel de administración web). -------------------------------------------------------------------------- PASO 2: ABRIR E INSTALAR GOOGLE APPS SCRIPT -------------------------------------------------------------------------- 1. Dentro de tu Hoja de Cálculo, haz clic en el menú superior: "Extensiones" -> "Apps Script". 2. Borra todo el código que aparezca en el editor. 3. Copia y pega exactamente todo el código de programación que se incluye abajo en el apartado "CÓDIGO DE APPS SCRIPT". 4. Haz clic en el icono del disco para Guardar el proyecto. -------------------------------------------------------------------------- PASO 3: IMPLEMENTAR (DESPLEGAR) COMO APLICACIÓN WEB -------------------------------------------------------------------------- 1. En la parte superior derecha de Apps Script, haz clic en el botón azul: "Implementar" -> "Nueva implementación". 2. En la ventana emergente, haz clic en el icono del engranaje junto a "Seleccionar tipo" y elige "Aplicación web". 3. Rellena los datos de la siguiente manera: - Descripción: "Base de datos RL TRUCK SALES API V2" - Ejecutar como: "Yo (tu_correo@gmail.com)" - Quién tiene acceso: "Cualquiera" (Debe ser "Cualquiera" para que el login y catálogo funcionen sin requerir iniciar sesión de Google). 4. Haz clic en el botón azul "Implementar". 5. Google te pedirá "Autorizar acceso". Haz clic en él, selecciona tu cuenta de Google, ve a "Configuración avanzada" (abajo) y haz clic en "Ir a Proyecto sin título (no seguro)" para otorgar los permisos necesarios para crear imágenes en tu Google Drive y escribir en tu Google Sheet. 6. Copia la URL de la aplicación web generada (debe parecerse a esta: https://script.google.com/macros/s/XXXXXX/exec). 7. Haz clic en "Listo". -------------------------------------------------------------------------- PASO 4: PEGAR LA URL EN TU CÓDIGO WEB -------------------------------------------------------------------------- 1. Abre el archivo de Javascript del proyecto: "js/main.js". 2. En la línea 4, encontrarás la variable: window.SCRIPT_URL = "TU_URL_DE_GOOGLE_APPS_SCRIPT_AQUI"; 3. Reemplaza el texto de ejemplo con la URL real de tu aplicación web copiada en el paso anterior. 4. Guarda el archivo y ¡listo! Ya puedes acceder con el usuario "admin" y contraseña "admin123" desde tu panel de administración. ========================================================================== CÓDIGO DE APPS SCRIPT (PARA PEGAR EN GOOGLE EXTENSIONES) ========================================================================== function getOrCreateSheets() { var ss = SpreadsheetApp.getActiveSpreadsheet(); // 1. Pestaña de Equipos (Catálogo) var sheetEquipos = ss.getSheetByName("equipos"); if (!sheetEquipos) { var sheets = ss.getSheets(); if (sheets.length > 0 && sheets[0].getName() === "Hoja 1") { sheetEquipos = sheets[0]; sheetEquipos.setName("equipos"); } else { sheetEquipos = ss.insertSheet("equipos"); } } if (sheetEquipos.getLastRow() === 0) { var headers = ["category", "brand", "title", "price", "year", "km", "status", "engine", "transmission", "drive", "capacity", "photos", "photo360", "desc"]; sheetEquipos.appendRow(headers); } // 2. Pestaña de Usuarios (Admin Accounts) var sheetUsuarios = ss.getSheetByName("usuarios"); if (!sheetUsuarios) { sheetUsuarios = ss.insertSheet("usuarios"); var userHeaders = ["username", "password", "role", "date_created"]; sheetUsuarios.appendRow(userHeaders); // Insertar cuenta de admin por defecto sheetUsuarios.appendRow(["admin", "admin123", "Administrador", new Date().toLocaleDateString()]); } return { equipos: sheetEquipos, usuarios: sheetUsuarios }; } function doGet(e) { var sheets = getOrCreateSheets(); var sheet = sheets.equipos; var data = sheet.getDataRange().getValues(); var items = []; if (data.length > 1) { var headers = data[0]; for (var i = 1; i < data.length; i++) { var row = data[i]; var item = {}; for (var j = 0; j < headers.length; j++) { item[headers[j]] = row[j]; } items.push(item); } } return ContentService.createTextOutput(JSON.stringify(items)) .setMimeType(ContentService.MimeType.JSON); } function doPost(e) { try { var params = JSON.parse(e.postData.contents); var sheets = getOrCreateSheets(); // --- ACCIÓN: LOGIN --- if (params.action === "login") { var username = params.username ? params.username.trim() : ""; var password = params.password ? params.password.trim() : ""; var usersData = sheets.usuarios.getDataRange().getValues(); for (var i = 1; i < usersData.length; i++) { if (usersData[i][0] === username && usersData[i][1] === password) { return ContentService.createTextOutput(JSON.stringify({ status: "success", user: { username: username, role: usersData[i][2] } })).setMimeType(ContentService.MimeType.JSON); } } return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Usuario o contraseña incorrectos." })).setMimeType(ContentService.MimeType.JSON); } // --- ACCIÓN: OBTENER USUARIOS --- if (params.action === "get_users") { var authUser = params.authUser; var authPass = params.authPass; var usersData = sheets.usuarios.getDataRange().getValues(); var authorized = false; for (var i = 1; i < usersData.length; i++) { if (usersData[i][0] === authUser && usersData[i][1] === authPass) { authorized = true; break; } } if (!authorized) { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "No autorizado." })) .setMimeType(ContentService.MimeType.JSON); } var userList = []; for (var i = 1; i < usersData.length; i++) { userList.push({ username: usersData[i][0], role: usersData[i][2], date_created: usersData[i][3] }); } return ContentService.createTextOutput(JSON.stringify(userList)) .setMimeType(ContentService.MimeType.JSON); } // --- ACCIÓN: REGISTRAR NUEVO USUARIO --- if (params.action === "add_user") { var authUser = params.authUser; var authPass = params.authPass; var usersData = sheets.usuarios.getDataRange().getValues(); var authorized = false; for (var i = 1; i < usersData.length; i++) { if (usersData[i][0] === authUser && usersData[i][1] === authPass) { authorized = true; break; } } if (!authorized) { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "No autorizado." })) .setMimeType(ContentService.MimeType.JSON); } var newUsername = params.newUsername ? params.newUsername.trim() : ""; var newPassword = params.newPassword ? params.newPassword.trim() : ""; var newRole = params.newRole || "Supervisor"; if (!newUsername || !newPassword) { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Datos de usuario inválidos." })) .setMimeType(ContentService.MimeType.JSON); } for (var i = 1; i < usersData.length; i++) { if (usersData[i][0].toLowerCase() === newUsername.toLowerCase()) { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "El usuario ya existe." })) .setMimeType(ContentService.MimeType.JSON); } } sheets.usuarios.appendRow([newUsername, newPassword, newRole, new Date().toLocaleDateString()]); return ContentService.createTextOutput(JSON.stringify({ status: "success" })) .setMimeType(ContentService.MimeType.JSON); } // --- ACCIÓN: ELIMINAR USUARIO --- if (params.action === "delete_user") { var authUser = params.authUser; var authPass = params.authPass; var usersData = sheets.usuarios.getDataRange().getValues(); var authorized = false; for (var i = 1; i < usersData.length; i++) { if (usersData[i][0] === authUser && usersData[i][1] === authPass) { authorized = true; break; } } if (!authorized) { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "No autorizado." })) .setMimeType(ContentService.MimeType.JSON); } var userToDelete = params.username; if (userToDelete === "admin") { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "No puedes eliminar el usuario administrador principal." })) .setMimeType(ContentService.MimeType.JSON); } var deleted = false; for (var i = usersData.length - 1; i >= 1; i--) { if (usersData[i][0] === userToDelete) { sheets.usuarios.deleteRow(i + 1); deleted = true; } } return ContentService.createTextOutput(JSON.stringify({ status: "success", deleted: deleted })) .setMimeType(ContentService.MimeType.JSON); } // --- ACCIÓN: ELIMINAR EQUIPO (MAQUINARIA) --- if (params.action === "delete") { var titleToDelete = params.title; var data = sheets.equipos.getDataRange().getValues(); var deleted = false; for (var i = data.length - 1; i >= 1; i--) { if (data[i][2] === titleToDelete) { sheets.equipos.deleteRow(i + 1); deleted = true; } } return ContentService.createTextOutput(JSON.stringify({ status: "success", deleted: deleted })) .setMimeType(ContentService.MimeType.JSON); } // --- ACCIÓN POR DEFECTO: AGREGAR EQUIPO --- var folderName = "LR_Truck_Photos"; var folders = DriveApp.getFoldersByName(folderName); var folder; if (folders.hasNext()) { folder = folders.next(); } else { folder = DriveApp.createFolder(folderName); folder.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); } function uploadBase64(base64Str, prefix) { if (!base64Str) return ""; var matches = base64Str.match(/^data:(image\/[a-z]+);base64,(.+)$/); if (!matches || matches.length < 3) return ""; var mimeType = matches[1]; var rawData = matches[2]; var decoded = Utilities.base64Decode(rawData); var fileExtension = mimeType.split('/')[1]; var blob = Utilities.newBlob(decoded, mimeType, prefix + "_" + new Date().getTime() + "." + fileExtension); var file = folder.createFile(blob); file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); return "https://lh3.googleusercontent.com/d/" + file.getId(); } var photoUrls = []; if (params.photos) { var base64Photos = []; if (Array.isArray(params.photos)) { base64Photos = params.photos; } else if (typeof params.photos === 'string') { if (params.photos.indexOf('|') !== -1) { base64Photos = params.photos.split('|'); } else { var parts = params.photos.split(','); for (var k = 0; k < parts.length; k++) { if (parts[k].indexOf('data:') === 0 && k + 1 < parts.length) { base64Photos.push(parts[k] + ',' + parts[k+1]); k++; } else { base64Photos.push(parts[k]); } } } } for (var i = 0; i < base64Photos.length; i++) { var url = uploadBase64(base64Photos[i], "img"); if (url) photoUrls.push(url); } } var photo360Url = ""; if (params.photo360) { photo360Url = uploadBase64(params.photo360, "360"); } var headers = ["category", "brand", "title", "price", "year", "km", "status", "engine", "transmission", "drive", "capacity", "photos", "photo360", "desc"]; var rowData = []; for (var j = 0; j < headers.length; j++) { var key = headers[j]; if (key === "photos") { rowData.push(photoUrls.join(',')); } else if (key === "photo360") { rowData.push(photo360Url); } else { rowData.push(params[key] || ""); } } sheets.equipos.appendRow(rowData); return ContentService.createTextOutput(JSON.stringify({ status: "success" })) .setMimeType(ContentService.MimeType.JSON); } catch (err) { return ContentService.createTextOutput(JSON.stringify({ status: "error", message: err.toString() })) .setMimeType(ContentService.MimeType.JSON); } }