8
n8n 中文网amn8n.com

具有Telegram和Sheets实时更新的智能POS系统

高级

这是一个CRM, AI Summarization领域的自动化工作流,包含 16 个节点。主要使用 Code, Wait, Webhook, Telegram, GoogleSheets 等节点。 使用Web POS界面、AI报告、Telegram提醒和Sheets创建销售订单

前置要求
  • HTTP Webhook 端点(n8n 会自动生成)
  • Telegram Bot Token
  • Google Sheets API 凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "XRTJrZHlkDjGCLDq",
  "meta": {
    "instanceId": "0d045f8fe3802ff2be0bb9a9ea445ee6c9ed61973377effe00767e483681e2f4"
  },
  "name": "具有 Telegram 和 Sheets 实时更新的智能 POS 系统",
  "tags": [],
  "nodes": [
    {
      "id": "c6129fa5-cd4f-4903-8522-dcbe4fcd50af",
      "name": "发送文本消息",
      "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": "在表格中追加或更新行",
      "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": "启动 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": "获取产品数据",
      "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": "响应 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": "等待点击",
      "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": "响应点击",
      "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": "格式化表格数据",
      "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": "AI Agent",
      "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": "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": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1760,
        -440
      ],
      "parameters": {
        "width": 500,
        "height": 1320,
        "content": "# 具有 Telegram 和 Sheets 实时更新的智能 POS 系统"
      },
      "typeVersion": 1
    },
    {
      "id": "ae33dc7d-b8c1-483d-a685-370c5b08e61f",
      "name": "为 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": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -540,
        40
      ],
      "parameters": {
        "color": 2,
        "width": 640,
        "height": 340,
        "content": "- 以 HTML 格式创建 POS 界面"
      },
      "typeVersion": 1
    },
    {
      "id": "570f22fa-08aa-4b1f-8e84-de57f893f90f",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1220,
        40
      ],
      "parameters": {
        "width": 640,
        "height": 340,
        "content": "- 提供可从浏览器访问的 webhook 端点"
      },
      "typeVersion": 1
    },
    {
      "id": "a873b6fd-a22c-46c6-a6aa-0a88188df4e0",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        140,
        -120
      ],
      "parameters": {
        "color": 4,
        "width": 500,
        "height": 340,
        "content": "- 创建适合 Google Sheets 格式的销售数据。"
      },
      "typeVersion": 1
    },
    {
      "id": "28e4314f-7883-4517-95f6-3653674085a2",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        140,
        300
      ],
      "parameters": {
        "color": 5,
        "width": 500,
        "height": 420,
        "content": "- 接收订单数据,将其转换为业务所有者友好的销售报告(使用 LLM),然后将报告发送到 Telegram。"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5be9223c-a7a3-4295-a853-a7776c722d17",
  "connections": {
    "AI Agent": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for click": {
      "main": [
        [
          {
            "node": "Respond to click",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond to click": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          },
          {
            "node": "Format data for sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get products data": {
      "main": [
        [
          {
            "node": "Format data for webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start the webhook": {
      "main": [
        [
          {
            "node": "Get products data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond to Webhook": {
      "main": [
        [
          {
            "node": "Wait for click",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format data for sheet": {
      "main": [
        [
          {
            "node": "Append or update row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Format data for webhook": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 客户关系管理, AI 摘要总结

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量16
分类2
节点类型9
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Budi SJ

Budi SJ

@budisj

I’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.

外部链接
在 n8n.io 查看

分享此工作流