correcting customer display

This commit is contained in:
Flatlogic Bot 2026-02-06 05:41:19 +00:00
parent 49c4d4dab1
commit 73951729f9
7 changed files with 784 additions and 415 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,277 +1,320 @@
{% load static i18n l10n %}
<!DOCTYPE html>
{% load static i18n %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "Customer Display" %} | {{ settings.business_name }}</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<title>{% trans "Customer Display" %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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>
:root {
--bs-primary-rgb: 13, 110, 253;
}
body {
background-color: #f8f9fa;
height: 100vh;
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;
padding: 2rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
font-size: 0.85rem; /* Reduced font size further */
}
.cart-list {
flex: 1;
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;
#welcome-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
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;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
transition: opacity 0.5s;
}
/* UPDATED: display: none is safer than opacity for hiding overlay */
.welcome-screen.hidden {
display: none !important;
.grand-total {
font-size: 2rem; /* Slightly smaller grand total */
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>
</head>
<body>
<!-- Welcome / Idle Screen -->
<div id="welcomeScreen" class="welcome-screen">
<div class="text-center">
<!-- Welcome Screen -->
<div id="welcome-screen">
<div class="mb-4">
{% 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 %}
<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 %}
<h1 class="display-4 fw-bold text-primary">{{ settings.business_name }}</h1>
<p class="lead text-muted mt-3">{% trans "Welcome! We are ready to serve you." %}</p>
<h2 class="fw-bold">{{ settings.business_name }}</h2>
<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>
<!-- Active Transaction Screen -->
<div class="main-container">
<!-- Left: Cart Items -->
<div class="left-panel">
<h4 class="fw-bold mb-4 border-bottom pb-3">{% trans "Your Order" %}</h4>
<div class="cart-list" id="cartList">
<!-- Items injected via JS -->
</div>
</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>
<!-- Main Transaction Display -->
<div id="main-display" class="container-fluid p-0">
<div class="row g-0 h-100">
<!-- Left: Item List -->
<div class="col-8 h-100 bg-light p-3 overflow-auto">
<h5 class="mb-3 fw-bold">{% trans "Your Order" %}</h5>
<div id="cart-items-container">
<!-- Items injected here -->
</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">
<span class="small">{% trans "Items" %}: <span id="itemCount">0</span></span>
<span class="small">{% trans "Tax" %}: <span id="taxAmount">0.000</span></span>
<!-- Right: Totals -->
<div class="col-4 h-100 shadow-lg">
<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>
<!-- Debug Status (z-index increased to 2000) -->
<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>
<!-- Debug Overlay -->
<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>
const currencySymbol = '{{ 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');
const currency = '{{ site_settings.currency_symbol|escapejs }}';
function updateStatus(msg) {
console.log('[CFD] ' + msg);
debugEl.innerText = msg + ' (' + new Date().toLocaleTimeString() + ')';
// Elements
const welcomeScreen = document.getElementById('welcome-screen');
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) {
const data = event.data;
function toggleLog() {
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;
updateStatus('Msg: ' + (data.type || 'unknown'));
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();
// Dedup based on timestamp if provided
if (data.timestamp && data.timestamp <= lastProcessedTimestamp) {
return;
}
if (data.timestamp) lastProcessedTimestamp = data.timestamp;
welcomeScreen.classList.add('hidden');
cartList.innerHTML = '';
cart.forEach(item => {
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<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;
log('New Data: ' + data.type);
if (data.type === 'update_cart') {
updateDisplay(data);
} else if (data.type === 'clear') {
welcomeScreen.style.display = 'flex';
mainDisplay.style.display = 'none';
setStatus('Cleared');
}
}
function showWelcome() {
updateStatus('State: Welcome');
welcomeScreen.classList.remove('hidden');
cartList.innerHTML = '';
totalEl.innerText = `${currencySymbol} 0.000`;
countEl.innerText = '0';
taxEl.innerText = '0.000';
}
// Server Sync Polling (v8)
// The most reliable method when windows are isolated
setInterval(() => {
fetch('/api/pos/sync/state/')
.then(res => res.json())
.then(data => {
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>
</body>
</html>

View File

@ -5,65 +5,276 @@
{% block head %}
<style>
.product-card {
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
border: none;
border-radius: 12px;
/* Full Screen POS Layout Overrides */
:root {
--pos-cart-width: 420px;
--pos-bg: #f1f5f9;
--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;
}
.product-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(0,0,0,0.1) !important;
}
.cart-container {
position: sticky;
top: 80px;
height: calc(100vh - 120px);
/* In case sidebar is present, ensure we handle it */
#content {
height: 100vh;
display: flex;
flex-direction: column;
}
.cart-items {
flex-grow: 1;
overflow-y: auto;
}
.category-badge {
cursor: pointer;
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;
/* POS Container */
.pos-layout {
display: flex;
height: 100%;
width: 100%;
overflow: hidden;
}
.payment-method-btn.active {
background-color: var(--bs-primary);
color: white;
border-color: var(--bs-primary);
/* Left Side: Products */
.pos-products-section {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--pos-bg);
overflow: hidden;
position: relative;
}
/* Loyalty Styles */
.loyalty-badge {
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 10px;
font-weight: bold;
.pos-products-header {
padding: 1rem 1.5rem;
background: white;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
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 */
@ -91,13 +302,6 @@
.no-print {
display: none !important;
}
#sidebar, .top-navbar, .btn, .modal {
display: none !important;
}
main {
padding: 0 !important;
margin: 0 !important;
}
}
#invoice-print {
@ -122,158 +326,188 @@
{% endblock %}
{% block content %}
<div class="container-fluid px-2 px-md-4 no-print">
<div class="row g-3 g-lg-4">
<!-- Products Section -->
<div class="col-lg-8">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 gap-3">
<div class="pos-layout no-print">
<!-- LEFT PANEL: Products & Search -->
<div class="pos-products-section">
<!-- Top Bar: Title & Actions -->
<div class="pos-products-header justify-content-between">
<div>
<h4 class="fw-bold mb-0">{% trans "Point of Sale" %}</h4>
<div class="ms-3 d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" onclick="openCustomerDisplay()" title="{% trans 'Open Customer Screen' %}">
<i class="bi bi-display"></i> <span class="d-none d-md-inline">{% trans "Customer Screen" %}</span>
<small class="text-muted">{% now "l, j F Y" %}</small>
</div>
<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 class="btn btn-sm btn-outline-warning" onclick="broadcastCartUpdate()" title="{% trans 'Force Sync' %}">
<i class="bi bi-arrow-repeat"></i> <span class="d-none d-md-inline">{% trans "Sync" %}</span>
<button class="btn btn-light border shadow-sm rounded-pill px-3" onclick="broadcastCartUpdate()" title="{% trans 'Force Sync' %}">
<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>
</div>
<div class="input-group w-100 w-md-50">
<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>
<div class="mb-4 overflow-auto text-nowrap pb-2">
<div class="category-badge active" data-category="all">{% trans "All" %}</div>
<!-- Search & Filter Bar -->
<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 %}
<div class="category-badge" data-category="{{ category.id }}">
<button class="nav-link category-badge" data-category="{{ category.id }}">
{{ category.name_ar }} / {{ category.name_en }}
</div>
</button>
{% endfor %}
</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 %}
<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 }})">
{% if product.image %}
<img src="{{ product.image.url }}" class="card-img-top rounded-3" alt="{{ product.name_en }}" style="height: 80px; object-fit: cover;">
{% else %}
<div class="bg-light rounded-3 d-flex align-items-center justify-content-center" style="height: 80px;">
<i class="bi bi-image text-muted opacity-25" style="font-size: 1.5rem;"></i>
</div>
{% 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 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 }})">
<div class="product-image-container">
{% if product.image %}
<img src="{{ product.image.url }}" alt="{{ product.name_en }}">
{% else %}
<div class="d-flex align-items-center justify-content-center h-100 bg-light text-muted">
<i class="bi bi-image fs-1 opacity-25"></i>
</div>
<p class="text-primary fw-bold mb-0" style="font-size: 0.8rem;">{{ site_settings.currency_symbol }}{{ product.price|floatformat:decimal_places }}</p>
<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>
{% endif %}
</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>
{% endfor %}
</div>
</div>
</div>
<!-- Cart Section -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm rounded-4 cart-container" id="posCart">
<div class="card-header bg-white border-0 pt-4 px-4 d-flex justify-content-between 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()">
<i class="bi bi-chevron-left fs-4"></i>
</button>
<h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5>
</div>
<div class="d-flex gap-2 align-items-center">
<button class="btn btn-sm btn-outline-warning shadow-none position-relative" onclick="loadHeldSales()" data-bs-toggle="modal" data-bs-target="#heldSalesModal">
<i class="bi bi-clock-history"></i>
<span id="heldCountBadge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger d-none">0</span>
</button>
<button class="btn btn-sm btn-link text-danger text-decoration-none p-0" onclick="clearCart()">
<i class="bi bi-trash"></i> {% trans "Clear" %}
</button>
<!-- RIGHT PANEL: Cart -->
<div class="pos-cart-section" id="posCart">
<!-- Cart Header -->
<div class="pos-cart-header d-flex justify-content-between 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()">
<i class="bi bi-chevron-left fs-4"></i>
</button>
<h5 class="fw-bold mb-0">{% trans "Current Order" %}</h5>
</div>
<div>
<button class="btn btn-sm btn-light text-danger border-0 rounded-pill px-3" onclick="clearCart()">
<i class="bi bi-trash me-1"></i> {% trans "Clear" %}
</button>
</div>
</div>
<!-- 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 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>
<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">
<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 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>
<button class="btn btn-sm btn-outline-primary shadow-none" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
<i class="bi bi-person-plus"></i>
</button>
</div>
<!-- Loyalty Info -->
<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="d-flex align-items-center gap-2">
<i class="bi bi-star-fill text-warning"></i>
<span id="loyaltyTierBadge" class="badge bg-warning text-dark rounded-pill"></span>
</div>
<div class="small fw-bold">
<span id="loyaltyPointsDisplay">0</span> {% trans "Pts" %}
</div>
</div>
<!-- Loyalty Info Display -->
<div id="loyaltyInfo" class="d-none bg-light p-2 rounded-3 mb-2 animate__animated animate__fadeIn">
<div class="d-flex justify-content-between align-items-center">
<span class="small fw-bold text-muted"><i class="bi bi-star-fill text-warning me-1"></i> {% trans "Loyalty" %}</span>
<span id="loyaltyTierBadge" class="loyalty-badge text-white"></span>
</div>
<div class="d-flex justify-content-between mt-1">
<span class="small text-muted">{% trans "Available Points" %}</span>
<span id="loyaltyPointsDisplay" class="small fw-bold">0.00</span>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-2">
<button class="btn btn-sm btn-outline-secondary border-0 small py-0" onclick="loadHeldSales()" data-bs-toggle="modal" data-bs-target="#heldSalesModal">
<i class="bi bi-clock-history me-1"></i> {% trans "Held Orders" %}
<span id="heldCountBadge" class="badge bg-danger rounded-pill ms-1 d-none">0</span>
</button>
<div id="syncIndicator" class="sync-indicator badge bg-success-subtle text-success border border-success-subtle rounded-pill">
<i class="bi bi-check2-circle me-1"></i> {% trans "Syncing" %}
</div>
</div>
</div>
<div class="card-body px-4 py-3 cart-items">
<div id="cartItemsList">
<!-- Cart items will be injected here -->
</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>
{% trans "Your cart is empty" %}
</div>
<!-- Cart Items (Scrollable) -->
<div class="pos-cart-items-area custom-scrollbar" id="cartItemsList">
<!-- Items injected via JS -->
</div>
<!-- Empty State -->
<div id="emptyCartMsg" class="d-flex flex-column align-items-center justify-content-center h-50 text-muted opacity-50">
<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 class="card-footer bg-light border-0 p-4 rounded-bottom-4 mt-auto">
<div class="d-flex justify-content-between mb-2">
<span class="small">{% trans "Subtotal" %}</span>
<span id="subtotalAmount" class="small">{{ site_settings.currency_symbol }}0.000</span>
</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="d-flex justify-content-between align-items-end mb-4 pt-3 border-top">
<span class="fs-5 fw-bold text-dark">{% trans "Total" %}</span>
<span class="fs-2 fw-bold text-primary lh-1" id="totalAmount">{{ site_settings.currency_symbol }}0.000</span>
</div>
<div class="row g-2">
<div class="col-9">
<button id="payNowBtn" class="btn btn-primary w-100 py-3 fw-bold rounded-3 shadow-none" onclick="checkout()" disabled>
{% trans "PAY NOW" %}
</button>
</div>
<div class="col-3">
<button id="holdBtn" class="btn btn-warning w-100 py-3 fw-bold rounded-3 shadow-none" onclick="holdSale()" disabled title="{% trans 'Hold Order' %}">
<i class="bi bi-pause-fill fs-4"></i>
</button>
</div>
</div>
<div class="row g-2">
<div class="col-3">
<button id="holdBtn" class="btn btn-warning w-100 py-3 rounded-4 border-0 shadow-sm" onclick="holdSale()" disabled title="{% trans 'Hold Order' %}">
<i class="bi bi-pause-fill fs-4 text-white"></i>
</button>
</div>
<div class="col-9">
<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>
<span>{% trans "PAY NOW" %}</span> <i class="bi bi-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Mobile View Cart FAB -->
<button class="btn btn-primary mobile-cart-toggle shadow-lg" onclick="toggleMobileCart()">
<i class="bi bi-cart-fill me-2"></i>
<span>{% trans "View Cart" %}</span>
<!-- Mobile Cart Toggle FAB -->
<button class="btn btn-primary mobile-cart-toggle shadow-lg d-lg-none" onclick="toggleMobileCart()">
<i class="bi bi-basket2-fill me-2"></i>
<span>{% trans "View Order" %}</span>
<span class="badge bg-white text-primary ms-2 rounded-pill" id="mobileCartCountBadge">0</span>
</button>
</div>
<!-- Modals (Add Customer, Held Sales, Payment, Receipt) -->
<!-- Add Customer Modal -->
<div class="modal fade no-print" id="addCustomerModal" tabindex="-1">
<div class="modal-dialog">
@ -530,6 +764,7 @@
const currency = '{{ site_settings.currency_symbol|escapejs }}';
const decimalPlaces = {{ site_settings.decimal_places|default:3 }};
const loyaltyEnabled = {{ settings.loyalty_enabled|yesno:"true,false" }};
const syncIndicator = document.getElementById('syncIndicator');
function formatAmount(amount) {
return parseFloat(amount).toFixed(decimalPlaces);
@ -607,6 +842,7 @@
if (cart.length === 0) {
emptyMsg.classList.remove('d-none');
emptyMsg.classList.add('d-flex');
listContainer.innerHTML = '';
updateTotals();
payBtn.disabled = true;
@ -615,22 +851,23 @@
}
emptyMsg.classList.add('d-none');
emptyMsg.classList.remove('d-flex');
payBtn.disabled = false;
holdBtn.disabled = false;
let html = '';
cart.forEach(item => {
html += `
<div class="d-flex justify-content-between align-items-center mb-3">
<div style="flex: 1;">
<div class="fw-bold small" dir="rtl">${item.name_ar}</div>
<div class="text-muted" style="font-size: 0.7rem;">${item.name_en}</div>
<div class="text-muted small">${currency} ${formatAmount(item.price)} x ${parseFloat(item.quantity).toFixed(2)}</div>
<div class="cart-item-row slide-in-bottom">
<div class="flex-grow-1 overflow-hidden me-2">
<div class="fw-bold text-dark text-truncate" dir="rtl">${item.name_ar}</div>
<div class="text-muted small text-truncate" style="font-size: 0.75rem;">${item.name_en}</div>
<div class="text-primary fw-bold" style="font-size: 0.85rem;">${currency} ${formatAmount(item.line_total)}</div>
</div>
<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>
<span class="fw-bold small">${parseFloat(item.quantity).toFixed(2)}</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-dash"></i></button>
<span class="fw-bold" style="min-width: 25px; text-align: center;">${parseFloat(item.quantity).toFixed(0)}</span>
<button class="qty-btn" onclick="updateQuantity(${item.id}, 1)"><i class="bi bi-plus"></i></button>
</div>
</div>
`;
@ -638,6 +875,9 @@
listContainer.innerHTML = html;
updateTotals();
// Auto scroll to bottom
listContainer.scrollTop = listContainer.scrollHeight;
}
function checkout() {
@ -1099,6 +1339,8 @@
// Search and Category Filtering
document.getElementById('productSearch').addEventListener('input', filterProducts);
// Updated Logic for new Pill buttons
document.querySelectorAll('.category-badge').forEach(badge => {
badge.addEventListener('click', function() {
document.querySelectorAll('.category-badge').forEach(b => b.classList.remove('active'));
@ -1199,6 +1441,12 @@ document.addEventListener('click', function(e) {
let customerDisplayWindow = null;
const displayChannel = new BroadcastChannel('pos_channel');
// EXPOSE DATA GLOBALLY for window.opener access
window.lastCartData = null;
window.getPosCartData = function() {
return window.lastCartData;
};
function openCustomerDisplay() {
if (customerDisplayWindow && !customerDisplayWindow.closed) {
customerDisplayWindow.focus();
@ -1222,6 +1470,17 @@ document.addEventListener('click', function(e) {
const discount = parseFloat(document.getElementById('discountInput').value) || 0;
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 = {
type: 'update_cart',
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
try { displayChannel.postMessage(msg); } catch(e) { console.error('BC Error:', e); }
// 2. Direct Window
if (customerDisplayWindow && !customerDisplayWindow.closed) {
customerDisplayWindow.postMessage(msg, '*');
try {
customerDisplayWindow.postMessage(msg, '*');
} catch(e) {}
}
// 3. LocalStorage Fallback (triggers 'storage' event in other tabs)
localStorage.setItem('pos_cart_update', JSON.stringify(msg));
console.log('[POS] Cart Broadcast Sent');
// 3. LocalStorage
localStorage.setItem('pos_cart_update', jsonMsg);
// 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() {
const msg = { type: 'clear' };
window.lastCartData = msg;
displayChannel.postMessage(msg);
if (customerDisplayWindow && !customerDisplayWindow.closed) {
customerDisplayWindow.postMessage(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>

View File

@ -65,6 +65,10 @@ urlpatterns = [
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'),
# 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
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'),

View File

@ -2592,3 +2592,28 @@ def customer_display(request):
"""
settings = SystemSetting.objects.first()
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)