@receeco/pos-agent
Version:
Receeco POS Integration Middleware Agent
694 lines (610 loc) • 22.2 kB
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>