correcting customer display
This commit is contained in:
parent
49c4d4dab1
commit
73951729f9
BIN
assets/pasted-20260206-044350-8c65cfaa.png
Normal file
BIN
assets/pasted-20260206-044350-8c65cfaa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
Binary file not shown.
@ -1,277 +1,320 @@
|
|||||||
{% load static i18n l10n %}
|
{% load static i18n %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% trans "Customer Display" %} | {{ settings.business_name }}</title>
|
<title>{% trans "Customer Display" %}</title>
|
||||||
<!-- Bootstrap CSS -->
|
|
||||||
<link rel="stylesheet" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&family=Plus+Jakarta+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
{% if LANGUAGE_CODE == 'ar' %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.rtl.min.css">
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
|
||||||
--bs-primary-rgb: 13, 110, 253;
|
|
||||||
}
|
|
||||||
body {
|
body {
|
||||||
background-color: #f8f9fa;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.main-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.left-panel {
|
|
||||||
flex: 1;
|
|
||||||
padding: 2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-right: 1px solid #dee2e6;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
.right-panel {
|
|
||||||
width: 40%;
|
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
padding: 2rem;
|
font-size: 0.85rem; /* Reduced font size further */
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
.cart-list {
|
#welcome-screen {
|
||||||
flex: 1;
|
position: fixed;
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
.cart-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
border-bottom: 1px solid #f1f3f5;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
.cart-item.new-item {
|
|
||||||
animation: highlight 1s ease-out;
|
|
||||||
}
|
|
||||||
@keyframes highlight {
|
|
||||||
0% { background-color: rgba(var(--bs-primary-rgb), 0.1); }
|
|
||||||
100% { background-color: transparent; }
|
|
||||||
}
|
|
||||||
.item-name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
}
|
|
||||||
.item-meta {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
.item-price {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
.total-display {
|
|
||||||
background: var(--bs-primary);
|
|
||||||
color: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: auto;
|
|
||||||
box-shadow: 0 10px 20px rgba(var(--bs-primary-rgb), 0.2);
|
|
||||||
}
|
|
||||||
.welcome-screen {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: white;
|
background: white;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#main-display {
|
||||||
|
height: 100vh;
|
||||||
|
display: none; /* Hidden by default until data arrives */
|
||||||
|
}
|
||||||
|
.cart-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0.25rem; /* Reduced margin */
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
padding: 0.5rem; /* Reduced padding */
|
||||||
|
}
|
||||||
|
.total-section {
|
||||||
|
background: white;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem; /* Reduced padding */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
}
|
}
|
||||||
/* UPDATED: display: none is safer than opacity for hiding overlay */
|
.grand-total {
|
||||||
.welcome-screen.hidden {
|
font-size: 2rem; /* Slightly smaller grand total */
|
||||||
display: none !important;
|
font-weight: bold;
|
||||||
|
color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Debug Panel */
|
||||||
|
#debugStatus {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(0,0,0,0.8);
|
||||||
|
color: lime;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
z-index: 2000;
|
||||||
|
pointer-events: auto;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
.debug-btn {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin-left: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #444;
|
||||||
|
border: 1px solid #666;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#debugLog {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 40px;
|
||||||
|
right: 10px;
|
||||||
|
width: 300px;
|
||||||
|
height: 200px;
|
||||||
|
background: rgba(0,0,0,0.9);
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 2000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Welcome / Idle Screen -->
|
<!-- Welcome Screen -->
|
||||||
<div id="welcomeScreen" class="welcome-screen">
|
<div id="welcome-screen">
|
||||||
<div class="text-center">
|
<div class="mb-4">
|
||||||
{% if settings.logo %}
|
{% if settings.logo %}
|
||||||
<img src="{{ settings.logo.url }}" alt="Logo" class="mb-4" style="max-height: 150px;">
|
<img src="{{ settings.logo.url }}" alt="Logo" style="max-height: 100px;" class="mb-3">
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="bi bi-shop display-1 text-primary mb-4"></i>
|
<i class="bi bi-cart3 text-primary display-4 mb-3"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1 class="display-4 fw-bold text-primary">{{ settings.business_name }}</h1>
|
<h2 class="fw-bold">{{ settings.business_name }}</h2>
|
||||||
<p class="lead text-muted mt-3">{% trans "Welcome! We are ready to serve you." %}</p>
|
<p class="text-muted">{% trans "Welcome! Please wait while we prepare your order." %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Diagnostic Info for User -->
|
||||||
|
<div class="mt-4 text-muted small">
|
||||||
|
<div id="connection-status">Waiting for POS...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Active Transaction Screen -->
|
<!-- Main Transaction Display -->
|
||||||
<div class="main-container">
|
<div id="main-display" class="container-fluid p-0">
|
||||||
<!-- Left: Cart Items -->
|
<div class="row g-0 h-100">
|
||||||
<div class="left-panel">
|
<!-- Left: Item List -->
|
||||||
<h4 class="fw-bold mb-4 border-bottom pb-3">{% trans "Your Order" %}</h4>
|
<div class="col-8 h-100 bg-light p-3 overflow-auto">
|
||||||
<div class="cart-list" id="cartList">
|
<h5 class="mb-3 fw-bold">{% trans "Your Order" %}</h5>
|
||||||
<!-- Items injected via JS -->
|
<div id="cart-items-container">
|
||||||
</div>
|
<!-- Items injected here -->
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right: Totals & Ads -->
|
|
||||||
<div class="right-panel">
|
|
||||||
<div class="mb-auto w-100">
|
|
||||||
{% if settings.logo %}
|
|
||||||
<img src="{{ settings.logo.url }}" alt="Logo" class="mb-3" style="max-height: 80px;">
|
|
||||||
{% endif %}
|
|
||||||
<h5 class="fw-bold">{{ settings.business_name }}</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Big Total Box -->
|
|
||||||
<div class="total-display">
|
|
||||||
<div class="d-flex justify-content-between align-items-end mb-2">
|
|
||||||
<span class="h5 mb-0 opacity-75">{% trans "Total" %}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="display-3 fw-bold" id="totalAmount">{{ settings.currency_symbol }} 0.000</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 pt-3 border-top border-white border-opacity-25 d-flex justify-content-between">
|
<!-- Right: Totals -->
|
||||||
<span class="small">{% trans "Items" %}: <span id="itemCount">0</span></span>
|
<div class="col-4 h-100 shadow-lg">
|
||||||
<span class="small">{% trans "Tax" %}: <span id="taxAmount">0.000</span></span>
|
<div class="total-section">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">{% trans "Subtotal" %}</span>
|
||||||
|
<span id="display-subtotal" class="fw-bold">0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="text-muted">{% trans "VAT" %}</span>
|
||||||
|
<span id="display-tax" class="fw-bold">0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-3 text-success">
|
||||||
|
<span>{% trans "Discount" %}</span>
|
||||||
|
<span id="display-discount">-0.00</span>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<div class="text-uppercase text-muted small fw-bold mb-1">{% trans "Total to Pay" %}</div>
|
||||||
|
<div id="display-total" class="grand-total">0.00</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Debug Status (z-index increased to 2000) -->
|
<!-- Debug Overlay -->
|
||||||
<div id="debugStatus" style="position: fixed; bottom: 5px; right: 5px; font-size: 11px; color: #555; background: rgba(255,255,200,0.9); padding: 5px 10px; border-radius: 5px; border: 1px solid #ccc; z-index: 2000; font-family: monospace;">Init (v3)...</div>
|
<div id="debugStatus">
|
||||||
|
<span id="statusText">Init (v10.1 - Standalone/Compact)...</span>
|
||||||
|
<button class="debug-btn" onclick="runTestRender()">Test Render</button>
|
||||||
|
<button class="debug-btn" onclick="toggleLog()">Log</button>
|
||||||
|
</div>
|
||||||
|
<div id="debugLog"></div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const currencySymbol = '{{ settings.currency_symbol|escapejs }}';
|
const currency = '{{ site_settings.currency_symbol|escapejs }}';
|
||||||
const welcomeScreen = document.getElementById('welcomeScreen');
|
|
||||||
const cartList = document.getElementById('cartList');
|
|
||||||
const totalEl = document.getElementById('totalAmount');
|
|
||||||
const countEl = document.getElementById('itemCount');
|
|
||||||
const taxEl = document.getElementById('taxAmount');
|
|
||||||
const debugEl = document.getElementById('debugStatus');
|
|
||||||
|
|
||||||
function updateStatus(msg) {
|
// Elements
|
||||||
console.log('[CFD] ' + msg);
|
const welcomeScreen = document.getElementById('welcome-screen');
|
||||||
debugEl.innerText = msg + ' (' + new Date().toLocaleTimeString() + ')';
|
const mainDisplay = document.getElementById('main-display');
|
||||||
|
const itemsContainer = document.getElementById('cart-items-container');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
const logPanel = document.getElementById('debugLog');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// Logging & Debug
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
function log(msg) {
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
console.log(`[CFD ${time}] ${msg}`);
|
||||||
|
logPanel.innerHTML += `<div>[${time}] ${msg}</div>`;
|
||||||
|
logPanel.scrollTop = logPanel.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMessage(event) {
|
function toggleLog() {
|
||||||
const data = event.data;
|
logPanel.style.display = logPanel.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(msg) {
|
||||||
|
statusText.innerText = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// Rendering Logic
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
function updateDisplay(data) {
|
||||||
|
// Hide welcome, show main
|
||||||
|
welcomeScreen.style.display = 'none';
|
||||||
|
mainDisplay.style.display = 'block';
|
||||||
|
|
||||||
|
if (!data.cart || data.cart.length === 0) {
|
||||||
|
if (data.type === 'clear') {
|
||||||
|
log('Cart cleared');
|
||||||
|
welcomeScreen.style.display = 'flex';
|
||||||
|
mainDisplay.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Items
|
||||||
|
let html = '';
|
||||||
|
data.cart.forEach(item => {
|
||||||
|
html += `
|
||||||
|
<div class="cart-item d-flex justify-content-between align-items-center animate__animated animate__fadeIn">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width:25px; height:25px; font-weight:bold; font-size: 0.8rem;">
|
||||||
|
${item.quantity}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold text-dark" style="font-size: 0.9rem;">${item.name_ar}</div>
|
||||||
|
<div class="text-muted small" style="font-size: 0.75rem;">${item.name_en}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fw-bold" style="font-size: 0.9rem;">
|
||||||
|
${currency} ${(item.price * item.quantity).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
itemsContainer.innerHTML = html;
|
||||||
|
|
||||||
|
// Render Totals
|
||||||
|
document.getElementById('display-subtotal').innerText = currency + ' ' + (data.totals.subtotal).toFixed(3);
|
||||||
|
document.getElementById('display-tax').innerText = currency + ' ' + (data.totals.tax).toFixed(3);
|
||||||
|
document.getElementById('display-discount').innerText = '- ' + currency + ' ' + (data.totals.discount).toFixed(3);
|
||||||
|
document.getElementById('display-total').innerText = currency + ' ' + (data.totals.total).toFixed(3);
|
||||||
|
|
||||||
|
setStatus('Sync: ' + new Date().toLocaleTimeString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// Communication Logic
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
let lastProcessedTimestamp = 0;
|
||||||
|
|
||||||
|
function handleMessage(data) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
updateStatus('Msg: ' + (data.type || 'unknown'));
|
// Dedup based on timestamp if provided
|
||||||
|
if (data.timestamp && data.timestamp <= lastProcessedTimestamp) {
|
||||||
if (data.type === 'update_cart') {
|
|
||||||
renderCart(data.cart, data.totals);
|
|
||||||
} else if (data.type === 'clear') {
|
|
||||||
showWelcome();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. BroadcastChannel (Cross-tab)
|
|
||||||
const channel = new BroadcastChannel('pos_channel');
|
|
||||||
channel.onmessage = handleMessage;
|
|
||||||
|
|
||||||
// 2. Direct Window Message (Parent-Child)
|
|
||||||
window.addEventListener('message', handleMessage);
|
|
||||||
|
|
||||||
// 3. LocalStorage (Cross-window backup)
|
|
||||||
window.addEventListener('storage', (event) => {
|
|
||||||
if (event.key === 'pos_cart_update' && event.newValue) {
|
|
||||||
updateStatus('Storage Event');
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(event.newValue);
|
|
||||||
handleMessage({ data: msg });
|
|
||||||
} catch(e) { updateStatus('Storage Parse Error'); }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial load: Check storage
|
|
||||||
const stored = localStorage.getItem('pos_cart_update');
|
|
||||||
if (stored) {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(stored);
|
|
||||||
updateStatus('Restored Storage');
|
|
||||||
// Small delay to ensure DOM is ready
|
|
||||||
setTimeout(() => handleMessage({ data: msg }), 100);
|
|
||||||
} catch(e) {
|
|
||||||
updateStatus('Storage Error');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateStatus('No Storage Data');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handshake: Ask POS for current state
|
|
||||||
setTimeout(() => {
|
|
||||||
updateStatus('Requesting Sync...');
|
|
||||||
try {
|
|
||||||
channel.postMessage({ type: 'new_client' });
|
|
||||||
} catch(e) {
|
|
||||||
updateStatus('BC Error');
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
function renderCart(cart, totals) {
|
|
||||||
updateStatus('Render: ' + (cart ? cart.length : 0));
|
|
||||||
|
|
||||||
if (!cart || cart.length === 0) {
|
|
||||||
showWelcome();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data.timestamp) lastProcessedTimestamp = data.timestamp;
|
||||||
|
|
||||||
welcomeScreen.classList.add('hidden');
|
log('New Data: ' + data.type);
|
||||||
cartList.innerHTML = '';
|
if (data.type === 'update_cart') {
|
||||||
|
updateDisplay(data);
|
||||||
cart.forEach(item => {
|
} else if (data.type === 'clear') {
|
||||||
const div = document.createElement('div');
|
welcomeScreen.style.display = 'flex';
|
||||||
div.className = 'cart-item';
|
mainDisplay.style.display = 'none';
|
||||||
div.innerHTML = `
|
setStatus('Cleared');
|
||||||
<div>
|
|
||||||
<div class="item-name">${item.name_ar} / ${item.name_en}</div>
|
|
||||||
<div class="item-meta">${item.quantity} x ${parseFloat(item.price).toFixed(3)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="item-price">${parseFloat(item.line_total).toFixed(3)}</div>
|
|
||||||
`;
|
|
||||||
cartList.appendChild(div);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Scroll to bottom
|
|
||||||
cartList.scrollTop = cartList.scrollHeight;
|
|
||||||
|
|
||||||
// Update Totals
|
|
||||||
if (totals) {
|
|
||||||
totalEl.innerText = `${currencySymbol} ${parseFloat(totals.total).toFixed(3)}`;
|
|
||||||
taxEl.innerText = parseFloat(totals.tax).toFixed(3);
|
|
||||||
// Calculate item count
|
|
||||||
const count = cart.reduce((acc, item) => acc + parseFloat(item.quantity), 0);
|
|
||||||
countEl.innerText = count;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWelcome() {
|
// Server Sync Polling (v8)
|
||||||
updateStatus('State: Welcome');
|
// The most reliable method when windows are isolated
|
||||||
welcomeScreen.classList.remove('hidden');
|
setInterval(() => {
|
||||||
cartList.innerHTML = '';
|
fetch('/api/pos/sync/state/')
|
||||||
totalEl.innerText = `${currencySymbol} 0.000`;
|
.then(res => res.json())
|
||||||
countEl.innerText = '0';
|
.then(data => {
|
||||||
taxEl.innerText = '0.000';
|
if (data.status === 'empty') return;
|
||||||
}
|
|
||||||
|
// If we got data, check timestamp
|
||||||
|
if (data.timestamp && data.timestamp > lastProcessedTimestamp) {
|
||||||
|
handleMessage(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// silent fail, maybe offline
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also keep checking LocalStorage as a backup for same-origin speed
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('pos_cart_update');
|
||||||
|
if (raw) {
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
if (data.timestamp > lastProcessedTimestamp) handleMessage(data);
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// Diagnostics
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
window.runTestRender = function() {
|
||||||
|
log('Running Test Render');
|
||||||
|
const testData = {
|
||||||
|
type: 'update_cart',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
cart: [
|
||||||
|
{ name_en: 'Test Item A', name_ar: 'منتج تجريبي أ', quantity: 2, price: 10.00 },
|
||||||
|
{ name_en: 'Test Item B', name_ar: 'منتج تجريبي ب', quantity: 1, price: 5.50 }
|
||||||
|
],
|
||||||
|
totals: { subtotal: 25.50, tax: 3.825, discount: 0, total: 29.325 }
|
||||||
|
};
|
||||||
|
updateDisplay(testData);
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -5,65 +5,276 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
.product-card {
|
/* Full Screen POS Layout Overrides */
|
||||||
cursor: pointer;
|
:root {
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
--pos-cart-width: 420px;
|
||||||
border: none;
|
--pos-bg: #f1f5f9;
|
||||||
border-radius: 12px;
|
--pos-card-bg: #ffffff;
|
||||||
|
--pos-accent: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide default main padding to allow full-screen layout */
|
||||||
|
main {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: calc(100vh - 76px); /* Approx navbar height */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.product-card:hover {
|
|
||||||
transform: translateY(-3px);
|
/* In case sidebar is present, ensure we handle it */
|
||||||
box-shadow: 0 8px 15px rgba(0,0,0,0.1) !important;
|
#content {
|
||||||
}
|
height: 100vh;
|
||||||
.cart-container {
|
|
||||||
position: sticky;
|
|
||||||
top: 80px;
|
|
||||||
height: calc(100vh - 120px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.cart-items {
|
|
||||||
flex-grow: 1;
|
/* POS Container */
|
||||||
overflow-y: auto;
|
.pos-layout {
|
||||||
}
|
display: flex;
|
||||||
.category-badge {
|
height: 100%;
|
||||||
cursor: pointer;
|
width: 100%;
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
margin-right: 4px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: inline-block;
|
|
||||||
background: #f1f3f5;
|
|
||||||
color: #495057;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
.category-badge.active {
|
|
||||||
background: var(--bs-primary);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--bs-primary);
|
|
||||||
}
|
|
||||||
.product-name {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
height: auto;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-method-btn.active {
|
/* Left Side: Products */
|
||||||
background-color: var(--bs-primary);
|
.pos-products-section {
|
||||||
color: white;
|
flex: 1;
|
||||||
border-color: var(--bs-primary);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--pos-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loyalty Styles */
|
.pos-products-header {
|
||||||
.loyalty-badge {
|
padding: 1rem 1.5rem;
|
||||||
font-size: 0.7rem;
|
background: white;
|
||||||
padding: 2px 8px;
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
border-radius: 10px;
|
display: flex;
|
||||||
font-weight: bold;
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-categories-bar {
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.pos-categories-bar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-products-grid-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Product Card Styling */
|
||||||
|
.product-card {
|
||||||
|
background: var(--pos-card-bg);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0,0,0,0.08) !important;
|
||||||
|
border-color: rgba(var(--bs-primary-rgb), 0.2);
|
||||||
|
}
|
||||||
|
.product-card:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
.product-image-container {
|
||||||
|
height: 120px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f8f9fa;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.product-image-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
.product-card:hover .product-image-container img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.product-price-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--pos-accent);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Side: Cart */
|
||||||
|
.pos-cart-section {
|
||||||
|
width: var(--pos-cart-width);
|
||||||
|
background: white;
|
||||||
|
border-left: 1px solid rgba(0,0,0,0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: -5px 0 25px rgba(0,0,0,0.03);
|
||||||
|
z-index: 100;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-cart-header {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||||
|
background: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-cart-items-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-item-row {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.cart-item-row:hover {
|
||||||
|
border-color: var(--bs-primary);
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-cart-footer {
|
||||||
|
background: #fff;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.08);
|
||||||
|
box-shadow: 0 -5px 20px rgba(0,0,0,0.02);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Badges (Pill Style) */
|
||||||
|
.nav-pills .nav-link {
|
||||||
|
color: #6c757d;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.nav-pills .nav-link.active {
|
||||||
|
background-color: var(--pos-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--pos-accent);
|
||||||
|
box-shadow: 0 4px 10px rgba(var(--bs-primary-rgb), 0.3);
|
||||||
|
}
|
||||||
|
.nav-pills .nav-link:hover:not(.active) {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Styling */
|
||||||
|
.search-input {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50px;
|
||||||
|
padding-left: 1.25rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: var(--pos-accent);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 4px rgba(var(--bs-primary-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
.pos-cart-section {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
z-index: 1050;
|
||||||
|
}
|
||||||
|
.pos-cart-section.show {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .pos-cart-section {
|
||||||
|
right: auto;
|
||||||
|
left: -100%;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .pos-cart-section.show {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
height: calc(100vh - 60px); /* Smaller navbar on mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quantity Control Buttons */
|
||||||
|
.qty-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #495057;
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
.qty-btn:hover {
|
||||||
|
background: var(--pos-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--pos-accent);
|
||||||
|
}
|
||||||
|
.qty-btn:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Invoice Print Styles */
|
/* Invoice Print Styles */
|
||||||
@ -91,13 +302,6 @@
|
|||||||
.no-print {
|
.no-print {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
#sidebar, .top-navbar, .btn, .modal {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#invoice-print {
|
#invoice-print {
|
||||||
@ -122,158 +326,188 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid px-2 px-md-4 no-print">
|
<div class="pos-layout no-print">
|
||||||
<div class="row g-3 g-lg-4">
|
<!-- LEFT PANEL: Products & Search -->
|
||||||
<!-- Products Section -->
|
<div class="pos-products-section">
|
||||||
<div class="col-lg-8">
|
<!-- Top Bar: Title & Actions -->
|
||||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
|
<div class="pos-products-header justify-content-between">
|
||||||
|
<div>
|
||||||
<h4 class="fw-bold mb-0">{% trans "Point of Sale" %}</h4>
|
<h4 class="fw-bold mb-0">{% trans "Point of Sale" %}</h4>
|
||||||
<div class="ms-3 d-flex gap-2">
|
<small class="text-muted">{% now "l, j F Y" %}</small>
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="openCustomerDisplay()" title="{% trans 'Open Customer Screen' %}">
|
</div>
|
||||||
<i class="bi bi-display"></i> <span class="d-none d-md-inline">{% trans "Customer Screen" %}</span>
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-light border shadow-sm rounded-pill px-3" onclick="openCustomerDisplay()" title="{% trans 'Open Customer Screen' %}">
|
||||||
|
<i class="bi bi-display text-primary"></i> <span class="d-none d-md-inline ms-1 small fw-bold">{% trans "Screen" %}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-warning" onclick="broadcastCartUpdate()" title="{% trans 'Force Sync' %}">
|
<button class="btn btn-light border shadow-sm rounded-pill px-3" onclick="broadcastCartUpdate()" title="{% trans 'Force Sync' %}">
|
||||||
<i class="bi bi-arrow-repeat"></i> <span class="d-none d-md-inline">{% trans "Sync" %}</span>
|
<i class="bi bi-arrow-repeat text-warning"></i> <span class="d-none d-md-inline ms-1 small fw-bold">{% trans "Sync" %}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group w-100 w-md-50">
|
</div>
|
||||||
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span>
|
|
||||||
<input type="text" id="productSearch" class="form-control border-start-0 shadow-none" placeholder="{% trans 'Search products...' %}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4 overflow-auto text-nowrap pb-2">
|
<!-- Search & Filter Bar -->
|
||||||
<div class="category-badge active" data-category="all">{% trans "All" %}</div>
|
<div class="pos-products-header pt-0 border-bottom-0 pb-2">
|
||||||
|
<div class="position-relative w-100">
|
||||||
|
<i class="bi bi-search position-absolute top-50 start-0 translate-middle-y ms-3 text-muted"></i>
|
||||||
|
<input type="text" id="productSearch" class="form-control form-control-lg search-input ps-5 shadow-sm" placeholder="{% trans 'Search products by name or barcode...' %}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categories Strip -->
|
||||||
|
<div class="pos-categories-bar">
|
||||||
|
<div class="nav nav-pills flex-nowrap" id="categoryTabs" role="tablist">
|
||||||
|
<button class="nav-link active category-badge" data-category="all">{% trans "All Items" %}</button>
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<div class="category-badge" data-category="{{ category.id }}">
|
<button class="nav-link category-badge" data-category="{{ category.id }}">
|
||||||
{{ category.name_ar }} / {{ category.name_en }}
|
{{ category.name_ar }} / {{ category.name_en }}
|
||||||
</div>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-2" id="productGrid">
|
<!-- Scrollable Product Grid -->
|
||||||
|
<div class="pos-products-grid-container custom-scrollbar">
|
||||||
|
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-3" id="productGrid">
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
<div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}">
|
<div class="col product-item" data-category="{{ product.category.id }}" data-name-en="{{ product.name_en|lower }}" data-name-ar="{{ product.name_ar }}">
|
||||||
<div class="card h-100 shadow-sm product-card p-1" onclick="addToCart({{ product.id|unlocalize }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price|unlocalize }}, {{ product.vat|default:site_settings.tax_rate|default:0|unlocalize }})">
|
<div class="product-card shadow-sm" onclick="addToCart({{ product.id|unlocalize }}, '{{ product.name_en|escapejs }}', '{{ product.name_ar|escapejs }}', {{ product.price|unlocalize }}, {{ product.vat|default:site_settings.tax_rate|default:0|unlocalize }})">
|
||||||
{% if product.image %}
|
<div class="product-image-container">
|
||||||
<img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 80px; object-fit: cover;">
|
{% if product.image %}
|
||||||
{% else %}
|
<img src="{{ product.image.url }}" alt="{{ product.name_en }}">
|
||||||
<div class="bg-light rounded-3 d-flex align-items-center justify-content-center" style="height: 80px;">
|
{% else %}
|
||||||
<i class="bi bi-image text-muted opacity-25" style="font-size: 1.5rem;"></i>
|
<div class="d-flex align-items-center justify-content-center h-100 bg-light text-muted">
|
||||||
</div>
|
<i class="bi bi-image fs-1 opacity-25"></i>
|
||||||
{% endif %}
|
|
||||||
<div class="card-body p-1 text-center">
|
|
||||||
<div class="product-name fw-bold mb-1">
|
|
||||||
<div dir="rtl">{{ product.name_ar }}</div>
|
|
||||||
<div class="small text-muted fw-normal" style="font-size: 0.65rem;">{{ product.name_en }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-primary fw-bold mb-0" style="font-size: 0.8rem;">{{ site_settings.currency_symbol }}{{ product.price|floatformat:decimal_places }}</p>
|
{% endif %}
|
||||||
<small class="text-muted d-block" style="font-size: 0.65rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{% trans "Stock" %}: {{ product.stock_quantity }}</small>
|
</div>
|
||||||
|
<div class="product-price-badge">
|
||||||
|
{{ site_settings.currency_symbol }}{{ product.price|floatformat:decimal_places }}
|
||||||
|
</div>
|
||||||
|
<div class="p-3 text-center">
|
||||||
|
<h6 class="fw-bold mb-1 text-truncate" dir="rtl">{{ product.name_ar }}</h6>
|
||||||
|
<small class="text-muted d-block text-truncate mb-2">{{ product.name_en }}</small>
|
||||||
|
<span class="badge bg-light text-secondary border rounded-pill fw-normal">{% trans "Stock" %}: {{ product.stock_quantity }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Cart Section -->
|
<!-- RIGHT PANEL: Cart -->
|
||||||
<div class="col-lg-4">
|
<div class="pos-cart-section" id="posCart">
|
||||||
<div class="card border-0 shadow-sm rounded-4 cart-container" id="posCart">
|
<!-- Cart Header -->
|
||||||
<div class="card-header bg-white border-0 pt-4 px-4 d-flex justify-content-between align-items-center">
|
<div class="pos-cart-header d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<button class="btn btn-link d-lg-none text-dark p-0 me-3" onclick="toggleMobileCart()">
|
<button class="btn btn-link d-lg-none text-dark p-0 me-3" onclick="toggleMobileCart()">
|
||||||
<i class="bi bi-chevron-left fs-4"></i>
|
<i class="bi bi-chevron-left fs-4"></i>
|
||||||
</button>
|
</button>
|
||||||
<h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5>
|
<h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 align-items-center">
|
<div>
|
||||||
<button class="btn btn-sm btn-outline-warning shadow-none position-relative" onclick="loadHeldSales()" data-bs-toggle="modal" data-bs-target="#heldSalesModal">
|
<button class="btn btn-sm btn-light text-danger border-0 rounded-pill px-3" onclick="clearCart()">
|
||||||
<i class="bi bi-clock-history"></i>
|
<i class="bi bi-trash me-1"></i> {% trans "Clear" %}
|
||||||
<span id="heldCountBadge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger d-none">0</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<button class="btn btn-sm btn-link text-danger text-decoration-none p-0" onclick="clearCart()">
|
</div>
|
||||||
<i class="bi bi-trash"></i> {% trans "Clear" %}
|
|
||||||
</button>
|
<!-- Customer Selector (Fixed at top of cart) -->
|
||||||
|
<div class="px-4 py-3 border-bottom bg-light">
|
||||||
|
<div class="d-flex gap-2 mb-2">
|
||||||
|
<div class="position-relative w-100">
|
||||||
|
<input type="hidden" id="customerSelect" value="" onchange="onCustomerChange()">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-white border-end-0 rounded-start-pill ps-3"><i class="bi bi-person text-muted"></i></span>
|
||||||
|
<input type="text" id="customerSearchInput" class="form-control border-start-0 rounded-end-pill shadow-none" placeholder="{% trans 'Customer...' %}" autocomplete="off">
|
||||||
|
<button class="btn btn-link position-absolute end-0 top-50 translate-middle-y text-muted text-decoration-none z-3 pe-3" type="button" onclick="clearCustomerSelection()" id="clearCustomerBtn" style="display:none;"><i class="bi bi-x-circle-fill"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="customerSearchResults" class="list-group position-absolute w-100 shadow-lg mt-1 rounded-3 overflow-hidden d-none" style="z-index: 1050; max-height: 250px; overflow-y: auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-primary shadow-sm rounded-circle d-flex align-items-center justify-content-center" style="width: 38px; height: 38px;" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
||||||
|
<i class="bi bi-plus-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="px-4 mt-2">
|
<!-- Loyalty Info -->
|
||||||
<div class="d-flex gap-2 mb-2">
|
<div id="loyaltyInfo" class="d-none mt-2 p-2 bg-white rounded-3 border border-warning-subtle d-flex justify-content-between align-items-center">
|
||||||
<div class="position-relative w-100"><input type="hidden" id="customerSelect" value="" onchange="onCustomerChange()"><div class="input-group input-group-sm"><span class="input-group-text bg-white border-end-0"><i class="bi bi-search"></i></span><input type="text" id="customerSearchInput" class="form-control border-start-0 shadow-none" placeholder="{% trans 'Search Name / Phone...' %}" autocomplete="off"><button class="btn btn-outline-secondary border-start-0" type="button" onclick="clearCustomerSelection()" id="clearCustomerBtn" style="display:none;"><i class="bi bi-x"></i></button></div><div id="customerSearchResults" class="list-group position-absolute w-100 shadow-sm d-none" style="z-index: 1050; max-height: 250px; overflow-y: auto; top: 100%;"></div></div>
|
<div class="d-flex align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-outline-primary shadow-none" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
<i class="bi bi-star-fill text-warning"></i>
|
||||||
<i class="bi bi-person-plus"></i>
|
<span id="loyaltyTierBadge" class="badge bg-warning text-dark rounded-pill"></span>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="small fw-bold">
|
||||||
|
<span id="loyaltyPointsDisplay">0</span> {% trans "Pts" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Loyalty Info Display -->
|
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||||
<div id="loyaltyInfo" class="d-none bg-light p-2 rounded-3 mb-2 animate__animated animate__fadeIn">
|
<button class="btn btn-sm btn-outline-secondary border-0 small py-0" onclick="loadHeldSales()" data-bs-toggle="modal" data-bs-target="#heldSalesModal">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<i class="bi bi-clock-history me-1"></i> {% trans "Held Orders" %}
|
||||||
<span class="small fw-bold text-muted"><i class="bi bi-star-fill text-warning me-1"></i> {% trans "Loyalty" %}</span>
|
<span id="heldCountBadge" class="badge bg-danger rounded-pill ms-1 d-none">0</span>
|
||||||
<span id="loyaltyTierBadge" class="loyalty-badge text-white"></span>
|
</button>
|
||||||
</div>
|
<div id="syncIndicator" class="sync-indicator badge bg-success-subtle text-success border border-success-subtle rounded-pill">
|
||||||
<div class="d-flex justify-content-between mt-1">
|
<i class="bi bi-check2-circle me-1"></i> {% trans "Syncing" %}
|
||||||
<span class="small text-muted">{% trans "Available Points" %}</span>
|
|
||||||
<span id="loyaltyPointsDisplay" class="small fw-bold">0.00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-body px-4 py-3 cart-items">
|
<!-- Cart Items (Scrollable) -->
|
||||||
<div id="cartItemsList">
|
<div class="pos-cart-items-area custom-scrollbar" id="cartItemsList">
|
||||||
<!-- Cart items will be injected here -->
|
<!-- Items injected via JS -->
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center py-5 text-muted opacity-50" id="emptyCartMsg">
|
|
||||||
<i class="bi bi-cart3 display-1 d-block mb-3"></i>
|
<!-- Empty State -->
|
||||||
{% trans "Your cart is empty" %}
|
<div id="emptyCartMsg" class="d-flex flex-column align-items-center justify-content-center h-50 text-muted opacity-50">
|
||||||
</div>
|
<i class="bi bi-basket3 display-1 mb-3"></i>
|
||||||
|
<p>{% trans "Cart is empty" %}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer: Totals & Checkout -->
|
||||||
|
<div class="pos-cart-footer mt-auto">
|
||||||
|
<div class="d-flex justify-content-between mb-1">
|
||||||
|
<span class="text-muted small">{% trans "Subtotal" %}</span>
|
||||||
|
<span id="subtotalAmount" class="fw-bold small">{{ site_settings.currency_symbol }}0.000</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mb-1">
|
||||||
|
<span class="text-muted small">{% trans "VAT" %}</span>
|
||||||
|
<span id="taxAmount" class="fw-bold small">{{ site_settings.currency_symbol }}0.000</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<span class="text-muted small">{% trans "Discount" %}</span>
|
||||||
|
<div class="input-group input-group-sm w-25">
|
||||||
|
<input type="number" id="discountInput" class="form-control text-end border-0 bg-light rounded" value="0" min="0" step="0.001" onchange="updateTotals()">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-footer bg-light border-0 p-4 rounded-bottom-4 mt-auto">
|
<div class="d-flex justify-content-between align-items-end mb-4 pt-3 border-top">
|
||||||
<div class="d-flex justify-content-between mb-2">
|
<span class="fs-5 fw-bold text-dark">{% trans "Total" %}</span>
|
||||||
<span class="small">{% trans "Subtotal" %}</span>
|
<span class="fs-2 fw-bold text-primary lh-1" id="totalAmount">{{ site_settings.currency_symbol }}0.000</span>
|
||||||
<span id="subtotalAmount" class="small">{{ site_settings.currency_symbol }}0.000</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span class="small">{% trans "VAT" %}</span>
|
|
||||||
<span id="taxAmount" class="small">{{ site_settings.currency_symbol }}0.000</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<span class="small">{% trans "Discount" %}</span>
|
|
||||||
<div class="input-group input-group-sm w-50">
|
|
||||||
<input type="number" id="discountInput" class="form-control text-end shadow-none" value="0" min="0" step="0.001" onchange="updateTotals()">
|
|
||||||
<span class="input-group-text bg-white border-start-0 small">{{ site_settings.currency_symbol }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-3 pt-2 border-top">
|
|
||||||
<span class="fw-bold fs-5">{% trans "Total" %}</span>
|
|
||||||
<span class="fw-bold fs-5 text-primary" id="totalAmount">{{ site_settings.currency_symbol }}0.000</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-9">
|
<div class="col-3">
|
||||||
<button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-3 shadow-none" onclick="checkout()" disabled>
|
<button id="holdBtn" class="btn btn-warning w-100 py-3 rounded-4 border-0 shadow-sm" onclick="holdSale()" disabled title="{% trans 'Hold Order' %}">
|
||||||
{% trans "PAY NOW" %}
|
<i class="bi bi-pause-fill fs-4 text-white"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-9">
|
||||||
<button id="holdBtn" class="btn btn-warning w-100 py-3 fw-bold rounded-3 shadow-none" onclick="holdSale()" disabled title="{% trans 'Hold Order' %}">
|
<button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-4 shadow-lg border-0 d-flex align-items-center justify-content-center gap-2" onclick="checkout()" disabled>
|
||||||
<i class="bi bi-pause-fill fs-4"></i>
|
<span>{% trans "PAY NOW" %}</span> <i class="bi bi-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile View Cart FAB -->
|
<!-- Mobile Cart Toggle FAB -->
|
||||||
<button class="btn btn-primary mobile-cart-toggle shadow-lg" onclick="toggleMobileCart()">
|
<button class="btn btn-primary mobile-cart-toggle shadow-lg d-lg-none" onclick="toggleMobileCart()">
|
||||||
<i class="bi bi-cart-fill me-2"></i>
|
<i class="bi bi-basket2-fill me-2"></i>
|
||||||
<span>{% trans "View Cart" %}</span>
|
<span>{% trans "View Order" %}</span>
|
||||||
<span class="badge bg-white text-primary ms-2 rounded-pill" id="mobileCartCountBadge">0</span>
|
<span class="badge bg-white text-primary ms-2 rounded-pill" id="mobileCartCountBadge">0</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals (Add Customer, Held Sales, Payment, Receipt) -->
|
||||||
<!-- Add Customer Modal -->
|
<!-- Add Customer Modal -->
|
||||||
<div class="modal fade no-print" id="addCustomerModal" tabindex="-1">
|
<div class="modal fade no-print" id="addCustomerModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
@ -530,6 +764,7 @@
|
|||||||
const currency = '{{ site_settings.currency_symbol|escapejs }}';
|
const currency = '{{ site_settings.currency_symbol|escapejs }}';
|
||||||
const decimalPlaces = {{ site_settings.decimal_places|default:3 }};
|
const decimalPlaces = {{ site_settings.decimal_places|default:3 }};
|
||||||
const loyaltyEnabled = {{ settings.loyalty_enabled|yesno:"true,false" }};
|
const loyaltyEnabled = {{ settings.loyalty_enabled|yesno:"true,false" }};
|
||||||
|
const syncIndicator = document.getElementById('syncIndicator');
|
||||||
|
|
||||||
function formatAmount(amount) {
|
function formatAmount(amount) {
|
||||||
return parseFloat(amount).toFixed(decimalPlaces);
|
return parseFloat(amount).toFixed(decimalPlaces);
|
||||||
@ -607,6 +842,7 @@
|
|||||||
|
|
||||||
if (cart.length === 0) {
|
if (cart.length === 0) {
|
||||||
emptyMsg.classList.remove('d-none');
|
emptyMsg.classList.remove('d-none');
|
||||||
|
emptyMsg.classList.add('d-flex');
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
updateTotals();
|
updateTotals();
|
||||||
payBtn.disabled = true;
|
payBtn.disabled = true;
|
||||||
@ -615,22 +851,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
emptyMsg.classList.add('d-none');
|
emptyMsg.classList.add('d-none');
|
||||||
|
emptyMsg.classList.remove('d-flex');
|
||||||
payBtn.disabled = false;
|
payBtn.disabled = false;
|
||||||
holdBtn.disabled = false;
|
holdBtn.disabled = false;
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
cart.forEach(item => {
|
cart.forEach(item => {
|
||||||
html += `
|
html += `
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="cart-item-row slide-in-bottom">
|
||||||
<div style="flex: 1;">
|
<div class="flex-grow-1 overflow-hidden me-2">
|
||||||
<div class="fw-bold small" dir="rtl">${item.name_ar}</div>
|
<div class="fw-bold text-dark text-truncate" dir="rtl">${item.name_ar}</div>
|
||||||
<div class="text-muted" style="font-size: 0.7rem;">${item.name_en}</div>
|
<div class="text-muted small text-truncate" style="font-size: 0.75rem;">${item.name_en}</div>
|
||||||
<div class="text-muted small">${currency} ${formatAmount(item.price)} x ${parseFloat(item.quantity).toFixed(2)}</div>
|
<div class="text-primary fw-bold" style="font-size: 0.85rem;">${currency} ${formatAmount(item.line_total)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-outline-secondary rounded-circle" style="width:24px; height:24px; padding:0; display:flex; align-items:center; justify-content:center;" onclick="updateQuantity(${item.id}, -1)">-</button>
|
<button class="qty-btn" onclick="updateQuantity(${item.id}, -1)"><i class="bi bi-dash"></i></button>
|
||||||
<span class="fw-bold small">${parseFloat(item.quantity).toFixed(2)}</span>
|
<span class="fw-bold" style="min-width: 25px; text-align: center;">${parseFloat(item.quantity).toFixed(0)}</span>
|
||||||
<button class="btn btn-sm btn-outline-secondary rounded-circle" style="width:24px; height:24px; padding:0; display:flex; align-items:center; justify-content:center;" onclick="updateQuantity(${item.id}, 1)">+</button>
|
<button class="qty-btn" onclick="updateQuantity(${item.id}, 1)"><i class="bi bi-plus"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -638,6 +875,9 @@
|
|||||||
|
|
||||||
listContainer.innerHTML = html;
|
listContainer.innerHTML = html;
|
||||||
updateTotals();
|
updateTotals();
|
||||||
|
|
||||||
|
// Auto scroll to bottom
|
||||||
|
listContainer.scrollTop = listContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkout() {
|
function checkout() {
|
||||||
@ -1099,6 +1339,8 @@
|
|||||||
|
|
||||||
// Search and Category Filtering
|
// Search and Category Filtering
|
||||||
document.getElementById('productSearch').addEventListener('input', filterProducts);
|
document.getElementById('productSearch').addEventListener('input', filterProducts);
|
||||||
|
|
||||||
|
// Updated Logic for new Pill buttons
|
||||||
document.querySelectorAll('.category-badge').forEach(badge => {
|
document.querySelectorAll('.category-badge').forEach(badge => {
|
||||||
badge.addEventListener('click', function() {
|
badge.addEventListener('click', function() {
|
||||||
document.querySelectorAll('.category-badge').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.category-badge').forEach(b => b.classList.remove('active'));
|
||||||
@ -1199,6 +1441,12 @@ document.addEventListener('click', function(e) {
|
|||||||
let customerDisplayWindow = null;
|
let customerDisplayWindow = null;
|
||||||
const displayChannel = new BroadcastChannel('pos_channel');
|
const displayChannel = new BroadcastChannel('pos_channel');
|
||||||
|
|
||||||
|
// EXPOSE DATA GLOBALLY for window.opener access
|
||||||
|
window.lastCartData = null;
|
||||||
|
window.getPosCartData = function() {
|
||||||
|
return window.lastCartData;
|
||||||
|
};
|
||||||
|
|
||||||
function openCustomerDisplay() {
|
function openCustomerDisplay() {
|
||||||
if (customerDisplayWindow && !customerDisplayWindow.closed) {
|
if (customerDisplayWindow && !customerDisplayWindow.closed) {
|
||||||
customerDisplayWindow.focus();
|
customerDisplayWindow.focus();
|
||||||
@ -1222,6 +1470,17 @@ document.addEventListener('click', function(e) {
|
|||||||
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
|
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
|
||||||
const total = Math.max(0, subtotal + totalVat - discount);
|
const total = Math.max(0, subtotal + totalVat - discount);
|
||||||
|
|
||||||
|
// Indicate Sync
|
||||||
|
syncIndicator.classList.remove('bg-success-subtle', 'text-success');
|
||||||
|
syncIndicator.classList.add('bg-success', 'text-white');
|
||||||
|
syncIndicator.innerHTML = '<i class="bi bi-arrow-repeat me-1 spin"></i> Syncing';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
syncIndicator.classList.remove('bg-success', 'text-white');
|
||||||
|
syncIndicator.classList.add('bg-success-subtle', 'text-success');
|
||||||
|
syncIndicator.innerHTML = '<i class="bi bi-check2-circle me-1"></i> Synced';
|
||||||
|
}, 800);
|
||||||
|
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'update_cart',
|
type: 'update_cart',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@ -1234,26 +1493,64 @@ document.addEventListener('click', function(e) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update Global Data for Direct Link
|
||||||
|
window.lastCartData = msg;
|
||||||
|
|
||||||
|
const jsonMsg = JSON.stringify(msg);
|
||||||
|
|
||||||
// 1. BroadcastChannel
|
// 1. BroadcastChannel
|
||||||
try { displayChannel.postMessage(msg); } catch(e) { console.error('BC Error:', e); }
|
try { displayChannel.postMessage(msg); } catch(e) { console.error('BC Error:', e); }
|
||||||
|
|
||||||
// 2. Direct Window
|
// 2. Direct Window
|
||||||
if (customerDisplayWindow && !customerDisplayWindow.closed) {
|
if (customerDisplayWindow && !customerDisplayWindow.closed) {
|
||||||
customerDisplayWindow.postMessage(msg, '*');
|
try {
|
||||||
|
customerDisplayWindow.postMessage(msg, '*');
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. LocalStorage Fallback (triggers 'storage' event in other tabs)
|
// 3. LocalStorage
|
||||||
localStorage.setItem('pos_cart_update', JSON.stringify(msg));
|
localStorage.setItem('pos_cart_update', jsonMsg);
|
||||||
console.log('[POS] Cart Broadcast Sent');
|
|
||||||
|
// 4. Cookie Fallback (Robust cross-context/iframe sharing)
|
||||||
|
try {
|
||||||
|
// Encode and set cookie (expires in 1 day)
|
||||||
|
// We use 'path=/' to ensure it's visible to the whole app
|
||||||
|
document.cookie = "pos_active_cart=" + encodeURIComponent(jsonMsg) + "; path=/; SameSite=Lax";
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Cookie Set Error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. SERVER SYNC (The Fix for Isolated Windows)
|
||||||
|
fetch('/api/pos/sync/update/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: jsonMsg
|
||||||
|
}).then(r => console.log('[POS] Server Sync:', r.status))
|
||||||
|
.catch(e => console.error('[POS] Server Sync Failed:', e));
|
||||||
|
|
||||||
|
console.log('[POS] Cart Broadcast Sent (BC, Win, LS, Cookie, Direct, SERVER)');
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastClear() {
|
function broadcastClear() {
|
||||||
const msg = { type: 'clear' };
|
const msg = { type: 'clear' };
|
||||||
|
|
||||||
|
window.lastCartData = msg;
|
||||||
|
|
||||||
displayChannel.postMessage(msg);
|
displayChannel.postMessage(msg);
|
||||||
if (customerDisplayWindow && !customerDisplayWindow.closed) {
|
if (customerDisplayWindow && !customerDisplayWindow.closed) {
|
||||||
customerDisplayWindow.postMessage(msg, '*');
|
customerDisplayWindow.postMessage(msg, '*');
|
||||||
}
|
}
|
||||||
localStorage.setItem('pos_cart_update', JSON.stringify(msg));
|
localStorage.setItem('pos_cart_update', JSON.stringify(msg));
|
||||||
|
// Clear cookie
|
||||||
|
document.cookie = "pos_active_cart=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
|
||||||
|
|
||||||
|
// Server Clear
|
||||||
|
fetch('/api/pos/sync/update/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(msg)
|
||||||
|
}).catch(e => console.error('Server Clear Failed', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -65,6 +65,10 @@ urlpatterns = [
|
|||||||
path('expenses/categories/', views.expense_categories_view, name='expense_categories'),
|
path('expenses/categories/', views.expense_categories_view, name='expense_categories'),
|
||||||
path('expenses/categories/delete/<int:pk>/', views.expense_category_delete_view, name='expense_category_delete'),
|
path('expenses/categories/delete/<int:pk>/', views.expense_category_delete_view, name='expense_category_delete'),
|
||||||
|
|
||||||
|
# POS Sync
|
||||||
|
path('api/pos/sync/update/', views.pos_sync_update, name='pos_sync_update'),
|
||||||
|
path('api/pos/sync/state/', views.pos_sync_state, name='pos_sync_state'),
|
||||||
|
|
||||||
# API / Actions
|
# API / Actions
|
||||||
path('api/create-sale/', views.create_sale_api, name='create_sale_api'),
|
path('api/create-sale/', views.create_sale_api, name='create_sale_api'),
|
||||||
path('api/update-sale/<int:pk>/', views.update_sale_api, name='update_sale_api'),
|
path('api/update-sale/<int:pk>/', views.update_sale_api, name='update_sale_api'),
|
||||||
|
|||||||
@ -2592,3 +2592,28 @@ def customer_display(request):
|
|||||||
"""
|
"""
|
||||||
settings = SystemSetting.objects.first()
|
settings = SystemSetting.objects.first()
|
||||||
return render(request, "core/customer_display.html", {"settings": settings})
|
return render(request, "core/customer_display.html", {"settings": settings})
|
||||||
|
@csrf_exempt
|
||||||
|
def pos_sync_update(request):
|
||||||
|
"""
|
||||||
|
Saves the POS cart state to the user's session for the Customer Display to pick up.
|
||||||
|
"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
# Store the cart data in the session
|
||||||
|
request.session['pos_cart_state'] = data
|
||||||
|
request.session.modified = True
|
||||||
|
return JsonResponse({'status': 'ok', 'timestamp': timezone.now().timestamp()})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'status': 'error', 'message': str(e)}, status=400)
|
||||||
|
return JsonResponse({'status': 'error', 'message': 'Invalid method'}, status=405)
|
||||||
|
|
||||||
|
def pos_sync_state(request):
|
||||||
|
"""
|
||||||
|
Returns the POS cart state from the session.
|
||||||
|
"""
|
||||||
|
data = request.session.get('pos_cart_state', None)
|
||||||
|
if data is None:
|
||||||
|
# Return a special flag if no state is found yet
|
||||||
|
return JsonResponse({'status': 'empty'}, safe=False)
|
||||||
|
return JsonResponse(data, safe=False)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user