// Script para visor 3D interactivo con integración a Google Sheets // Carga productos desde hoja "Productos" y muestra STL correspondientes // Configuración de Google Sheets (pueden ser sobrescritas por window.SPREADSHEET_ID y window.API_KEY) const SPREADSHEET_ID = window.SPREADSHEET_ID || '1eCkidPqBRn48yK8sSFNmJ-qxu3us_K9c'; const API_KEY = window.API_KEY || 'AIzaSyCSEyIp3kVhPJZPeWI4TyGg4QJ2QMqYUDU'; // URL de la hoja de productos const sheetUrl = window.sheetUrl || '/api/gs?action=products'; const ordersUrl = window.ordersUrl || '/api/gs?action=order'; // URL para actualizar inventario const updateUrl = (row, column) => `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/Products!${column}${row}:${column}${row}?key=${API_KEY}`; // Variables globales let scene, camera, renderer, controls; let currentMesh = null; let currentProduct = null; let productsData = {}; let inventoryStatus = {}; let isFetching = false; let fetchError = null; // Variables del producto seleccionado let quantity = 1; let basePrice = 0; let colorPrice = 0; let currentMaterialColor = 0xffffff; // Inicialización del visor 3D function initThreeViewer() { const container = document.getElementById('mainViewer'); if (!container) return; scene = new THREE.Scene(); scene.background = new THREE.Color(0x0a0a0a); camera = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.1, 1000); camera.position.set(0, 5, 10); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(container.clientWidth, container.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); container.appendChild(renderer.domElement); const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 10, 10); scene.add(directionalLight); const gridHelper = new THREE.GridHelper(20, 20, 0x404040, 0x303030); scene.add(gridHelper); controls = new THREE.OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.autoRotate = true; controls.autoRotateSpeed = 2; animate(); } // Animación del visor function animate() { requestAnimationFrame(animate); if (controls) controls.update(); if (renderer && scene && camera) renderer.render(scene, camera); } // Cargar modelo STL function loadModel(url) { if (currentMesh) { currentMesh.geometry.dispose(); currentMesh.material.dispose(); scene.remove(currentMesh); currentMesh = null; } const loader = new THREE.STLLoader(); loader.load( url, (geometry) => { try { geometry.computeBoundingBox(); if (!geometry.boundingBox) { console.warn('Geometry has no bounding box'); return; } const material = new THREE.MeshStandardMaterial({ color: currentMaterialColor, metalness: 0.3, roughness: 0.7 }); currentMesh = new THREE.Mesh(geometry, material); currentMesh.position.set(0, 0, 0); const box = geometry.boundingBox; const center = box.getCenter(new THREE.Vector3()); geometry.translate(-center.x, -box.min.y, -center.z); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); const scale = maxDim > 0 ? 5 / maxDim : 1; currentMesh.scale.setScalar(scale); scene.add(currentMesh); // Mostrar controles de personalización document.getElementById('productInfo').classList.remove('hidden'); } catch (e) { console.error('Error loading model:', e); showError('Error al cargar el modelo 3D'); } }, undefined, (error) => { console.error('STL Load Error:', error); showError('Error al cargar el archivo STL. Verifica que el archivo exista.'); } ); } // Cargar datos de productos desde Google Sheets async function fetchProductsData() { isFetching = true; fetchError = null; try { const response = await fetch(sheetUrl, { redirect: 'follow', method: 'GET', mode: 'cors' }); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); if (data.values) { data.values.forEach((row, index) => { if (index === 0) return; // Saltar encabezado const [product, price, life, inventory] = row; if (product) { const productName = product.toString().trim().toLowerCase(); productsData[productName] = { name: product.toString().trim(), price: parseFloat(price) || 0, life: life, inventory: parseInt(inventory) || 0, file: `products/${productName.replace(/\s+/g, '_')}.stl` }; } }); console.log('Productos cargados:', productsData); updateProductList(); } } catch (e) { console.error('Error fetching products:', e); fetchError = e.message; productsData = {}; showError('Error al cargar datos de productos'); } finally { isFetching = false; } } async function calculateProductPrice(productName) { try { const calculateUrl = sheetUrl.replace('action=products', 'action=calculate') + '&producto=' + encodeURIComponent(productName); console.log('Calculando precio desde:', calculateUrl); const response = await fetch(calculateUrl, { redirect: 'follow', method: 'GET', mode: 'cors' }); console.log('Response status:', response.status); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); console.log('Precio calculado (raw):', data); const price = data.total_final || data.total_itbs || 0; console.log('Precio final:', price); return price; } catch (e) { console.error('Error calculating price:', e); return 0; } } // Actualizar lista de productos en el catálogo function updateProductList() { const productList = document.getElementById('productList'); if (!productList) return; productList.innerHTML = ''; Object.values(productsData).forEach(product => { const productCard = document.createElement('div'); productCard.className = 'col-span-6 sm:col-span-4 lg:col-span-3 p-3'; productCard.innerHTML = `

