X3D_Web / code.gs
Edoruin's picture
Initial clean commit with LFS
c2aab7f
// Script para Google Sheets con visor 3D interactivo y gesti贸n de productos/pedidos
// Hojas: "Productos" y "Pedidos"
// Variables globales
var productosSheet = "Productos";
var pedidosSheet = "Pedidos";
var productosData = [];
var stlFiles = {};
var currentColor = "#000000";
var currentModel = null;
// Inicializar al abrir el documento
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('3D Viewer')
.addItem('Agregar al carrito', 'addToCart')
.addSeparator()
.addItem('Limpiar carrito', 'clearCart')
.addToUi();
// Cargar datos de productos
loadProducts();
// Inicializar visor 3D
init3DViewer();
}
// Cargar productos desde la hoja de c谩lculo
function loadProducts() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(productosSheet);
if (!sheet) return;
var data = sheet.getDataRange().getValues();
productosData = [];
for (var i = 1; i < data.length; i++) {
if (data[i][0] && data[i][1]) {
productosData.push({
nombre: data[i][0],
precio: data[i][1],
vida: data[i][2],
inventario: data[i][3],
id: data[i][4]
});
}
}
Logger.log('Productos cargados: ' + productosData.length);
}
// Inicializar visor 3D
function init3DViewer() {
// Crear HTML para el visor
var html = HtmlService.createHtmlOutputFromFile('viewer')
.setWidth(400)
.setHeight(400);
SpreadsheetApp.getUi().showModelessDialog(html, 'Visor 3D');
}
// Funci贸n para agregar al carrito
function addToCart(productName, quantity, client) {
// Buscar el producto
var product = productosData.find(p =>
p.nombre.toLowerCase() === productName.toLowerCase()
);
if (!product) {
SpreadsheetApp.getUi().alert('Producto no encontrado: ' + productName);
return;
}
if (product.inventario <= 0) {
SpreadsheetApp.getUi().alert('Producto sin inventario: ' + productName);
return;
}
// Agregar a la hoja de pedidos
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(pedidosSheet);
if (!sheet) {
SpreadsheetApp.getUi().alert('Hoja de pedidos no encontrada');
return;
}
sheet.appendRow([
product.nombre,
quantity,
client,
product.inventario,
product.id
]);
// Actualizar inventario
updateInventory(product.nombre, -quantity);
SpreadsheetApp.getUi().alert('Producto agregado al carrito: ' + product.nombre);
}
// Actualizar inventario
function updateInventory(productName, quantity) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(productosSheet);
if (!sheet) return;
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] && data[i][0].toLowerCase() === productName.toLowerCase()) {
var currentInv = parseInt(data[i][3]) || 0;
var newInv = currentInv + quantity;
sheet.getRange(i+1, 4).setValue(newInv);
break;
}
}
}
// Limpiar carrito (eliminar 煤ltimas filas de pedidos)
function clearCart() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(pedidosSheet);
if (!sheet) return;
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
sheet.deleteRows(2, lastRow - 1);
SpreadsheetApp.getUi().alert('Carrito limpiado');
}
}
// Cargar STL files (simulado - en producci贸n conectar铆a con Google Drive o base de datos)
function loadSTLFiles() {
// Esta funci贸n simula la carga de archivos STL
// En producci贸n, conectar铆a con Google Drive API o base de datos
stlFiles = {
'greca': 'data/stl/greca.stl',
'minimalista': 'data/stl/minimalista.stl',
'cl谩sico': 'data/stl/cl谩sico.stl',
'moderno': 'data/stl/moderno.stl'
};
// Mapeo de ID de producto a nombre de archivo STL
// En producci贸n, esto deber铆a venir de una tabla de base de datos o configuraci贸n
stlIdMapping = {
// ID: nombre_archivo
1: 'greca',
2: 'mapa_rd',
3: 'arete_greca',
4: 'arete_greca',
5: 'arete_mapa_rd',
6: 'arete_mapa_rd',
7: 'arete_bandera_rd',
8: 'arete_bandera_rd',
9: 'llavero_greca',
10: 'llavero_mapa_rd',
11: 'figura_catalogo',
12: 'llavero_bandera_rd',
13: 'llavero_bandera_rd',
14: 'cortador_game',
15: 'poley_grande_steam',
16: 'poley_peq_steam',
17: 'love_jesus',
18: 'posa_vasos',
19: 'keychain_mom',
20: 'llavero_tambor',
21: 'arete_tambor',
22: 'gato_flexible',
23: 'gancho_llaves',
24: 'gancho_llaves'
};
// Mapeo de nombre de archivo STL a nombre de visualizaci贸n (basado en tu lista de Sheets)
stlFilenameToDisplayName = {
'greca': 'Llavero estilo Greca Grab.stl',
'mapa_rd': 'Llavero estilo Mapa RD Grab.stl',
'arete_greca': 'Arete estilo Greca peq.stl',
'arete_mapa_rd': 'Arete estilo Mapa RD.stl',
'arete_bandera_rd': 'Arete estilo Bandera RD peq.stl',
'llavero_bandera_rd': 'Llavero estilo Bandera RD Grab.stl',
'cortador_game': 'CortadorGalletasGameControl.stl',
'poley_grande_steam': 'Poleygrande STEAM.stl',
'poley_peq_steam': 'Poleypeq STEAM.stl',
'love_jesus': 'Love=Jesus.stl',
'posa_vasos': 'Posa Vasos.stl',
'keychain_mom': 'KeychainMom.stl',
'llavero_tambor': 'llavero estilo tambor.STL',
'arete_tambor': 'Arete estilo tambor.stl',
'gato_flexible': 'Gato+flexible.stl',
'gancho_llaves': 'Gancho para llaves sweet home.STL'
};
// Mapeo de nombre de visualizaci贸n a nombre de archivo STL
displayNameToStlFilename = {
'Llavero estilo Greca Grab.stl': 'greca',
'Llavero estilo Mapa RD Grab.stl': 'mapa_rd',
'Arete estilo Greca peq.stl': 'arete_greca',
'Arete estilo Mapa RD.stl': 'arete_mapa_rd',
'Arete estilo Bandera RD peq.stl': 'arete_bandera_rd',
'Llavero estilo Bandera RD Grab.stl': 'llavero_bandera_rd',
'CortadorGalletasGameControl.stl': 'cortador_game',
'Poleygrande STEAM.stl': 'poley_grande_steam',
'Poleypeq STEAM.stl': 'poley_peq_steam',
'Love=Jesus.stl': 'love_jesus',
'Posa Vasos.stl': 'posa_vasos',
'KeychainMom.stl': 'keychain_mom',
'llavero estilo tambor.STL': 'llavero_tambor',
'Arete estilo tambor.stl': 'arete_tambor',
'Gato+flexible.stl': 'gato_flexible',
'Gancho para llaves sweet home.STL': 'gancho_llaves'
};
// Mapeo de nombre base (sin variaciones) a nombre de archivo STL
baseNameToStlFilename = {
'llavero estilo greca': 'llavero_greca',
'llavero estilo mapa rd': 'llavero_mapa_rd',
'arete estilo greca': 'arete_greca',
'arete estilo mapa rd': 'arete_mapa_rd',
'arete estilo bandera rd': 'arete_bandera_rd',
'llavero estilo bandera rd': 'llavero_bandera_rd',
'cortador de galletas game': 'cortador_game',
'poleygrande steam': 'poley_grande_steam',
'poleypeq steam': 'poley_peq_steam',
'llavero love=jesus': 'love_jesus',
'posa vasos': 'posa_vasos',
'keychainmom': 'keychain_mom',
'llavero estilo tambor': 'llavero_tambor',
'arete estilo tambor': 'arete_tambor',
'gato flexible': 'gato_flexible',
'gancho para llaves': 'gancho_llaves'
};
Logger.log('STL files cargados: ' + Object.keys(stlFiles).length);
}
// Cambiar color del modelo 3D
function changeColor(color) {
currentColor = color;
// Enviar mensaje al visor para actualizar color
var app = UiApp.getActiveApplication();
if (app) {
app.getElementById('modelColor').setStyleAttribute('color', color);
}
SpreadsheetApp.getUi().showModelessDialog(
HtmlService.createHtmlOutput(
'<div id="modelColor" style="color:' + color + '">Color actualizado</div>'
).setWidth(200).setHeight(100),
'Color'
);
}
// Cargar modelo espec铆fico
function loadModel(modelName) {
currentModel = modelName;
// En producci贸n, cargar铆a el STL correspondiente
Logger.log('Cargando modelo: ' + modelName);
}
// Funci贸n para el visor HTML (contenido del archivo viewer.html)
function getViewerHTML() {
return `
<!DOCTYPE html>
<html>
<head>
<title>Visor 3D Interactivo</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body {
margin: 0;
padding: 10px;
background: #f0f0f0;
font-family: Arial, sans-serif;
}
#viewer {
width: 100%;
height: 300px;
border: 1px solid #ccc;
background: #ffffff;
}
.color-picker {
display: flex;
gap: 5px;
margin-top: 10px;
}
.color-btn {
width: 30px;
height: 30px;
border: none;
border-radius: 50%;
cursor: pointer;
}
</style>
</head>
<body>
<h3>Visor 3D - Selecciona un modelo</h3>
<div id="viewer"></div>
<div class="color-picker">
<button class="color-btn" style="background: #000000" onclick="selectColor('#000000')"></button>
<button class="color-btn" style="background: #ff0000" onclick="selectColor('#ff0000')"></button>
<button class="color-btn" style="background: #00ff00" onclick="selectColor('#00ff00')"></button>
<button class="color-btn" style="background: #0000ff" onclick="selectColor('#0000ff')"></button>
<button class="color-btn" style="background: #ffff00" onclick="selectColor('#ffff00')"></button>
<button class="color-btn" style="background: #ff00ff" onclick="selectColor('#ff00ff')"></button>
<button class="color-btn" style="background: #00ffff" onclick="selectColor('#00ffff')"></button>
</div>
<script>
let scene, camera, renderer, model;
let currentColor = '#000000';
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
camera.position.z = 5;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(380, 280);
document.getElementById('viewer').appendChild(renderer.domElement);
// Crear grid
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);
animate();
}
function animate() {
requestAnimationFrame(animate);
if (model) {
model.rotation.y += 0.01;
}
renderer.render(scene, camera);
}
function selectColor(color) {
currentColor = color;
if (model) {
model.traverse((child) => {
if (child.isMesh) {
child.material.color.set(color);
}
});
}
google.script.run.withSuccessHandler(function() {}).changeColor(color);
}
function loadModel(modelName) {
// Simular carga de modelo
if (model) {
scene.remove(model);
}
// Crear modelo simple para demostraci贸n
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshPhongMaterial({
color: currentColor,
shininess: 100
});
model = new THREE.Mesh(geometry, material);
scene.add(model);
// A帽adir luz
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(10, 10, 10);
scene.add(pointLight);
google.script.run.withSuccessHandler(function() {}).loadModel(modelName);
}
// Inicializar al cargar
window.onload = function() {
init();
// Cargar un modelo por defecto
loadModel('greca');
};
</script>
</body>
</html>
`;
}
// Funci贸n para obtener HTML del visor (llamada desde Google Sheets)
function showViewer() {
var html = HtmlService.createHtmlOutput(getViewerHTML())
.setWidth(420)
.setHeight(450);
SpreadsheetApp.getUi().showModelessDialog(html, 'Visor 3D');
}
// Manejar peticiones GET desde el formulario web
function doGet(e) {
var action = e.parameter.action;
if (action === 'products') {
return getProducts();
}
if (action === 'order') {
return saveOrder(e);
}
return ContentService.createTextOutput(JSON.stringify({ error: 'Acci贸n no vlida' }))
.setMimeType(ContentService.MimeType.JSON);
}
// Obtener productos para el visor web
function getProducts() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(productosSheet);
if (!sheet) {
return ContentService.createTextOutput(JSON.stringify({ values: [] }))
.setMimeType(ContentService.MimeType.JSON);
}
var data = sheet.getDataRange().getValues();
var result = [];
// Encabezados
result.push(['id_producto', 'Productos', 'Precio_Unitario_con_costos', 'Precio unitario', 'vida promedio', 'INVENTARIO']);
// Datos
for (var i = 1; i < data.length; i++) {
result.push([
data[i][0], // id_producto
data[i][1], // Productos
data[i][2], // Precio_Unitario_con_costos
data[i][3], // Precio unitario
data[i][4], // vida promedio
data[i][5] // INVENTARIO
]);
}
return ContentService.createTextOutput(JSON.stringify({ values: result }))
.setMimeType(ContentService.MimeType.JSON);
}
// Guardar pedido desde el formulario web
function saveOrder(e) {
var product = e.parameter.product;
var quantity = parseInt(e.parameter.quantity);
var client = e.parameter.client;
var phone = e.parameter.phone;
var latitude = e.parameter.latitude || '';
var longitude = e.parameter.longitude || '';
var fecha = e.parameter.fecha || new Date().toISOString();
if (!product || !quantity || !client) {
return ContentService.createTextOutput(JSON.stringify({ error: 'Faltan datos requeridos' }))
.setMimeType(ContentService.MimeType.JSON);
}
try {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(pedidosSheet);
if (!sheet) {
return ContentService.createTextOutput(JSON.stringify({ error: 'Hoja de pedidos no encontrada' }))
.setMimeType(ContentService.MimeType.JSON);
}
// Buscar el producto para obtener el ID
var productosSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Productos');
var productosData = productosSheet.getDataRange().getValues();
var productId = '';
var productPrice = 0;
// Buscar por ID primero (m谩s preciso)
for (var i = 1; i < productosData.length; i++) {
if (productosData[i][0] && productosData[i][0].toString() === productId) {
productId = productosData[i][0];
productPrice = productosData[i][2];
break;
}
}
// Si no se encontr贸 por ID, buscar por nombre (menos preciso)
if (!productId && product) {
for (var i = 1; i < productosData.length; i++) {
if (productosData[i][1] && productosData[i][1].toString().toLowerCase() === product.toLowerCase()) {
productId = productosData[i][0];
productPrice = productosData[i][2];
break;
}
}
}
// Generar ID de pedido
var pedidoId = 'PED-' + new Date().getTime();
// Agregar a la hoja de pedidos (columnas separadas para lat/lng para Google Looker Studio)
sheet.appendRow([
product, // Productos
quantity, // Cantidad
client, // Cliente
'', // INVENTARIO (dejar vaco)
productId, // id
pedidoId, // id_pedido
productPrice * quantity, // Precio
productId, // id_producto
latitude, // Latitud
longitude, // Longitud
phone, // Tel茅fono
fecha // fecha
]);
// Actualizar inventario
updateInventory(product, -quantity);
return ContentService.createTextOutput(JSON.stringify({
success: true,
pedidoId: pedidoId,
price: productPrice,
total: productPrice * quantity
})
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ error: error.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}