UNPKG

@receeco/pos-agent

Version:

Receeco POS Integration Middleware Agent

694 lines (610 loc) 22.2 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Receeco POS Emulator</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 800px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); overflow: hidden; } .header { background: linear-gradient(135deg, #22c55e, #16a34a); color: white; padding: 30px; text-align: center; } .header h1 { font-size: 2.5rem; margin-bottom: 10px; } .header p { opacity: 0.9; font-size: 1.1rem; } .content { padding: 40px; } .form-group { margin-bottom: 25px; } label { display: block; margin-bottom: 8px; font-weight: 600; color: #374151; } input, select, textarea { width: 100%; padding: 12px 16px; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 16px; transition: border-color 0.2s; } input:focus, select:focus, textarea:focus { outline: none; border-color: #22c55e; } .items-section { background: #f9fafb; padding: 20px; border-radius: 8px; margin-bottom: 25px; } .item-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr auto; gap: 10px; align-items: center; margin-bottom: 15px; } .item-row:last-child { margin-bottom: 0; } .btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .btn-primary { background: #22c55e; color: white; } .btn-primary:hover { background: #16a34a; transform: translateY(-1px); } .btn-secondary { background: #6b7280; color: white; } .btn-danger { background: #ef4444; color: white; padding: 8px 12px; font-size: 14px; } .btn-add { background: #3b82f6; color: white; margin-top: 10px; } .total-section { background: #22c55e; color: white; padding: 20px; border-radius: 8px; text-align: center; margin-bottom: 25px; } .total-amount { font-size: 2rem; font-weight: bold; } .result { margin-top: 30px; padding: 20px; border-radius: 8px; display: none; } .result.success { background: #dcfce7; border: 2px solid #22c55e; color: #166534; } .result.error { background: #fef2f2; border: 2px solid #ef4444; color: #991b1b; } .qr-code { text-align: center; margin: 20px 0; } .qr-code img { max-width: 200px; border: 2px solid #e5e7eb; border-radius: 8px; } .short-code { font-size: 1.5rem; font-weight: bold; background: #f3f4f6; padding: 10px 20px; border-radius: 8px; display: inline-block; margin: 10px 0; } .loading { display: none; text-align: center; padding: 20px; } .spinner { border: 3px solid #f3f3f3; border-top: 3px solid #22c55e; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🧪 Receeco POS Emulator</h1> <p>Test your POS integration with simulated transactions</p> </div> <div class="content"> <form id="transactionForm"> <div class="form-group"> <label for="merchantId">Merchant ID</label> <input type="text" id="merchantId" value="" required /> <small style="color: #6b7280; font-size: 14px" >Will be loaded from .env file if available</small> </div> <div class="form-group"> <label for="merchantName">Store Name</label> <input type="text" id="merchantName" value="" required /> <small style="color: #6b7280; font-size: 14px" >Enter your business name</small> </div> <div class="items-section"> <h3 style="margin-bottom: 15px">Items</h3> <div id="itemsContainer"> <div class="item-row"> <input type="text" placeholder="Item name" class="item-name" value="Premium Rice (5kg)" required /> <input type="number" placeholder="Qty" class="item-quantity" value="1" min="1" required /> <input type="number" placeholder="Unit Price (₦)" class="item-price" value="8500" min="0" step="0.01" required /> <input type="number" placeholder="Total (₦)" class="item-total" value="8500" readonly /> <button type="button" class="btn btn-danger" onclick="removeItem(this)" > × </button> </div> </div> <button type="button" class="btn btn-add" onclick="addItem()"> + Add Item </button> </div> <div class="total-section"> <div>Total Amount</div> <div class="total-amount" id="totalAmount">₦8,500.00</div> </div> <div class="form-group"> <label for="paymentMethod">Payment Method</label> <select id="paymentMethod" required> <option value="card">Card</option> <option value="cash">Cash</option> <option value="transfer">Bank Transfer</option> <option value="mobile">Mobile Money</option> </select> </div> <div class="form-group"> <label for="apiUrl">POS Agent URL</label> <input type="url" id="apiUrl" value="http://localhost:3001" required /> </div> <button type="submit" class="btn btn-primary" style="width: 100%"> 🚀 Process Transaction </button> </form> <div class="loading" id="loading"> <div class="spinner"></div> <p>Processing transaction...</p> </div> <div class="result" id="result"> <div id="resultContent"></div> </div> </div> </div> <script> let itemCounter = 1; function addItem() { const container = document.getElementById("itemsContainer"); const itemRow = document.createElement("div"); itemRow.className = "item-row"; itemRow.innerHTML = ` <input type="text" placeholder="Item name" class="item-name" required> <input type="number" placeholder="Qty" class="item-quantity" value="1" min="1" required> <input type="number" placeholder="Unit Price (₦)" class="item-price" value="0" min="0" step="0.01" required> <input type="number" placeholder="Total (₦)" class="item-total" value="0" readonly> <button type="button" class="btn btn-danger" onclick="removeItem(this)">×</button> `; container.appendChild(itemRow); // Add event listeners for price calculation const quantityInput = itemRow.querySelector(".item-quantity"); const priceInput = itemRow.querySelector(".item-price"); const totalInput = itemRow.querySelector(".item-total"); function updateItemTotal() { const quantity = parseFloat(quantityInput.value) || 0; const price = parseFloat(priceInput.value) || 0; const total = quantity * price; totalInput.value = total.toFixed(2); updateGrandTotal(); } quantityInput.addEventListener("input", updateItemTotal); priceInput.addEventListener("input", updateItemTotal); } function removeItem(button) { const itemRow = button.closest(".item-row"); const container = document.getElementById("itemsContainer"); if (container.children.length > 1) { itemRow.remove(); updateGrandTotal(); } } function updateGrandTotal() { const itemTotals = document.querySelectorAll(".item-total"); let grandTotal = 0; itemTotals.forEach((input) => { grandTotal += parseFloat(input.value) || 0; }); document.getElementById( "totalAmount", ).textContent = `₦${ grandTotal.toLocaleString("en-NG", { minimumFractionDigits: 2, }) }`; } // Load configuration from POS agent async function loadConfiguration() { try { const apiUrl = document.getElementById("apiUrl").value; const response = await fetch(`${apiUrl}/config`); if (response.ok) { const config = await response.json(); // Pre-populate form with config data if (config.merchantId) { document.getElementById("merchantId").value = config.merchantId; } // Generate a unique store name based on merchant ID if ( config.merchantId && !document.getElementById("merchantName").value ) { const storeName = config.merchantId.startsWith("test-") ? `Test Store ${ config.merchantId .replace("test-", "") .toUpperCase() }` : `${config.merchantId} Store`; document.getElementById("merchantName").value = storeName; } console.log("✅ Configuration loaded:", config); // Show success message const merchantIdField = document.getElementById( "merchantId", ); merchantIdField.style.borderColor = "#22c55e"; setTimeout(() => { merchantIdField.style.borderColor = "#e5e7eb"; }, 2000); } else { console.warn( "⚠️ Could not load configuration from POS agent", ); // Use fallback values setFallbackValues(); } } catch (error) { console.warn( "⚠️ POS agent not running, using fallback values:", error.message, ); setFallbackValues(); } } function setFallbackValues() { const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 8); if (!document.getElementById("merchantId").value) { document.getElementById( "merchantId", ).value = `test-merchant-${randomId}`; } if (!document.getElementById("merchantName").value) { document.getElementById( "merchantName", ).value = `Emulator Test Store ${randomId.toUpperCase()}`; } } // Add event listeners to initial item document.addEventListener("DOMContentLoaded", function () { // Load configuration first loadConfiguration(); const initialQuantity = document.querySelector( ".item-quantity", ); const initialPrice = document.querySelector(".item-price"); function updateInitialTotal() { const quantity = parseFloat(initialQuantity.value) || 0; const price = parseFloat(initialPrice.value) || 0; const total = quantity * price; document.querySelector(".item-total").value = total.toFixed( 2, ); updateGrandTotal(); } initialQuantity.addEventListener("input", updateInitialTotal); initialPrice.addEventListener("input", updateInitialTotal); }); // Reset form function function resetForm() { // Hide result section document.getElementById("result").style.display = "none"; // Try to load configuration first, fallback to test values loadConfiguration().then(() => { // If no config was loaded, use fallback test values if (!document.getElementById("merchantId").value) { const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 8); document.getElementById("merchantId").value = `test-merchant-${randomId}`; document.getElementById("merchantName").value = `Test Store ${randomId.toUpperCase()}`; } }); document.getElementById("paymentMethod").selectedIndex = 0; // Reset items to single default item const itemsContainer = document.getElementById( "itemsContainer", ); itemsContainer.innerHTML = ` <div class="item-row"> <input type="text" placeholder="Item name" class="item-name" value="Premium Rice (5kg)" required> <input type="number" placeholder="Qty" class="item-quantity" value="1" min="1" required> <input type="number" placeholder="Unit Price (₦)" class="item-price" value="8500" min="0" step="0.01" required> <input type="number" placeholder="Total (₦)" class="item-total" value="8500" readonly> <button type="button" class="btn btn-danger" onclick="removeItem(this)">×</button> </div> `; // Re-add event listeners to the new initial item const initialQuantity = document.querySelector( ".item-quantity", ); const initialPrice = document.querySelector(".item-price"); function updateInitialTotal() { const quantity = parseFloat(initialQuantity.value) || 0; const price = parseFloat(initialPrice.value) || 0; const total = quantity * price; document.querySelector(".item-total").value = total.toFixed( 2, ); updateGrandTotal(); } initialQuantity.addEventListener("input", updateInitialTotal); initialPrice.addEventListener("input", updateInitialTotal); // Update grand total updateGrandTotal(); // Scroll to top of form document .getElementById("transactionForm") .scrollIntoView({ behavior: "smooth" }); } // Form submission document .getElementById("transactionForm") .addEventListener("submit", async function (e) { e.preventDefault(); const loading = document.getElementById("loading"); const result = document.getElementById("result"); const resultContent = document.getElementById( "resultContent", ); // Show loading loading.style.display = "block"; result.style.display = "none"; try { // Collect form data const merchantId = document.getElementById("merchantId").value; const merchantName = document.getElementById("merchantName").value; const paymentMethod = document.getElementById("paymentMethod").value; const apiUrl = document.getElementById("apiUrl").value; // Collect items const itemRows = document.querySelectorAll(".item-row"); const items = []; itemRows.forEach((row) => { const name = row.querySelector(".item-name").value; const quantity = parseInt( row.querySelector(".item-quantity").value, ); const unitPrice = parseFloat( row.querySelector(".item-price").value, ); const totalPrice = parseFloat( row.querySelector(".item-total").value, ); if (name && quantity && unitPrice) { items.push({ name, quantity, unitPrice, totalPrice, }); } }); // Calculate total const totalAmount = items.reduce( (sum, item) => sum + item.totalPrice, 0, ); // Generate unique test ID const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 8); // Create transaction object const transaction = { merchantId, merchantName, merchantLogo: "https://res.cloudinary.com/dmmz1qe2d/image/upload/v1752945022/receeco_icon_at5wtn.png", accentColor: "#16a34a", items, totalAmount, currency: "NGN", paymentMethod, location: `${merchantName} - Test Location`, timestamp: new Date().toISOString(), cashierId: "EMULATOR001", terminalId: "TERMINAL001", testId: `emulator-${timestamp}-${randomId}`, }; // Send to POS agent const response = await fetch(`${apiUrl}/transactions`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(transaction), }); const data = await response.json(); if (response.ok) { // Success result.className = "result success"; resultContent.innerHTML = ` <h3>✅ Transaction Successful!</h3> <div class="qr-code"> <img src="${data.qrCodeUrl}" alt="QR Code" /> </div> <div> <strong>Short Code:</strong> <div class="short-code">${data.shortCode}</div> </div> <p><strong>Receipt URL:</strong> <a href="${data.receiptUrl}" target="_blank">${data.receiptUrl}</a></p> <p><strong>Transaction ID:</strong> ${ data.transactionId || "N/A" }</p> <hr style="margin: 20px 0;"> <h4>💡 Next Steps:</h4> <ol> <li>Visit: <a href="https://receeco.com/app/scan" target="_blank">https://receeco.com/app/scan</a></li> <li>Enter short code: <strong>${data.shortCode}</strong></li> <li>View your receipt!</li> </ol> <div style="margin-top: 20px;"> <button type="button" class="btn btn-primary" onclick="resetForm()" style="width: 100%;"> 🔄 Process New Transaction </button> </div> `; } else { throw new Error(data.error || "Transaction failed"); } } catch (error) { // Error result.className = "result error"; resultContent.innerHTML = ` <h3>❌ Transaction Failed</h3> <p><strong>Error:</strong> ${error.message}</p> <p>Please check:</p> <ul> <li>POS Agent is running at the specified URL</li> <li>All required fields are filled</li> <li>Network connection is stable</li> </ul> `; } finally { // Hide loading and show result loading.style.display = "none"; result.style.display = "block"; } }); </script> </body> </html>