${product.name}

${product.inventory} disponibles

RD$${product.price.toFixed(2)} c/u
`; productList.appendChild(productCard); }); if (Object.keys(productsData).length === 0) { productList.innerHTML = `
No hay productos disponibles. Sube archivos .STL a la carpeta products/ y actualiza la hoja de productos.
`; } } // Seleccionar producto del catálogo async function selectProduct(productName) { currentProduct = productsData[productName.toLowerCase()]; if (!currentProduct) return; // Cargar modelo 3D loadModel(currentProduct.file); // Calcular precio con Apps Script const calculatedPrice = await calculateProductPrice(currentProduct.name); if (calculatedPrice > 0) { currentProduct.price = calculatedPrice; } // Actualizar información del producto updateProductInfo(); // Habilitar botón de agregar al carrito document.getElementById('addToCartBtn').disabled = false; document.getElementById('addToCartBtn').classList.remove('cursor-not-allowed', 'bg-neutral-700', 'text-neutral-400'); document.getElementById('addToCartBtn').classList.add('bg-neutral-700', 'text-neutral-400'); document.getElementById('addToCartBtn').textContent = 'Selecciona un producto'; } // Actualizar información del producto seleccionado function updateProductInfo() { if (!currentProduct) return; document.getElementById('selectedProductName').textContent = currentProduct.name; document.getElementById('productPrice').textContent = `RD$${currentProduct.price.toFixed(2)}`; document.getElementById('availableCount').textContent = currentProduct.inventory; document.getElementById('unitPrice').textContent = `RD$${currentProduct.price.toFixed(2)}`; basePrice = currentProduct.price; updatePrice(); } // Cambiar color del modelo 3D function changeColor(color) { currentMaterialColor = color; if (currentMesh && currentMesh.material) { currentMesh.material.dispose(); const material = new THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.7 }); currentMesh.material = material; } } // Actualizar precio total function updatePrice() { const total = (basePrice + colorPrice) * quantity; document.getElementById('productPrice').textContent = `RD$${total.toFixed(2)}`; const addBtn = document.getElementById('addToCartBtn'); if (addBtn && !addBtn.disabled) { addBtn.textContent = `Ñadir al Carrito - RD$${total.toFixed(2)}`; } } // Actualizar cantidad function updateQuantity(delta) { quantity = Math.max(1, quantity + delta); document.getElementById('quantity').value = quantity; updatePrice(); } // Enviar pedido a Google Sheets async function sendOrderToSheet() { if (!currentProduct) { alert('Por favor, selecciona un producto primero'); return; } const clientName = prompt('Ingresa el nombre del cliente:', ''); if (!clientName) return; const clientAddress = prompt('Ingresa la dirección de entrega:', ''); if (!clientAddress) return; const clientPhone = prompt('Ingresa el número de teléfono:', ''); if (!clientPhone) return; const orderData = { producto: currentProduct.name, cantidad: quantity, cliente: clientName, direccion: clientAddress, telefono: clientPhone, fecha: new Date().toISOString(), precio: (basePrice + colorPrice) * quantity }; try { showLoading(); const orderUrl = sheetUrl.replace('action=products', 'action=order') + '&product=' + encodeURIComponent(orderData.producto) + '&quantity=' + orderData.cantidad + '&client=' + encodeURIComponent(orderData.cliente) + '&address=' + encodeURIComponent(orderData.direccion) + '&phone=' + encodeURIComponent(orderData.telefono) + '&fecha=' + encodeURIComponent(orderData.fecha); const response = await fetch(orderUrl, { redirect: 'follow', method: 'GET', mode: 'cors' }); if (!response.ok) { throw new Error('Error al enviar el pedido'); } const result = await response.json(); hideLoading(); alert('¡Pedido enviado exitosamente!' + ` • Producto: ${orderData.producto} • Cantidad: ${orderData.cantidad} • Cliente: ${orderData.cliente} • Dirección: ${orderData.direccion} • Teléfono: ${orderData.telefono} • Total: RD$${result.total || orderData.precio.toFixed(2)}`); // Download invoice PDF const invoiceUrl = \`/api/invoice?product=${encodeURIComponent(orderData.producto)}&quantity=${orderData.cantidad}&client=${encodeURIComponent(orderData.cliente)}&phone=${encodeURIComponent(orderData.telefono)}&price=${orderData.precio}&pedido_id=${result.pedidoId}\`; window.location.href = invoiceUrl; // Redirect to home after 2 seconds setTimeout(() => { window.location.href = '/visor-3d'; }, 2000); } catch (e) { console.error('Error sending order:', e); hideLoading(); showError('Error al enviar el pedido. Inténtalo de nuevo.'); } } } // Actualizar inventario en Google Sheets async function updateInventory(productName, quantity) { try { const sheetUrl = '/api/gs?action=products'; const response = await fetch(sheetUrl); const data = await response.json(); if (data.values) { for (let i = 1; i < data.values.length; i++) { const row = data.values[i]; if (row[0] && row[0].toLowerCase() === productName.toLowerCase()) { const currentInv = parseInt(row[3]) || 0; const newInv = currentInv + quantity; // Actualizar en la hoja const updateUrl = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/Products!D${i+1}:D${i+1}?key=${API_KEY}`; await fetch(updateUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ values: [[newInv]] }) }); // Actualizar datos locales currentProduct.inventory = newInv; updateProductInfo(); break; } } } } catch (e) { console.error('Error updating inventory:', e); } } // Mostrar overlay de carga function showLoading() { const loadingEl = document.getElementById('loadingOverlay'); if (loadingEl) loadingEl.classList.remove('hidden'); } // Ocultar overlay de carga function hideLoading() { const loadingEl = document.getElementById('loadingOverlay'); if (loadingEl) loadingEl.classList.add('hidden'); } // Mostrar mensaje de error function showError(message) { const errorEl = document.getElementById('errorMessage'); const errorContainer = document.getElementById('errorMessages'); if (errorEl) errorEl.textContent = message; if (errorContainer) errorContainer.classList.remove('hidden'); } // Ocultar mensaje de error function hideError() { const errorContainer = document.getElementById('errorMessages'); if (errorContainer) errorContainer.classList.add('hidden'); } // Inicializar al cargar la página window.addEventListener('DOMContentLoaded', async () => { // Inicializar visor 3D initThreeViewer(); // Cargar productos desde Google Sheets await fetchProductsData(); // Verificar si hay error if (fetchError) { console.warn('Error al cargar productos:', fetchError); } }); // Event listeners para colores document.querySelectorAll('.color-swatch').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.color-swatch').forEach(b => b.classList.remove('active')); btn.classList.add('active'); colorPrice = parseFloat(btn.dataset.price) || 0; updatePrice(); }); }); // Only run if addToCartBtn exists (visor-3d page) const addToCartBtn = document.getElementById('addToCartBtn'); if (addToCartBtn) { addToCartBtn.addEventListener('click', sendOrderToSheet); }