| |
| |
|
|
| |
| const SPREADSHEET_ID = window.SPREADSHEET_ID || '1eCkidPqBRn48yK8sSFNmJ-qxu3us_K9c'; |
| const API_KEY = window.API_KEY || 'AIzaSyCSEyIp3kVhPJZPeWI4TyGg4QJ2QMqYUDU'; |
|
|
| |
| const sheetUrl = window.sheetUrl || '/api/gs?action=products'; |
|
|
| const ordersUrl = window.ordersUrl || '/api/gs?action=order'; |
|
|
| |
| const updateUrl = (row, column) => `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/Products!${column}${row}:${column}${row}?key=${API_KEY}`; |
|
|
| |
| let scene, camera, renderer, controls; |
| let currentMesh = null; |
| let currentProduct = null; |
| let productsData = {}; |
| let inventoryStatus = {}; |
| let isFetching = false; |
| let fetchError = null; |
|
|
| |
| let quantity = 1; |
| let basePrice = 0; |
| let colorPrice = 0; |
| let currentMaterialColor = 0xffffff; |
|
|
| |
| 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(); |
| } |
|
|
| |
| function animate() { |
| requestAnimationFrame(animate); |
| if (controls) controls.update(); |
| if (renderer && scene && camera) renderer.render(scene, camera); |
| } |
|
|
| |
| 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); |
| |
| |
| 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.'); |
| } |
| ); |
| } |
|
|
| |
| 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; |
| 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; |
| } |
| } |
|
|
| |
| 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 = ` |
| <div class="bg-neutral-900 rounded-2xl border border-neutral-800 p-6 cursor-pointer hover:shadow-lg transition-all" |
| onclick="selectProduct('${product.name}')"> |
| <div class="text-center mb-4"> |
| <div class="w-20 h-20 mx-auto bg-neutral-800 rounded-lg flex items-center justify-center"> |
| <svg class="w-12 h-12 text-neutral-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path> |
| </svg> |
| </div> |
| </div> |
| <h3 class="text-lg font-medium mb-2">${product.name}</h3> |
| <p class="text-sm text-neutral-400 mb-4">${product.inventory} disponibles</p> |
| <div class="flex items-baseline gap-2"> |
| <span class="text-2xl font-display">RD$${product.price.toFixed(2)}</span> |
| <span class="text-neutral-500 text-sm">c/u</span> |
| </div> |
| </div> |
| `; |
| productList.appendChild(productCard); |
| }); |
| |
| if (Object.keys(productsData).length === 0) { |
| productList.innerHTML = ` |
| <div class="col-span-full text-center py-8 text-neutral-500"> |
| No hay productos disponibles. Sube archivos .STL a la carpeta products/ y actualiza la hoja de productos. |
| </div> |
| `; |
| } |
| } |
|
|
| |
| async function selectProduct(productName) { |
| currentProduct = productsData[productName.toLowerCase()]; |
| if (!currentProduct) return; |
| |
| |
| loadModel(currentProduct.file); |
| |
| |
| const calculatedPrice = await calculateProductPrice(currentProduct.name); |
| if (calculatedPrice > 0) { |
| currentProduct.price = calculatedPrice; |
| } |
| |
| |
| updateProductInfo(); |
| |
| |
| 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'; |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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)}`; |
| } |
| } |
|
|
| |
| function updateQuantity(delta) { |
| quantity = Math.max(1, quantity + delta); |
| document.getElementById('quantity').value = quantity; |
| updatePrice(); |
| } |
|
|
| |
| 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)}`); |
| |
| |
| 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: |
| await fetch(updateUrl, { |
| method: 'PUT', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| values: [[newInv]] |
| }) |
| }); |
| |
| |
| currentProduct.inventory = newInv; |
| updateProductInfo(); |
| break; |
| } |
| } |
| } |
| } catch (e) { |
| console.error('Error updating inventory:', e); |
| } |
| } |
|
|
| |
| function showLoading() { |
| const loadingEl = document.getElementById('loadingOverlay'); |
| if (loadingEl) loadingEl.classList.remove('hidden'); |
| } |
|
|
| |
| function hideLoading() { |
| const loadingEl = document.getElementById('loadingOverlay'); |
| if (loadingEl) loadingEl.classList.add('hidden'); |
| } |
|
|
| |
| function showError(message) { |
| const errorEl = document.getElementById('errorMessage'); |
| const errorContainer = document.getElementById('errorMessages'); |
| if (errorEl) errorEl.textContent = message; |
| if (errorContainer) errorContainer.classList.remove('hidden'); |
| } |
|
|
| |
| function hideError() { |
| const errorContainer = document.getElementById('errorMessages'); |
| if (errorContainer) errorContainer.classList.add('hidden'); |
| } |
|
|
| |
| window.addEventListener('DOMContentLoaded', async () => { |
| |
| initThreeViewer(); |
| |
| |
| await fetchProductsData(); |
| |
| |
| if (fetchError) { |
| console.warn('Error al cargar productos:', fetchError); |
| } |
| }); |
|
|
| |
| 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(); |
| }); |
| }); |
|
|
| |
| const addToCartBtn = document.getElementById('addToCartBtn'); |
| if (addToCartBtn) { |
| addToCartBtn.addEventListener('click', sendOrderToSheet); |
| } |