Système de point de vente intelligent avec des mises à jour en temps réel via Telegram et Sheets

Avancé

Ceci est unCRM, AI Summarizationworkflow d'automatisation du domainecontenant 16 nœuds.Utilise principalement des nœuds comme Code, Wait, Webhook, Telegram, GoogleSheets. Créer des commandes de vente en utilisant l'interface Web POS, les rapports IA, les notifications Telegram et Sheets

Prérequis
  • Point de terminaison HTTP Webhook (généré automatiquement par n8n)
  • Token Bot Telegram
  • Informations d'identification Google Sheets API

Catégorie

Aperçu du workflow
Visualisation des connexions entre les nœuds, avec support du zoom et du déplacement
Exporter le workflow
Copiez la configuration JSON suivante dans n8n pour importer et utiliser ce workflow
{
  "id": "XRTJrZHlkDjGCLDq",
  "meta": {
    "instanceId": "0d045f8fe3802ff2be0bb9a9ea445ee6c9ed61973377effe00767e483681e2f4"
  },
  "name": "Smart POS System with Live Updates to Telegram & Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "c6129fa5-cd4f-4903-8522-dcbe4fcd50af",
      "name": "Envoyer un message texte",
      "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": "Ajouter ou mettre à jour une ligne dans la feuille",
      "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": "Démarrer le 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": "Obtenir les données produits",
      "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": "Répondre au 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": "Attendre un 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": "Répondre au 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": "Formater les données pour la feuille",
      "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": "Agent 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": "Modèle 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": "Note autocollante",
      "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": "Formater les données pour 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": "Note autocollante1",
      "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": "Note autocollante2",
      "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": "Note autocollante3",
      "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": "Note autocollante4",
      "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
          }
        ]
      ]
    }
  }
}
Foire aux questions

Comment utiliser ce workflow ?

Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.

Dans quelles scénarios ce workflow est-il adapté ?

Avancé - CRM, Résumé IA

Est-ce payant ?

Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.

Informations sur le workflow
Niveau de difficulté
Avancé
Nombre de nœuds16
Catégorie2
Types de nœuds9
Description de la difficulté

Adapté aux utilisateurs avancés, avec des workflows complexes contenant 16+ nœuds

Auteur
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.

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34