ttup / cashier.html
Phoe2004's picture
Upload 6 files
2b03906 verified
<!DOCTYPE html>
<html lang="my">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Cashier - POS</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Myanmar:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
:root {
--bg: #0a0e1a;
--card: #111827;
--card2: #1a2234;
--border: #1e293b;
--accent: #f59e0b;
--green: #10b981;
--red: #ef4444;
--text: #f1f5f9;
--muted: #64748b;
--scan: #3b82f6;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Noto Sans Myanmar', sans-serif;
min-height: 100vh;
max-width: 480px;
margin: 0 auto;
}
/* ── Header ── */
.header {
background: var(--card);
border-bottom: 1px solid var(--border);
padding: 14px 16px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky; top: 0; z-index: 100;
}
.header-left { display: flex; align-items: center; gap: 10px; }
.logo-sm {
width: 36px; height: 36px;
background: linear-gradient(135deg, var(--accent), #d97706);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
}
.header h2 { font-size: 15px; font-weight: 600; }
.header p { font-size: 11px; color: var(--muted); }
.btn-logout {
background: transparent;
border: 1px solid var(--border);
border-radius: 8px;
padding: 7px 12px;
color: var(--muted);
font-size: 12px;
cursor: pointer;
font-family: 'Noto Sans Myanmar', sans-serif;
}
/* ── Scanner ── */
.scanner-section {
padding: 16px;
}
.scan-toggle {
width: 100%;
background: linear-gradient(135deg, var(--scan), #2563eb);
border: none;
border-radius: 14px;
padding: 16px;
color: white;
font-size: 15px;
font-weight: 600;
font-family: 'Noto Sans Myanmar', sans-serif;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: all 0.2s;
box-shadow: 0 4px 16px rgba(59,130,246,0.3);
}
.scan-toggle.active {
background: linear-gradient(135deg, var(--red), #dc2626);
box-shadow: 0 4px 16px rgba(239,68,68,0.3);
}
.scanner-wrap {
display: none;
margin-top: 12px;
border-radius: 14px;
overflow: hidden;
border: 2px solid var(--scan);
position: relative;
}
.scanner-wrap.visible { display: block; }
#reader { width: 100%; }
.scan-overlay {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
pointer-events: none;
}
.scan-frame {
width: 200px; height: 100px;
border: 2px solid var(--accent);
border-radius: 8px;
box-shadow: 0 0 0 9999px rgba(0,0,0,0.4);
position: relative;
}
.scan-frame::before, .scan-frame::after {
content: '';
position: absolute;
width: 20px; height: 20px;
border-color: var(--accent);
border-style: solid;
}
.scan-frame::before { top: -2px; left: -2px; border-width: 3px 0 0 3px; }
.scan-frame::after { bottom: -2px; right: -2px; border-width: 0 3px 3px 0; }
.scan-line {
position: absolute;
left: 0; right: 0; height: 2px;
background: var(--accent);
animation: scanMove 2s ease-in-out infinite;
}
@keyframes scanMove {
0% { top: 5px; }
100% { top: calc(100% - 5px); }
}
/* Manual input */
.manual-input {
display: flex; gap: 8px;
margin-top: 10px;
}
.manual-input input {
flex: 1;
background: var(--card2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 14px;
color: var(--text);
font-size: 14px;
font-family: 'JetBrains Mono', monospace;
outline: none;
}
.manual-input input:focus { border-color: var(--scan); }
.btn-search {
background: var(--scan);
border: none;
border-radius: 10px;
padding: 12px 16px;
color: white;
font-size: 18px;
cursor: pointer;
}
/* ── Product Found Toast ── */
.product-toast {
margin: 0 16px;
background: rgba(16,185,129,0.1);
border: 1px solid rgba(16,185,129,0.3);
border-radius: 12px;
padding: 14px 16px;
display: none;
align-items: center;
gap: 12px;
animation: fadeIn 0.3s ease;
}
.product-toast.show { display: flex; }
.product-toast.error {
background: rgba(239,68,68,0.1);
border-color: rgba(239,68,68,0.3);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.toast-icon { font-size: 28px; }
.toast-info { flex: 1; }
.toast-info h3 { font-size: 14px; font-weight: 600; }
.toast-info p { font-size: 13px; color: var(--accent); font-weight: 700; margin-top: 2px; }
.toast-info .toast-err { color: #fca5a5; font-size: 12px; }
/* ── Cart ── */
.cart-section {
padding: 16px;
margin-top: 8px;
}
.cart-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 12px;
}
.cart-title {
font-size: 14px;
font-weight: 700;
display: flex; align-items: center; gap: 8px;
}
.cart-badge {
background: var(--accent);
color: #000;
border-radius: 20px;
padding: 2px 8px;
font-size: 11px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
}
.btn-clear {
background: transparent;
border: 1px solid rgba(239,68,68,0.3);
border-radius: 8px;
padding: 5px 10px;
color: var(--red);
font-size: 11px;
cursor: pointer;
font-family: 'Noto Sans Myanmar', sans-serif;
}
.cart-empty {
text-align: center;
padding: 32px 16px;
color: var(--muted);
font-size: 13px;
background: var(--card2);
border-radius: 12px;
border: 1px dashed var(--border);
}
.cart-empty span { font-size: 36px; display: block; margin-bottom: 8px; }
.cart-item {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px 14px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
animation: slideIn 0.2s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.item-info { flex: 1; min-width: 0; }
.item-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-price { font-size: 12px; color: var(--accent); font-family: 'JetBrains Mono', monospace; margin-top: 2px; }
.qty-ctrl {
display: flex; align-items: center; gap: 8px;
flex-shrink: 0;
}
.qty-btn {
width: 30px; height: 30px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--card2);
color: var(--text);
font-size: 16px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.15s;
}
.qty-btn:active { transform: scale(0.9); }
.qty-btn.minus:active { background: rgba(239,68,68,0.2); }
.qty-btn.plus:active { background: rgba(16,185,129,0.2); }
.qty-num {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 15px;
min-width: 24px;
text-align: center;
}
.item-total {
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 13px;
color: var(--green);
min-width: 60px;
text-align: right;
flex-shrink: 0;
}
.btn-del {
background: transparent;
border: none;
color: var(--muted);
font-size: 16px;
cursor: pointer;
padding: 4px;
transition: color 0.15s;
}
.btn-del:hover { color: var(--red); }
/* ── Total & Checkout ── */
.checkout-bar {
position: sticky; bottom: 0;
background: var(--card);
border-top: 1px solid var(--border);
padding: 16px;
z-index: 100;
}
.total-row {
display: flex; justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.total-label { font-size: 13px; color: var(--muted); }
.total-amount {
font-family: 'JetBrains Mono', monospace;
font-size: 22px;
font-weight: 700;
color: var(--accent);
}
.cash-input-row {
display: flex; gap: 8px;
margin-bottom: 12px;
align-items: center;
}
.cash-input-row label { color: var(--muted); font-size: 12px; white-space: nowrap; }
.cash-input {
flex: 1;
background: var(--card2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
color: var(--text);
font-size: 16px;
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
outline: none;
text-align: right;
}
.cash-input:focus { border-color: var(--accent); }
.change-row {
display: flex; justify-content: space-between;
margin-bottom: 12px;
padding: 8px 12px;
background: rgba(16,185,129,0.1);
border-radius: 8px;
display: none;
}
.change-row.show { display: flex; }
.change-row span { font-size: 13px; color: var(--green); }
.change-row strong {
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
color: var(--green);
}
.btn-checkout {
width: 100%;
background: linear-gradient(135deg, var(--accent), #d97706);
border: none;
border-radius: 12px;
padding: 16px;
color: #000;
font-size: 16px;
font-weight: 700;
font-family: 'Noto Sans Myanmar', sans-serif;
cursor: pointer;
transition: all 0.2s;
}
.btn-checkout:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(245,158,11,0.4); }
.btn-checkout:active { transform: scale(0.98); }
.btn-checkout:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
/* ── Modal (Receipt) ── */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.8);
z-index: 200;
display: flex; align-items: flex-end; justify-content: center;
display: none;
}
.modal-overlay.show { display: flex; animation: fadeIn 0.2s; }
.receipt-modal {
background: var(--card);
border-radius: 20px 20px 0 0;
padding: 24px 20px;
width: 100%; max-width: 480px;
max-height: 90vh;
overflow-y: auto;
animation: slideUp2 0.3s ease;
}
@keyframes slideUp2 {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.receipt-header {
text-align: center; margin-bottom: 20px;
}
.receipt-header h2 { font-size: 18px; margin-bottom: 4px; }
.receipt-header p { color: var(--muted); font-size: 12px; font-family: 'JetBrains Mono', monospace; }
.receipt-body {
background: #0f172a;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
font-family: 'JetBrains Mono', monospace;
}
.r-row {
display: flex; justify-content: space-between;
font-size: 12px;
padding: 4px 0;
border-bottom: 1px dashed rgba(255,255,255,0.06);
}
.r-row:last-child { border: none; }
.r-total {
font-size: 15px; font-weight: 700;
color: var(--accent);
padding-top: 8px;
border-top: 1px solid var(--border);
margin-top: 4px;
}
.receipt-actions {
display: grid; grid-template-columns: 1fr 1fr;
gap: 10px;
}
.btn-print {
background: var(--scan);
border: none; border-radius: 10px;
padding: 13px; color: white;
font-size: 14px; font-weight: 600;
cursor: pointer;
font-family: 'Noto Sans Myanmar', sans-serif;
}
.btn-new {
background: var(--green);
border: none; border-radius: 10px;
padding: 13px; color: white;
font-size: 14px; font-weight: 600;
cursor: pointer;
font-family: 'Noto Sans Myanmar', sans-serif;
}
/* Print styles */
@media print {
body * { visibility: hidden; }
#printArea, #printArea * { visibility: visible; }
#printArea {
position: fixed; left: 0; top: 0;
width: 80mm;
font-family: monospace;
font-size: 12px;
color: #000;
}
}
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<div class="logo-sm">🛒</div>
<div>
<h2>Cashier POS</h2>
<p id="cashierName">Loading...</p>
</div>
</div>
<button class="btn-logout" onclick="logout()">ထွက်မည်</button>
</div>
<!-- Scanner -->
<div class="scanner-section">
<button class="scan-toggle" id="scanBtn" onclick="toggleScanner()">
<span id="scanIcon">📷</span>
<span id="scanText">ကင်မရာဖြင့် Scan လုပ်မည်</span>
</button>
<div class="scanner-wrap" id="scannerWrap">
<div id="reader"></div>
<div class="scan-overlay">
<div class="scan-frame">
<div class="scan-line"></div>
</div>
</div>
</div>
<div class="manual-input">
<input type="text" id="barcodeInput" placeholder="Barcode ကိုယ်တိုင်ရိုက်ထည့်ပါ..."
inputmode="numeric" onkeydown="if(event.key==='Enter') manualSearch()">
<button class="btn-search" onclick="manualSearch()">🔍</button>
</div>
</div>
<!-- Product Toast -->
<div class="product-toast" id="productToast">
<span class="toast-icon" id="toastIcon">📦</span>
<div class="toast-info">
<h3 id="toastName"></h3>
<p id="toastPrice"></p>
<p class="toast-err" id="toastErr"></p>
</div>
</div>
<!-- Cart -->
<div class="cart-section">
<div class="cart-header">
<div class="cart-title">
🛒 Cart <span class="cart-badge" id="cartBadge">0</span>
</div>
<button class="btn-clear" onclick="clearCart()">အားလုံးဖျက်</button>
</div>
<div id="cartEmpty" class="cart-empty">
<span>🛍️</span>
Barcode Scan လုပ်ပြီး ကုန်ပစ္စည်း ထည့်ပါ
</div>
<div id="cartItems"></div>
</div>
<!-- Checkout bar -->
<div class="checkout-bar">
<div class="total-row">
<span class="total-label">စုစုပေါင်းဈေး</span>
<span class="total-amount" id="totalDisplay">0 ကျပ်</span>
</div>
<div class="cash-input-row">
<label>ငွေပေး</label>
<input type="number" class="cash-input" id="cashInput"
placeholder="0" oninput="calcChange()" inputmode="numeric">
</div>
<div class="change-row" id="changeRow">
<span>အကြွေ</span>
<strong id="changeDisplay">0 ကျပ်</strong>
</div>
<button class="btn-checkout" id="checkoutBtn" onclick="checkout()" disabled>
💰 ငွေရှင်းမည်
</button>
</div>
<!-- Receipt Modal -->
<div class="modal-overlay" id="receiptModal">
<div class="receipt-modal">
<div class="receipt-header">
<h2>✅ ငွေရှင်းပြီးပါပြီ</h2>
<p id="receiptDate"></p>
</div>
<div class="receipt-body" id="receiptBody"></div>
<div class="receipt-actions">
<button class="btn-print" onclick="printReceipt()">🖨️ Print</button>
<button class="btn-new" onclick="newSale()">🔄 အသစ်ဆက်မည်</button>
</div>
</div>
</div>
<div id="printArea" style="display:none"></div>
<script>
let cart = [];
let scanner = null;
let scannerActive = false;
let lastSaleData = null;
let scanCooldown = false;
// Init
document.addEventListener('DOMContentLoaded', async () => {
const r = await fetch('/api/me');
if (!r.ok) { window.location.href = '/login'; return; }
const d = await r.json();
document.getElementById('cashierName').textContent = d.username;
});
// ── Scanner ────────────────────────────────────────────────────────────────
function toggleScanner() {
if (!scannerActive) startScanner(); else stopScanner();
}
function startScanner() {
const wrap = document.getElementById('scannerWrap');
wrap.classList.add('visible');
document.getElementById('scanBtn').classList.add('active');
document.getElementById('scanText').textContent = 'Scanner ပိတ်မည်';
document.getElementById('scanIcon').textContent = '⏹️';
scannerActive = true;
scanner = new Html5Qrcode("reader");
scanner.start(
{ facingMode: "environment" },
{ fps: 10, qrbox: { width: 250, height: 100 } },
(code) => {
if (scanCooldown) return;
scanCooldown = true;
lookupBarcode(code);
setTimeout(() => { scanCooldown = false; }, 2000);
},
() => {}
).catch(err => {
showToast(null, 'ကင်မရာ ဖွင့်၍မရပါ: ' + err);
});
}
function stopScanner() {
if (scanner) {
scanner.stop().catch(() => {});
scanner = null;
}
scannerActive = false;
document.getElementById('scannerWrap').classList.remove('visible');
document.getElementById('scanBtn').classList.remove('active');
document.getElementById('scanText').textContent = 'ကင်မရာဖြင့် Scan လုပ်မည်';
document.getElementById('scanIcon').textContent = '📷';
}
function manualSearch() {
const v = document.getElementById('barcodeInput').value.trim();
if (!v) return;
lookupBarcode(v);
document.getElementById('barcodeInput').value = '';
}
// ── Barcode Lookup ─────────────────────────────────────────────────────────
async function lookupBarcode(barcode) {
// Vibrate on scan
if (navigator.vibrate) navigator.vibrate(50);
try {
const r = await fetch(`/api/products/scan/${encodeURIComponent(barcode)}`);
if (r.ok) {
const product = await r.json();
showToast(product, null);
addToCart(product);
} else {
showToast(null, `"${barcode}" — ကုန်ပစ္စည်း မတွေ့ပါ။ Admin မှ ထည့်ပါ။`);
}
} catch(e) {
showToast(null, 'ချိတ်ဆက်မှု ပြဿနာ');
}
}
function showToast(product, err) {
const t = document.getElementById('productToast');
const icon = document.getElementById('toastIcon');
const name = document.getElementById('toastName');
const price = document.getElementById('toastPrice');
const errEl = document.getElementById('toastErr');
t.className = 'product-toast';
if (product) {
icon.textContent = '✅';
name.textContent = product.name;
price.textContent = `${product.price.toLocaleString()} ကျပ်`;
errEl.textContent = '';
t.classList.add('show');
} else {
icon.textContent = '❌';
name.textContent = 'မတွေ့ပါ';
price.textContent = '';
errEl.textContent = err;
t.classList.add('show', 'error');
}
clearTimeout(t._timer);
t._timer = setTimeout(() => t.classList.remove('show', 'error'), 3000);
}
// ── Cart ───────────────────────────────────────────────────────────────────
function addToCart(product) {
const existing = cart.find(i => i.product_id === product.id);
if (existing) {
existing.quantity++;
} else {
cart.push({
product_id: product.id,
name: product.name,
price: product.price,
quantity: 1
});
}
renderCart();
}
function changeQty(idx, delta) {
cart[idx].quantity += delta;
if (cart[idx].quantity <= 0) cart.splice(idx, 1);
renderCart();
}
function removeItem(idx) {
cart.splice(idx, 1);
renderCart();
}
function clearCart() {
if (cart.length === 0) return;
if (confirm('Cart ကို အားလုံးဖျက်မည်လား?')) {
cart = [];
renderCart();
}
}
function renderCart() {
const el = document.getElementById('cartItems');
const empty = document.getElementById('cartEmpty');
const badge = document.getElementById('cartBadge');
const total = cart.reduce((s, i) => s + i.price * i.quantity, 0);
const count = cart.reduce((s, i) => s + i.quantity, 0);
badge.textContent = count;
document.getElementById('totalDisplay').textContent = total.toLocaleString() + ' ကျပ်';
document.getElementById('checkoutBtn').disabled = cart.length === 0;
calcChange();
if (cart.length === 0) {
empty.style.display = 'block';
el.innerHTML = '';
return;
}
empty.style.display = 'none';
el.innerHTML = cart.map((item, i) => `
<div class="cart-item">
<div class="item-info">
<div class="item-name">${item.name}</div>
<div class="item-price">${item.price.toLocaleString()} × ${item.quantity}</div>
</div>
<div class="qty-ctrl">
<button class="qty-btn minus" onclick="changeQty(${i}, -1)">−</button>
<span class="qty-num">${item.quantity}</span>
<button class="qty-btn plus" onclick="changeQty(${i}, 1)">+</button>
</div>
<div class="item-total">${(item.price * item.quantity).toLocaleString()}</div>
<button class="btn-del" onclick="removeItem(${i})">🗑️</button>
</div>
`).join('');
}
function calcChange() {
const total = cart.reduce((s, i) => s + i.price * i.quantity, 0);
const cash = parseFloat(document.getElementById('cashInput').value) || 0;
const change = cash - total;
const row = document.getElementById('changeRow');
if (cash > 0 && cash >= total) {
document.getElementById('changeDisplay').textContent = change.toLocaleString() + ' ကျပ်';
row.classList.add('show');
} else {
row.classList.remove('show');
}
}
// ── Checkout ───────────────────────────────────────────────────────────────
async function checkout() {
if (cart.length === 0) return;
const total = cart.reduce((s, i) => s + i.price * i.quantity, 0);
const cash = parseFloat(document.getElementById('cashInput').value) || total;
if (cash < total) {
alert('ငွေပေးသည် စုစုပေါင်းထက် နည်းနေသည်');
return;
}
document.getElementById('checkoutBtn').disabled = true;
document.getElementById('checkoutBtn').textContent = '⏳ လုပ်ဆောင်နေသည်...';
try {
const r = await fetch('/api/sales', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart, cash_received: cash })
});
const data = await r.json();
if (r.ok) {
lastSaleData = data;
showReceipt(data);
} else {
alert('Error: ' + data.error);
}
} catch(e) {
alert('ချိတ်ဆက်မှု ပြဿနာ');
}
document.getElementById('checkoutBtn').disabled = false;
document.getElementById('checkoutBtn').textContent = '💰 ငွေရှင်းမည်';
}
// ── Receipt ────────────────────────────────────────────────────────────────
function showReceipt(data) {
stopScanner();
document.getElementById('receiptDate').textContent = data.created_at;
const rows = data.items.map(i =>
`<div class="r-row"><span>${i.name} ×${i.quantity}</span><span>${(i.price_at_time * i.quantity).toLocaleString()} ကျပ်</span></div>`
).join('');
document.getElementById('receiptBody').innerHTML = `
${rows}
<div class="r-row r-total"><span>စုစုပေါင်း</span><span>${data.total.toLocaleString()} ကျပ်</span></div>
<div class="r-row"><span>ငွေပေး</span><span>${data.cash.toLocaleString()} ကျပ်</span></div>
<div class="r-row" style="color:var(--green)"><span>အကြွေ</span><span>${data.change.toLocaleString()} ကျပ်</span></div>
<div class="r-row" style="color:var(--muted);font-size:11px;margin-top:8px"><span>Sale #${data.sale_id}</span><span>${data.created_at}</span></div>
`;
document.getElementById('receiptModal').classList.add('show');
}
function printReceipt() {
if (!lastSaleData) return;
const d = lastSaleData;
const lines = d.items.map(i =>
`${i.name.substring(0,18).padEnd(18)} x${i.quantity}\n${''.padEnd(18)} ${(i.price_at_time*i.quantity).toLocaleString().padStart(8)} ကျပ်`
).join('\n');
document.getElementById('printArea').innerHTML = `
<div style="text-align:center;font-family:monospace;font-size:12px;padding:10px">
<h2 style="font-size:16px">ကုန်စုံဆိုင် POS</h2>
<p>${d.created_at}</p>
<p>Sale #${d.sale_id}</p>
<hr>
<pre style="text-align:left">${lines}</pre>
<hr>
<p><b>စုစုပေါင်း: ${d.total.toLocaleString()} ကျပ်</b></p>
<p>ငွေပေး: ${d.cash.toLocaleString()} ကျပ်</p>
<p>အကြွေ: ${d.change.toLocaleString()} ကျပ်</p>
<hr>
<p>ကျေးဇူးတင်ပါသည်</p>
</div>
`;
window.print();
}
function newSale() {
cart = [];
lastSaleData = null;
document.getElementById('cashInput').value = '';
document.getElementById('changeRow').classList.remove('show');
document.getElementById('receiptModal').classList.remove('show');
renderCart();
}
function logout() {
stopScanner();
fetch('/api/logout', { method: 'POST' })
.then(() => window.location.href = '/login');
}
</script>
</body>
</html>