461 lines
14 KiB
HTML
461 lines
14 KiB
HTML
{% extends 'base.html' %}
|
|
{% load i18n static %}
|
|
|
|
{% block title %}{% trans "Customer Display" %} | {{ site_settings.business_name }}{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
/* --- Critical Layout Overrides for Full Screen --- */
|
|
#sidebar,
|
|
.navbar,
|
|
.top-navbar,
|
|
#sidebarCollapse,
|
|
.mobile-cart-toggle,
|
|
footer {
|
|
display: none !important;
|
|
}
|
|
|
|
#wrapper {
|
|
display: block !important;
|
|
width: 100% !important;
|
|
height: 100vh !important;
|
|
overflow: hidden !important;
|
|
}
|
|
|
|
#content {
|
|
margin-left: 0 !important;
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
padding: 0 !important;
|
|
overflow: hidden !important;
|
|
}
|
|
|
|
main {
|
|
padding: 0 !important;
|
|
height: 100% !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
body {
|
|
overflow: hidden;
|
|
background-color: #f8f9fa;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
/* --- Customer Display Specific Styles --- */
|
|
.customer-display-container {
|
|
height: 100vh;
|
|
width: 100vw;
|
|
display: flex;
|
|
flex-direction: row;
|
|
position: relative;
|
|
}
|
|
|
|
.left-panel {
|
|
flex: 1; /* Takes 50% or remaining space */
|
|
background: white;
|
|
padding: 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 2px 0 10px rgba(0,0,0,0.05);
|
|
z-index: 10;
|
|
height: 100%;
|
|
max-width: 50%; /* limit width to half screen */
|
|
}
|
|
|
|
.right-panel {
|
|
flex: 1;
|
|
background: #f8f9fa;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2rem;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.cart-items-container {
|
|
flex-grow: 1;
|
|
overflow-y: auto;
|
|
margin-bottom: 2rem;
|
|
padding-right: 10px; /* space for scrollbar */
|
|
}
|
|
|
|
/* Custom Scrollbar for items */
|
|
.cart-items-container::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
.cart-items-container::-webkit-scrollbar-thumb {
|
|
background-color: #dee2e6;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.cart-item {
|
|
border-bottom: 1px solid #eee;
|
|
padding: 1rem 0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
animation: fadeIn 0.3s ease-in-out;
|
|
}
|
|
.cart-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.item-name {
|
|
font-weight: 600;
|
|
font-size: 1.2rem;
|
|
color: #212529;
|
|
}
|
|
.item-price {
|
|
color: #6c757d;
|
|
font-size: 0.9rem;
|
|
}
|
|
.item-total {
|
|
font-weight: 700;
|
|
font-size: 1.3rem;
|
|
color: var(--bs-primary);
|
|
}
|
|
|
|
.total-section {
|
|
background: var(--bs-primary);
|
|
color: white;
|
|
padding: 2.5rem;
|
|
border-radius: 1.5rem;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
|
|
margin-bottom: 2rem;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
}
|
|
.total-label {
|
|
font-size: 1.4rem;
|
|
opacity: 0.9;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.total-amount {
|
|
font-size: 4.5rem;
|
|
font-weight: 800;
|
|
line-height: 1;
|
|
}
|
|
|
|
.logo-placeholder {
|
|
max-width: 300px;
|
|
opacity: 0.5;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.promo-card {
|
|
flex: 1;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
background: white;
|
|
border-radius: 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
padding: 2rem;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* Connection Status Dot */
|
|
.status-dot {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background-color: #dc3545; /* Red initially */
|
|
z-index: 100;
|
|
transition: background-color 0.3s;
|
|
border: 2px solid white;
|
|
box-shadow: 0 0 5px rgba(0,0,0,0.2);
|
|
}
|
|
.status-dot.connected {
|
|
background-color: #198754; /* Green */
|
|
}
|
|
|
|
/* Debug Panel */
|
|
#debugPanel {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background: rgba(0,0,0,0.8);
|
|
color: #0f0;
|
|
font-family: monospace;
|
|
font-size: 10px;
|
|
padding: 10px;
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
display: none; /* Hidden by default, toggleable */
|
|
z-index: 2000;
|
|
}
|
|
.debug-toggle {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
left: 10px;
|
|
font-size: 10px;
|
|
opacity: 0.3;
|
|
z-index: 2001;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="customer-display-container">
|
|
<div id="connectionStatus" class="status-dot" title="Connection Status"></div>
|
|
<div class="debug-toggle" onclick="toggleDebug()">Debug</div>
|
|
|
|
<!-- Left Panel: Cart Items -->
|
|
<div class="left-panel">
|
|
<div class="d-flex align-items-center mb-4 border-bottom pb-3">
|
|
{% if site_settings.logo %}
|
|
<img src="{{ site_settings.logo.url }}" alt="Logo" height="50" class="me-3">
|
|
{% endif %}
|
|
<div>
|
|
<h2 class="fw-bold mb-0 text-dark">{{ site_settings.business_name|default:"POS Display" }}</h2>
|
|
<div class="text-muted small">{% trans "Your Order Details" %}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cart-items-container" id="cartItems">
|
|
<!-- Default Empty State -->
|
|
<div class="h-100 d-flex flex-column align-items-center justify-content-center text-muted opacity-50">
|
|
<i class="bi bi-basket display-1 mb-3"></i>
|
|
<h3>{% trans "Welcome!" %}</h3>
|
|
<p>{% trans "Items will appear here as scanned." %}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-auto border-top pt-3">
|
|
<div class="d-flex justify-content-between mb-2 fs-5">
|
|
<span class="text-muted">{% trans "Subtotal" %}</span>
|
|
<span id="subtotalDisplay" class="fw-bold">---</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2 fs-5">
|
|
<span class="text-muted">{% trans "VAT" %}</span>
|
|
<span id="vatDisplay" class="fw-bold">---</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2 fs-5 text-danger" style="visibility: hidden;">
|
|
<span class="text-muted">{% trans "Discount" %}</span>
|
|
<span id="discountDisplay" class="fw-bold">---</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel: Big Total / Ads -->
|
|
<div class="right-panel">
|
|
<div class="total-section text-center">
|
|
<div class="total-label">{% trans "Total to Pay" %}</div>
|
|
<div class="total-amount" id="totalDisplay">0.00</div>
|
|
</div>
|
|
|
|
<div class="promo-card">
|
|
<div class="d-flex flex-column justify-content-center h-100">
|
|
<div class="display-1 text-primary mb-3"><i class="bi bi-emoji-smile"></i></div>
|
|
<h3 class="fw-bold text-dark mb-2">{% trans "Thank You!" %}</h3>
|
|
<p class="text-muted fs-5">{% trans "We appreciate your business." %}</p>
|
|
<div class="d-flex gap-2 justify-content-center mt-3">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="requestSync()">{% trans "Refresh" %}</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="hardReset()">{% trans "Reset" %}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="debugPanel">Waiting for data...</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const channel = new BroadcastChannel('pos_channel');
|
|
let currencySymbol = '{{ site_settings.currency_symbol|escapejs }}';
|
|
if (!currencySymbol) currencySymbol = '{{ settings.currency_symbol|default:"$"|escapejs }}';
|
|
|
|
const statusDot = document.getElementById('connectionStatus');
|
|
const debugPanel = document.getElementById('debugPanel');
|
|
|
|
function log(msg) {
|
|
const time = new Date().toLocaleTimeString();
|
|
debugPanel.innerHTML = `<div>[${time}] ${msg}</div>` + debugPanel.innerHTML;
|
|
// console.log(msg); // Optional: keep console clean or enable for dev
|
|
}
|
|
|
|
function toggleDebug() {
|
|
debugPanel.style.display = debugPanel.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
|
|
function setConnected(isConnected) {
|
|
if (isConnected) {
|
|
statusDot.classList.add('connected');
|
|
statusDot.title = "Connected";
|
|
} else {
|
|
statusDot.classList.remove('connected');
|
|
statusDot.title = "Disconnected";
|
|
}
|
|
}
|
|
|
|
function requestSync() {
|
|
log("Requesting sync...");
|
|
channel.postMessage({ type: 'request_state' });
|
|
if (window.opener) {
|
|
window.opener.postMessage({ type: 'request_state' }, '*');
|
|
}
|
|
}
|
|
|
|
function hardReset() {
|
|
localStorage.removeItem('pos_cart_state');
|
|
location.reload();
|
|
}
|
|
|
|
// --- State Management ---
|
|
let lastTimestamp = 0;
|
|
|
|
function handleUpdate(data) {
|
|
if (!data || !data.items) return;
|
|
|
|
// Avoid re-rendering if timestamp hasn't changed
|
|
if (data.timestamp && data.timestamp <= lastTimestamp) {
|
|
return;
|
|
}
|
|
lastTimestamp = data.timestamp || Date.now();
|
|
|
|
setConnected(true);
|
|
renderCart(data);
|
|
}
|
|
|
|
// 1. Initial Load
|
|
try {
|
|
const savedState = localStorage.getItem('pos_cart_state');
|
|
if (savedState) {
|
|
log("Loaded LocalStorage");
|
|
handleUpdate(JSON.parse(savedState));
|
|
}
|
|
} catch (e) {
|
|
log("LS Error: " + e.message);
|
|
}
|
|
|
|
// 2. Broadcast Channel
|
|
channel.onmessage = (event) => {
|
|
log("Broadcast: " + event.data.type);
|
|
if (event.data.type === 'update') {
|
|
handleUpdate(event.data);
|
|
localStorage.setItem('pos_cart_state', JSON.stringify(event.data));
|
|
} else if (event.data.type === 'clear') {
|
|
clearDisplay();
|
|
localStorage.removeItem('pos_cart_state');
|
|
}
|
|
};
|
|
|
|
// 3. Polling LocalStorage (Robust Fallback)
|
|
setInterval(() => {
|
|
try {
|
|
const savedState = localStorage.getItem('pos_cart_state');
|
|
if (savedState) {
|
|
const data = JSON.parse(savedState);
|
|
if (data.timestamp > lastTimestamp) {
|
|
log("Polling found update");
|
|
handleUpdate(data);
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
}, 500);
|
|
|
|
// 4. PostMessage (Iframe/Opener)
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'update') {
|
|
log("PostMessage received");
|
|
handleUpdate(event.data);
|
|
localStorage.setItem('pos_cart_state', JSON.stringify(event.data));
|
|
}
|
|
});
|
|
|
|
// Request initial state
|
|
setTimeout(requestSync, 500);
|
|
|
|
function renderCart(data) {
|
|
const container = document.getElementById('cartItems');
|
|
const items = data.items || [];
|
|
|
|
if (items.length === 0) {
|
|
clearDisplay();
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
items.forEach(item => {
|
|
const nameAr = item.name_ar || item.name_en || 'Unknown';
|
|
const nameEn = item.name_en || '';
|
|
const price = parseFloat(item.price || 0).toFixed(2);
|
|
const qty = parseFloat(item.quantity || 1).toFixed(2);
|
|
const lineTotal = parseFloat(item.line_total || 0).toFixed(2);
|
|
|
|
html += `
|
|
<div class="cart-item">
|
|
<div>
|
|
<div class="item-name">${nameAr}</div>
|
|
<div class="small text-muted mb-1">${nameEn}</div>
|
|
<div class="item-price">${currencySymbol} ${price} x ${qty}</div>
|
|
</div>
|
|
<div class="item-total">
|
|
${currencySymbol} ${lineTotal}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
container.innerHTML = html;
|
|
|
|
// Auto scroll
|
|
setTimeout(() => container.scrollTop = container.scrollHeight, 50);
|
|
|
|
const subtotal = parseFloat(data.subtotal || 0).toFixed(2);
|
|
const vat = parseFloat(data.vat || 0).toFixed(2);
|
|
const total = parseFloat(data.total || 0).toFixed(2);
|
|
|
|
document.getElementById('subtotalDisplay').innerText = `${currencySymbol} ${subtotal}`;
|
|
document.getElementById('vatDisplay').innerText = `${currencySymbol} ${vat}`;
|
|
|
|
const discountVal = parseFloat(data.discount || 0);
|
|
const discountEl = document.getElementById('discountDisplay');
|
|
if (discountEl) {
|
|
const discountRow = discountEl.parentElement;
|
|
if (discountVal > 0) {
|
|
discountEl.innerText = `-${currencySymbol} ${discountVal.toFixed(2)}`;
|
|
discountRow.style.visibility = 'visible';
|
|
} else {
|
|
discountRow.style.visibility = 'hidden';
|
|
}
|
|
}
|
|
|
|
document.getElementById('totalDisplay').innerText = `${currencySymbol} ${total}`;
|
|
}
|
|
|
|
function clearDisplay() {
|
|
document.getElementById('cartItems').innerHTML = `
|
|
<div class="h-100 d-flex flex-column align-items-center justify-content-center text-muted opacity-50">
|
|
<i class="bi bi-basket display-1 mb-3"></i>
|
|
<h3>{% trans "Welcome!" %}</h3>
|
|
<p>{% trans "Items will appear here as scanned." %}</p>
|
|
</div>
|
|
`;
|
|
document.getElementById('subtotalDisplay').innerText = '---';
|
|
document.getElementById('vatDisplay').innerText = '---';
|
|
const discountEl = document.getElementById('discountDisplay');
|
|
if (discountEl) {
|
|
discountEl.innerText = '---';
|
|
discountEl.parentElement.style.visibility = 'hidden';
|
|
}
|
|
document.getElementById('totalDisplay').innerText = `${currencySymbol} 0.00`;
|
|
}
|
|
</script>
|
|
{% endblock %} |