Sistema POS inteligente con actualizaciones en tiempo real de Telegram y Sheets
Este es unCRM, AI Summarizationflujo de automatización del dominio deautomatización que contiene 16 nodos.Utiliza principalmente nodos como Code, Wait, Webhook, Telegram, GoogleSheets. Crear pedidos de venta usando una interfaz Web POS, informes de IA, recordatorios de Telegram y Sheets
- •Punto final de HTTP Webhook (n8n generará automáticamente)
- •Bot Token de Telegram
- •Credenciales de API de Google Sheets
Nodos utilizados (16)
Categoría
{
"id": "XRTJrZHlkDjGCLDq",
"meta": {
"instanceId": "0d045f8fe3802ff2be0bb9a9ea445ee6c9ed61973377effe00767e483681e2f4"
},
"name": "Smart POS System with Live Updates to Telegram & Sheets",
"tags": [],
"nodes": [
{
"id": "c6129fa5-cd4f-4903-8522-dcbe4fcd50af",
"name": "Enviar un mensaje de texto",
"type": "n8n-nodes-base.telegram",
"position": [
480,
420
],
"webhookId": "2ddba555-b1a7-4dca-b5d3-6ccb7866fad7",
"parameters": {
"text": "={{ $json.output }}",
"chatId": "YOUR_TELEGRAM_CHAT_ID",
"additionalFields": {
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"id": "AZVFe6SQjkKyufRE",
"name": "Laporan Keuangan"
}
},
"typeVersion": 1.2
},
{
"id": "75d42049-e27a-4208-82ef-4a121812563e",
"name": "Añadir o actualizar fila en la hoja",
"type": "n8n-nodes-base.googleSheets",
"position": [
380,
40
],
"parameters": {
"columns": {
"value": {
"SALES ID": "={{ $json[\"SALES ID\"] }}",
"SALES QTY": "={{ $json[\"SALES QTY\"] }}",
"SALES DATE": "={{ $json[\"SALES DATE\"] }}",
"SALES DISCOUNT": "={{ $json[\"SALES DISCOUNT\"] }}",
"SALES PRICE (USD)": "={{ $json[\"SALES PRICE (USD)\"] }}",
"SALES PRODUCT NAME": "={{ $json[\"SALES PRODUCT NAME\"] }}",
"SALES CUSTOMER NAME": "={{ $json[\"SALES CUSTOMER NAME\"] }}"
},
"schema": [
{
"id": "SALES ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "SALES ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES DATE",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES DATE",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES CUSTOMER NAME",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES CUSTOMER NAME",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES PRODUCT NAME",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES PRODUCT NAME",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES CATEGORY NAME",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "SALES CATEGORY NAME",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES PRICE (USD)",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES PRICE (USD)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES QTY",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES QTY",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES DISCOUNT",
"type": "string",
"display": true,
"required": false,
"displayName": "SALES DISCOUNT",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "SALES TOTAL",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "SALES TOTAL",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"SALES ID"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1157363351
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEETS_ID"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "wPmZzacn7hIP4akd",
"name": "Google Sheets account"
}
},
"executeOnce": false,
"typeVersion": 4.6
},
{
"id": "e8b420ed-e5cb-4835-9f9b-979bb9e44e05",
"name": "Iniciar el webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-1180,
220
],
"webhookId": "47332c29-1dd6-4aa2-b59e-80b4b265e3f4",
"parameters": {
"path": "smartpostsystem",
"options": {},
"responseMode": "responseNode"
},
"typeVersion": 1
},
{
"id": "84791b6d-a9b2-42b5-8c26-616c66adadd6",
"name": "Obtener datos de productos",
"type": "n8n-nodes-base.googleSheets",
"position": [
-960,
220
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "YOUR_GOOGLE_SHEETS_ID"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "wPmZzacn7hIP4akd",
"name": "Google Sheets account"
}
},
"executeOnce": true,
"typeVersion": 4.6
},
{
"id": "dff306ac-b6ad-4814-9ed2-2ffed96b420a",
"name": "Responder al Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-460,
220
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=UTF-8"
}
]
}
},
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html>\n\n<head>\n <title>Food Ordering App</title>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css\">\n <style>\n body {\n background-color: #f8f9fa;\n font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n }\n\n .main-container {\n display: flex;\n height: calc(100vh - 56px);\n max-width: 1400px;\n margin: 0 auto;\n background: white;\n box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);\n }\n\n .left-panel {\n flex: 2;\n padding: 20px;\n border-right: 1px solid #e9ecef;\n overflow-y: auto;\n }\n\n .right-panel {\n flex: 1;\n padding: 20px;\n background: #f8f9fa;\n overflow-y: auto;\n }\n\n .header-section {\n margin-bottom: 20px;\n }\n\n .category-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 15px;\n }\n\n .category-title {\n font-size: 24px;\n font-weight: 600;\n color: #333;\n margin: 0;\n }\n\n .search-filter {\n display: flex;\n gap: 10px;\n margin-bottom: 20px;\n }\n\n .search-box {\n flex: 1;\n position: relative;\n }\n\n .search-box input {\n width: 100%;\n padding: 10px 40px 10px 15px;\n border: 1px solid #ddd;\n border-radius: 8px;\n font-size: 14px;\n }\n\n .search-box i {\n position: absolute;\n right: 15px;\n top: 50%;\n transform: translateY(-50%);\n color: #666;\n }\n\n .filter-btn {\n padding: 10px 15px;\n border: 1px solid #ddd;\n border-radius: 8px;\n background: white;\n color: #666;\n }\n\n .category-tabs {\n display: flex;\n gap: 10px;\n margin-bottom: 20px;\n overflow-x: auto;\n padding-bottom: 5px;\n }\n\n .category-tab {\n padding: 8px 16px;\n border-radius: 20px;\n border: none;\n background: #e9ecef;\n color: #666;\n white-space: nowrap;\n cursor: pointer;\n transition: all 0.3s;\n }\n\n .category-tab.active {\n background: #6c757d;\n color: white;\n }\n\n .products-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n gap: 20px;\n }\n\n .product-card {\n background: white;\n border-radius: 12px;\n padding: 15px;\n text-align: center;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n transition: transform 0.2s;\n }\n\n .product-card:hover {\n transform: translateY(-2px);\n }\n\n .product-image {\n width: 100%;\n height: 120px;\n object-fit: cover;\n border-radius: 8px;\n margin-bottom: 10px;\n }\n\n .product-name {\n font-weight: 600;\n margin-bottom: 5px;\n color: #333;\n }\n\n .product-price {\n color: #666;\n font-weight: 500;\n margin-bottom: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n }\n\n .discount-badge {\n background: #dc3545;\n color: white;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 600;\n }\n\n .add-btn {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n border: none;\n background: #333;\n color: white;\n font-size: 18px;\n cursor: pointer;\n transition: background 0.3s;\n }\n\n .add-btn:hover {\n background: #555;\n }\n\n .navbar {\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n }\n\n .order-header {\n font-size: 20px;\n font-weight: 600;\n margin-bottom: 20px;\n color: #333;\n }\n\n .user-profile {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 20px;\n padding: 10px;\n background: white;\n border-radius: 8px;\n }\n\n .user-avatar {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: #6c757d;\n display: flex;\n align-items: center;\n justify-content: center;\n color: white;\n font-weight: 600;\n }\n\n .customer-form {\n background: white;\n border-radius: 8px;\n padding: 15px;\n margin-bottom: 20px;\n }\n\n .form-title {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 15px;\n color: #333;\n }\n\n .form-group {\n margin-bottom: 15px;\n }\n\n .form-group label {\n display: block;\n margin-bottom: 5px;\n font-weight: 500;\n color: #333;\n font-size: 14px;\n }\n\n .form-control {\n width: 100%;\n padding: 10px 12px;\n border: 1px solid #ddd;\n border-radius: 6px;\n font-size: 14px;\n transition: border-color 0.3s;\n }\n\n .form-control:focus {\n outline: none;\n border-color: #6f42c1;\n box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.1);\n }\n\n .order-items {\n margin-bottom: 20px;\n }\n\n .order-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px;\n background: white;\n border-radius: 8px;\n margin-bottom: 10px;\n }\n\n .item-thumbnail {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n object-fit: cover;\n }\n\n .item-details {\n flex: 1;\n }\n\n .item-name {\n font-weight: 500;\n color: #333;\n margin-bottom: 2px;\n }\n\n .item-price {\n color: #666;\n font-size: 14px;\n }\n\n .item-controls {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .qty-btn {\n width: 25px;\n height: 25px;\n border-radius: 50%;\n border: 1px solid #ddd;\n background: white;\n color: #666;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .qty-display {\n min-width: 20px;\n text-align: center;\n font-weight: 500;\n }\n\n .order-summary {\n background: white;\n border-radius: 8px;\n padding: 15px;\n margin-bottom: 20px;\n }\n\n .summary-row {\n display: flex;\n justify-content: space-between;\n margin-bottom: 8px;\n }\n\n .summary-row.total {\n font-weight: 600;\n font-size: 18px;\n border-top: 1px solid #e9ecef;\n padding-top: 10px;\n margin-top: 10px;\n }\n\n .continue-btn {\n width: 100%;\n padding: 15px;\n background: #6f42c1;\n color: white;\n border: none;\n border-radius: 8px;\n font-weight: 600;\n font-size: 16px;\n cursor: pointer;\n transition: background 0.3s;\n }\n\n .continue-btn:hover {\n background: #5a32a3;\n }\n\n @media (max-width: 768px) {\n .main-container {\n flex-direction: column;\n height: auto;\n }\n\n .left-panel,\n .right-panel {\n flex: none;\n }\n\n .products-grid {\n grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n }\n }\n </style>\n</head>\n\n<body>\n <!-- Navigation Menu -->\n <nav class=\"navbar navbar-expand-lg navbar-dark bg-dark\">\n <div class=\"container-fluid\">\n <a class=\"navbar-brand\" href=\"#\">POS SYSTEM</a>\n </div>\n </nav>\n\n <div class=\"main-container\">\n <!-- Left Panel - Items Selection -->\n <div class=\"left-panel\">\n <div class=\"header-section\">\n <div class=\"category-header\">\n <h1 class=\"category-title\">POS SYSTEM</h1>\n </div>\n\n <div class=\"search-filter\">\n <div class=\"search-box\">\n <input type=\"text\" id=\"searchInput\" placeholder=\"Search products...\" onkeyup=\"filterProducts()\">\n <i class=\"fas fa-search\"></i>\n </div>\n </div>\n\n <div class=\"category-tabs\" id=\"categoryTabs\">\n <!-- Categories will be dynamically generated from JSON data -->\n </div>\n </div>\n\n <div class=\"products-grid\" id=\"productsGrid\">\n <!-- Products will be dynamically generated from JSON data -->\n </div>\n </div>\n\n <!-- Right Panel - Current Order -->\n <div class=\"right-panel\">\n <h2 class=\"order-header\">Current Order</h2>\n\n <div class=\"customer-form\">\n <h4 class=\"form-title\">Customer Information</h4>\n <div class=\"form-group\">\n <label for=\"customerName\">Name</label>\n <input type=\"text\" id=\"customerName\" class=\"form-control\" placeholder=\"Enter customer name\"\n required>\n </div>\n </div>\n\n <div class=\"order-items\" id=\"orderItems\">\n <!-- Order items will be dynamically added here -->\n </div>\n\n <div class=\"order-summary\">\n <div class=\"summary-row\">\n <span>Subtotal</span>\n <span id=\"subtotal\">$0.00</span>\n </div>\n <div class=\"summary-row\">\n <span>Discount</span>\n <span id=\"discount\">$0.00</span>\n </div>\n <div class=\"summary-row total\">\n <span>Total</span>\n <span id=\"total\">$0.00</span>\n </div>\n </div>\n\n <form action=\"{{ $resumeWebhookUrl }}\" method=\"post\" id=\"orderForm\">\n <!-- Hidden inputs for order data -->\n <input type=\"hidden\" name=\"customerName\" id=\"hiddenCustomerName\">\n <input type=\"hidden\" name=\"orderItems\" id=\"hiddenOrderItems\">\n <input type=\"hidden\" name=\"orderTotals\" id=\"hiddenOrderTotals\">\n\n <button type=\"submit\" class=\"continue-btn\" onclick=\"prepareFormData(event)\">Continue</button>\n </form>\n </div>\n </div>\n\n <script>\n // Products data from n8n workflow - separate arrays\n const productIds = `{{ $json.productId }}`;\n const productNames = `{{ $json.productName }}`;\n const productImages = `{{ $json.productImage }}`;\n const categoryNames = `{{ $json.productCategoryName }}`;\n const prices = `{{ $json.productPriceUsd }}`;\n const discounts = `{{ $json.productDiscount }}`;\n \n let orderItems = [];\n let orderTotal = 0;\n let currentCategory = 'All';\n let allProducts = [];\n\n // Parse products data from n8n\n function initializeProducts() {\n try {\n // Parse the string arrays into actual arrays\n const parsedProductIds = JSON.parse(productIds);\n const parsedProductNames = JSON.parse(productNames);\n const parsedProductImages = JSON.parse(productImages);\n const parsedCategoryNames = JSON.parse(categoryNames);\n const parsedPrices = JSON.parse(prices);\n const parsedDiscounts = JSON.parse(discounts);\n\n // Combine arrays into product objects\n allProducts = parsedProductIds.map((id, index) => ({\n 'PRODUCT ID': id,\n 'PRODUCT NAME': parsedProductNames[index],\n 'PRODUCT IMAGE': parsedProductImages[index],\n 'CATEGORY NAME': parsedCategoryNames[index],\n 'PRICE (USD)': parsedPrices[index],\n 'DISCOUNT': parsedDiscounts[index]\n }));\n\n generateCategoryTabs();\n generateProductsGrid();\n } catch (error) {\n console.error('Error parsing products data:', error);\n // Fallback to empty array if parsing fails\n allProducts = [];\n }\n }\n\n function addToOrder(name, price, image, originalPrice, discountRate) {\n const existingItem = orderItems.find(item => item.name === name);\n\n if (existingItem) {\n existingItem.quantity += 1;\n existingItem.total = existingItem.quantity * existingItem.price;\n } else {\n orderItems.push({\n name: name,\n price: price,\n originalPrice: originalPrice,\n discountRate: discountRate,\n image: image,\n quantity: 1,\n total: price\n });\n }\n\n updateOrderDisplay();\n calculateTotals();\n }\n\n function updateQuantity(name, change) {\n const item = orderItems.find(item => item.name === name);\n\n if (item) {\n item.quantity += change;\n\n if (item.quantity <= 0) {\n orderItems = orderItems.filter(item => item.name !== name);\n } else {\n item.total = item.quantity * item.price;\n }\n\n updateOrderDisplay();\n calculateTotals();\n }\n }\n\n function updateOrderDisplay() {\n const orderItemsContainer = document.getElementById('orderItems');\n orderItemsContainer.innerHTML = '';\n\n orderItems.forEach(item => {\n const itemElement = document.createElement('div');\n itemElement.className = 'order-item';\n\n // Calculate item discount\n const itemDiscount = (item.originalPrice * item.quantity * item.discountRate);\n const discountText = item.discountRate > 0 ? `<br><small style=\"color: #dc3545;\">-$${itemDiscount.toFixed(2)} discount</small>` : '';\n\n itemElement.innerHTML = `\n <img src=\"${item.image}\" class=\"item-thumbnail\" alt=\"${item.name}\">\n <div class=\"item-details\">\n <div class=\"item-name\">${item.name}</div>\n <div class=\"item-price\">$${item.price.toFixed(2)}${discountText}</div>\n </div>\n <div class=\"item-controls\">\n <button class=\"qty-btn\" onclick=\"updateQuantity('${item.name}', -1)\">-</button>\n <span class=\"qty-display\">${item.quantity}</span>\n <button class=\"qty-btn\" onclick=\"updateQuantity('${item.name}', 1)\">+</button>\n </div>\n `;\n orderItemsContainer.appendChild(itemElement);\n });\n }\n\n function calculateTotals() {\n const subtotal = orderItems.reduce((sum, item) => sum + item.total, 0);\n\n // Calculate total discount based on individual item discounts\n const totalDiscount = orderItems.reduce((sum, item) => {\n const itemDiscount = (item.originalPrice * item.quantity * item.discountRate);\n return sum + itemDiscount;\n }, 0);\n\n const total = subtotal;\n\n document.getElementById('subtotal').textContent = `$${subtotal.toFixed(2)}`;\n document.getElementById('discount').textContent = `$${totalDiscount.toFixed(2)}`;\n document.getElementById('total').textContent = `$${total.toFixed(2)}`;\n }\n\n function prepareFormData(event) {\n event.preventDefault();\n\n // Validate customer information\n const customerName = document.getElementById('customerName').value.trim();\n\n if (!customerName) {\n alert('Please enter customer name.');\n document.getElementById('customerName').focus();\n return;\n }\n\n if (orderItems.length === 0) {\n alert('Please add items to your order first.');\n return;\n }\n\n // Prepare order data\n const orderData = {\n customer: {\n name: customerName\n },\n items: orderItems,\n totals: {\n subtotal: orderItems.reduce((sum, item) => sum + item.total, 0),\n total: orderItems.reduce((sum, item) => sum + item.total, 0)\n }\n };\n\n // Set hidden form values\n document.getElementById('hiddenCustomerName').value = customerName;\n document.getElementById('hiddenOrderItems').value = JSON.stringify(orderItems);\n document.getElementById('hiddenOrderTotals').value = JSON.stringify(orderData.totals);\n\n // Show loading state\n const submitBtn = document.querySelector('.continue-btn');\n const originalText = submitBtn.textContent;\n submitBtn.textContent = 'Processing...';\n submitBtn.disabled = true;\n\n // Submit the form\n document.getElementById('orderForm').submit();\n }\n\n // Function to get unique categories from products data\n function getCategories() {\n const categories = [...new Set(allProducts.map(product => product['CATEGORY NAME']))];\n return ['All', ...categories];\n }\n\n // Function to generate category tabs\n function generateCategoryTabs() {\n const categories = getCategories();\n const categoryTabsContainer = document.getElementById('categoryTabs');\n categoryTabsContainer.innerHTML = '';\n\n categories.forEach(category => {\n const tab = document.createElement('button');\n tab.className = 'category-tab';\n if (category === 'All') {\n tab.classList.add('active');\n }\n tab.textContent = category;\n tab.onclick = () => filterByCategory(category);\n categoryTabsContainer.appendChild(tab);\n });\n }\n\n // Function to filter products by category\n function filterByCategory(category) {\n currentCategory = category;\n\n // Update active tab\n document.querySelectorAll('.category-tab').forEach(tab => {\n tab.classList.remove('active');\n if (tab.textContent === category) {\n tab.classList.add('active');\n }\n });\n\n // Filter products\n if (category === 'All') {\n // Re-initialize all products\n initializeProducts();\n } else {\n // Filter by category\n const parsedProductIds = JSON.parse(productIds);\n const parsedProductNames = JSON.parse(productNames);\n const parsedProductImages = JSON.parse(productImages);\n const parsedCategoryNames = JSON.parse(categoryNames);\n const parsedPrices = JSON.parse(prices);\n const parsedDiscounts = JSON.parse(discounts);\n\n // Combine arrays into product objects and filter by category\n allProducts = parsedProductIds.map((id, index) => ({\n 'PRODUCT ID': id,\n 'PRODUCT NAME': parsedProductNames[index],\n 'PRODUCT IMAGE': parsedProductImages[index],\n 'CATEGORY NAME': parsedCategoryNames[index],\n 'PRICE (USD)': parsedPrices[index],\n 'DISCOUNT': parsedDiscounts[index]\n })).filter(product => product['CATEGORY NAME'] === category);\n\n generateProductsGrid();\n }\n }\n\n // Function to generate product cards\n function generateProductsGrid() {\n const productsGrid = document.getElementById('productsGrid');\n productsGrid.innerHTML = '';\n\n allProducts.forEach(product => {\n const productCard = document.createElement('div');\n productCard.className = 'product-card';\n\n // Calculate final price with discount\n const originalPrice = product['PRICE (USD)'];\n const discount = product['DISCOUNT'];\n const finalPrice = originalPrice * (1 - discount);\n\n // Use image from JSON data\n const imageUrl = product['PRODUCT IMAGE'];\n\n productCard.innerHTML = `\n <img src=\"${imageUrl}\" class=\"product-image\" alt=\"${product['PRODUCT NAME']}\">\n <div class=\"product-name\">${product['PRODUCT NAME']}</div>\n <div class=\"product-price\">\n $${finalPrice.toFixed(2)}\n ${discount > 0 ? `<span class=\"discount-badge\">-${(discount * 100).toFixed(0)}%</span>` : ''}\n </div>\n <button class=\"add-btn\" onclick=\"addToOrder('${product['PRODUCT NAME']}', ${finalPrice}, '${imageUrl}', ${originalPrice}, ${discount})\">+</button>\n `;\n\n productsGrid.appendChild(productCard);\n });\n }\n\n // Function to filter products by search\n function filterProducts() {\n const searchTerm = document.getElementById('searchInput').value.toLowerCase();\n const productsGrid = document.getElementById('productsGrid');\n productsGrid.innerHTML = '';\n\n const filteredProducts = allProducts.filter(product =>\n product['PRODUCT NAME'].toLowerCase().includes(searchTerm) ||\n product['CATEGORY NAME'].toLowerCase().includes(searchTerm)\n );\n\n filteredProducts.forEach(product => {\n const productCard = document.createElement('div');\n productCard.className = 'product-card';\n\n // Calculate final price with discount\n const originalPrice = product['PRICE (USD)'];\n const discount = product['DISCOUNT'];\n const finalPrice = originalPrice * (1 - discount);\n\n // Use image from JSON data\n const imageUrl = product['PRODUCT IMAGE'];\n\n productCard.innerHTML = `\n <img src=\"${imageUrl}\" class=\"product-image\" alt=\"${product['PRODUCT NAME']}\">\n <div class=\"product-name\">${product['PRODUCT NAME']}</div>\n <div class=\"product-price\">\n $${finalPrice.toFixed(2)}\n ${discount > 0 ? `<span class=\"discount-badge\">-${(discount * 100).toFixed(0)}%</span>` : ''}\n </div>\n <button class=\"add-btn\" onclick=\"addToOrder('${product['PRODUCT NAME']}', ${finalPrice}, '${imageUrl}', ${originalPrice}, ${discount})\">+</button>\n `;\n\n productsGrid.appendChild(productCard);\n });\n }\n\n // Initialize the display\n initializeProducts();\n updateOrderDisplay();\n calculateTotals();\n </script>\n</body>\n\n</html>"
},
"typeVersion": 1.4
},
{
"id": "da9ca9ef-1767-4fe9-aff2-9e15a7194fd8",
"name": "Esperar clic",
"type": "n8n-nodes-base.wait",
"position": [
-240,
220
],
"webhookId": "2b00ae48-f8f7-49f5-8237-d50f83ff5aa2",
"parameters": {
"resume": "webhook",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 1
},
{
"id": "7e69a7e5-5a31-41b8-9ac2-bae76e807dd1",
"name": "Responder al clic",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-40,
220
],
"parameters": {
"options": {},
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"id\">\n<head>\n <meta charset=\"UTF-8\" />\n <title>Processing Order</title>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <style>\n body {\n font-family: Arial, sans-serif;\n text-align: center;\n padding-top: 100px;\n background-color: #f0f9f4;\n }\n .icon {\n font-size: 64px;\n color: #4CAF50;\n }\n .message {\n font-size: 24px;\n color: #333;\n margin-top: 20px;\n }\n .redirect {\n font-size: 16px;\n color: #666;\n margin-top: 10px;\n }\n </style>\n <script>\n setTimeout(function() {\n window.location.href = \"{{ $resumeWebhookUrl }}\";\n }, 3000); // Wait 3 seconds\n </script>\n</head>\n<body>\n <div class=\"icon\">✅</div>\n <div class=\"message\">Order is successfully processed</div>\n <div class=\"redirect\">You will be redirected shortly...</div>\n</body>\n</html>\n"
},
"typeVersion": 1
},
{
"id": "2f766abf-2d22-4721-9942-247d5534b482",
"name": "Formatear datos para la hoja",
"type": "n8n-nodes-base.code",
"position": [
180,
40
],
"parameters": {
"jsCode": "// Get data from body\nconst data = $input.first().json.body;\n\n// Parse orderItems and orderTotals\nconst items = JSON.parse(data.orderItems);\n\n// Function to generate unique Sales ID\nfunction generateSalesId() {\n const timestamp = Date.now();\n const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');\n return `S-${timestamp}-${random}`;\n}\n\n// Get today's date\nconst today = new Date().toISOString().split('T')[0];\n\n// Generate output\nconst salesId = generateSalesId();\nconst output = items.map(item => {\n const discount = Number((item.originalPrice - item.price).toFixed(2));\n return {\n 'SALES ID': salesId,\n 'SALES DATE': today, \n 'SALES CUSTOMER NAME': data.customerName,\n 'SALES PRODUCT NAME': item.name,\n 'SALES CATEGORY NAME': '', // Category data not available yet\n 'SALES PRICE (USD)': item.price,\n 'SALES QTY': item.quantity,\n 'SALES DISCOUNT': discount,\n 'SALES TOTAL': item.total\n };\n});\n\nreturn output.map(item => ({ json: item }));\n"
},
"typeVersion": 2
},
{
"id": "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e",
"name": "Agente de IA",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
180,
420
],
"parameters": {
"text": "=customer name : {{ $json.body.customerName }}\norder items : {{ $json.body.orderItems }}\norder total : {{ $json.body.orderTotals }}\n\nSales report format : \nNew sales! (opening or greetings to the owner )\ncustomer name : \norder details :\nHave a good day (closing)",
"options": {
"systemMessage": "=You are a virtual assistant whose primary task is to create sales reports for business owners.\nWrite in a simple and friendly format. Use emojis to make it more interactive.\nSome item prices are separated by commas.\nFormat all numbers such as prices, subtotal, and total to 2 decimal places only (e.g., 12.97, not 12.969999999999999).\nAvoid using long floating-point numbers.\n\nAvoid using special characters that may break Markdown formatting, such as:\n*, _, [, ], (, ), ~, >, #, +, -, =, {, }, ., !, $.\nUse plain text without special symbols unless necessary.\nDo not use Markdown or HTML formatting."
},
"promptType": "define"
},
"typeVersion": 2
},
{
"id": "f4b29ac5-8dee-48bc-83a3-136795a447fa",
"name": "Modelo de Chat OpenRouter",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
"position": [
180,
600
],
"parameters": {
"model": "google/gemini-2.0-flash-exp:free",
"options": {}
},
"credentials": {
"openRouterApi": {
"id": "5gucapot70b4Qz8b",
"name": "OpenRouter ASNM"
}
},
"typeVersion": 1
},
{
"id": "72a622bc-b3b7-4840-9b08-686c65b482f9",
"name": "Nota adhesiva",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1760,
-440
],
"parameters": {
"width": 500,
"height": 1320,
"content": "# Smart POS System with Live Updates to Telegram & Sheets\n\nThis Smart POS (Point of Sale) System template provides a lightweight yet powerful sales management solution. It features a modern web based interface for placing orders, with **real-time integration** to **Google Sheets** and **instant Telegram notifications**, enhanced by **AI-generated reports**. \nIdeal for small businesses, mobile vendors, or anyone who needs a quick and smart POS system.\n\n## ✨ Key Features\n- 🖥️ Modern web interface with product catalog and search\n- 🛒 Cart system with quantity, price, and discount handling\n- 🆔 Unique Sales ID generation for every transaction\n- 📊 Google Sheets integration to store product and sales data\n- 🤖 AI-generated sales summary via OpenRouter\n- 🚀 Instant Telegram notifications for new orders\n\n---\n\n## 🔧 Requirements\n- A Google Sheet to store products and sales data \n 👉 [Use this Google Sheets template to get started](https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEETS_ID/edit?usp=sharing)\n- Telegram Bot Token and User ID \n (Create a bot via [@BotFather](https://t.me/BotFather))\n- OpenRouter API Key \n (Sign up at [openrouter.ai](https://openrouter.ai) and use the LLM model)\n\n---\n\n## ⚙️ Setup Instructions\n1. **Set Up Your Google Sheets**\n - Use the template and fill in product details in the `products` tab\n\n2. **Configure Telegram Bot**\n - Create a bot via BotFather\n - Obtain your Bot Token and Chat ID (message the bot once to get ID)\n\n3. **Set Up AI Agent**\n - In the AI agent node, replace the placeholder with your actual OpenRouter API Key\n\n---\n\n## 🚀 Deploy the Workflow\n1. **Activate** the workflow in n8n\n2. **Open** the webhook URL to access the POS interface\n3. **Enter** product orders and customer details\n4. **Submit** the order\n5. **Receive** an instant Telegram notification with AI-generated sales summary\n6. **Data** is automatically saved to Google Sheets for tracking and analysis\n\n"
},
"typeVersion": 1
},
{
"id": "ae33dc7d-b8c1-483d-a685-370c5b08e61f",
"name": "Formatear datos para webhook",
"type": "n8n-nodes-base.code",
"position": [
-740,
220
],
"parameters": {
"jsCode": "// Get all input data\nconst input = $input.all();\n\n// Extract product data columns\nconst productId = input.map(item => item.json[\"PRODUCT ID\"]);\nconst productName = input.map(item => item.json[\"PRODUCT NAME\"]);\nconst productImage = input.map(item => item.json[\"PRODUCT IMAGE\"]);\nconst productCategoryName = input.map(item => item.json[\"PRODUCT CATEGORY NAME\"]);\nconst productPriceUsd = input.map(item => item.json[\"PRODUCT PRICE (USD)\"]);\nconst productDiscount = input.map(item => item.json[\"PRODUCT DISCOUNT\"]);\n\n// Return in JSON string format\nreturn [{\n json: {\n // Product data\n productId: JSON.stringify(productId),\n productName: JSON.stringify(productName),\n productImage: JSON.stringify(productImage),\n productCategoryName: JSON.stringify(productCategoryName),\n productPriceUsd: JSON.stringify(productPriceUsd),\n productDiscount: JSON.stringify(productDiscount),\n\n // Webhook URL (if needed for redirect or resubmit)\n webhookUrl: '{{ $json.webhookUrl }}',\n resumeWebhookUrl: '{{ $resumeWebhookUrl }}'\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "7b344857-b997-4882-94c9-770c19df9394",
"name": "Nota adhesiva1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
40
],
"parameters": {
"color": 2,
"width": 640,
"height": 340,
"content": "- Creates POS interface in HTML format\n- Receives order data from HTML form submitted by user using POST method"
},
"typeVersion": 1
},
{
"id": "570f22fa-08aa-4b1f-8e84-de57f893f90f",
"name": "Nota adhesiva2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1220,
40
],
"parameters": {
"width": 640,
"height": 340,
"content": "- Provides webhook endpoint accessible from browser\n- Reads and retrieves product data from Google Sheets\n- Data is formatted and prepared for use in POS frontend or subsequent response."
},
"typeVersion": 1
},
{
"id": "a873b6fd-a22c-46c6-a6aa-0a88188df4e0",
"name": "Nota adhesiva3",
"type": "n8n-nodes-base.stickyNote",
"position": [
140,
-120
],
"parameters": {
"color": 4,
"width": 500,
"height": 340,
"content": "- Creates sales data in format suitable for Google Sheets.\n- Saves formatted sales results to sales sheet in Google Sheets file."
},
"typeVersion": 1
},
{
"id": "28e4314f-7883-4517-95f6-3653674085a2",
"name": "Nota adhesiva4",
"type": "n8n-nodes-base.stickyNote",
"position": [
140,
300
],
"parameters": {
"color": 5,
"width": 500,
"height": 420,
"content": "- Receives order data, converts it to business owner-friendly sales report (using LLM), then sends the report to Telegram."
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "5be9223c-a7a3-4295-a853-a7776c722d17",
"connections": {
"89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e": {
"main": [
[
{
"node": "c6129fa5-cd4f-4903-8522-dcbe4fcd50af",
"type": "main",
"index": 0
}
]
]
},
"da9ca9ef-1767-4fe9-aff2-9e15a7194fd8": {
"main": [
[
{
"node": "7e69a7e5-5a31-41b8-9ac2-bae76e807dd1",
"type": "main",
"index": 0
}
]
]
},
"7e69a7e5-5a31-41b8-9ac2-bae76e807dd1": {
"main": [
[
{
"node": "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e",
"type": "main",
"index": 0
},
{
"node": "2f766abf-2d22-4721-9942-247d5534b482",
"type": "main",
"index": 0
}
]
]
},
"84791b6d-a9b2-42b5-8c26-616c66adadd6": {
"main": [
[
{
"node": "ae33dc7d-b8c1-483d-a685-370c5b08e61f",
"type": "main",
"index": 0
}
]
]
},
"e8b420ed-e5cb-4835-9f9b-979bb9e44e05": {
"main": [
[
{
"node": "84791b6d-a9b2-42b5-8c26-616c66adadd6",
"type": "main",
"index": 0
}
]
]
},
"dff306ac-b6ad-4814-9ed2-2ffed96b420a": {
"main": [
[
{
"node": "da9ca9ef-1767-4fe9-aff2-9e15a7194fd8",
"type": "main",
"index": 0
}
]
]
},
"2f766abf-2d22-4721-9942-247d5534b482": {
"main": [
[
{
"node": "75d42049-e27a-4208-82ef-4a121812563e",
"type": "main",
"index": 0
}
]
]
},
"f4b29ac5-8dee-48bc-83a3-136795a447fa": {
"ai_languageModel": [
[
{
"node": "89ac1e1d-3a2d-47b7-b0c4-e0e06a90471e",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"ae33dc7d-b8c1-483d-a685-370c5b08e61f": {
"main": [
[
{
"node": "dff306ac-b6ad-4814-9ed2-2ffed96b420a",
"type": "main",
"index": 0
}
]
]
}
}
}¿Cómo usar este flujo de trabajo?
Copie el código de configuración JSON de arriba, cree un nuevo flujo de trabajo en su instancia de n8n y seleccione "Importar desde JSON", pegue la configuración y luego modifique la configuración de credenciales según sea necesario.
¿En qué escenarios es adecuado este flujo de trabajo?
Avanzado - CRM, Resumen de IA
¿Es de pago?
Este flujo de trabajo es completamente gratuito, puede importarlo y usarlo directamente. Sin embargo, tenga en cuenta que los servicios de terceros utilizados en el flujo de trabajo (como la API de OpenAI) pueden requerir un pago por su cuenta.
Flujos de trabajo relacionados recomendados
Budi SJ
@budisjI’m a Product Designer who also works as an Automation Developer. With a background in product design and systems thinking, I build user-centered workflows. My focus is on helping teams and businesses work more productively through impactful automation systems.
Compartir este flujo de trabajo