ttup / app.py
Phoe2004's picture
Upload 3 files
ec33f92 verified
from flask import Flask, render_template_string, request, jsonify, session, redirect, url_for
from flask_cors import CORS
import sqlite3
import os
from datetime import datetime, timedelta
from functools import wraps
app = Flask(__name__)
# HuggingFace: SECRET_KEY env var မှ ဖတ်ပါ (fallback ပါ)
app.secret_key = os.environ.get('SECRET_KEY', 'pos-secret-key-change-in-production')
CORS(app)
# HuggingFace Spaces: /data သည် persistent volume (container restart ကြည့်လည် ဒေတာ မပျောက်)
# Local run: current directory ထဲမှာ pos.db ဖန်တီးမည်
DATA_DIR = '/data' if os.path.exists('/data') and os.access('/data', os.W_OK) else '.'
DB_PATH = os.path.join(DATA_DIR, 'pos.db')
# ─── Inline HTML ─────────────────────────────────────────────────────────────
LOGIN_HTML = r"""<!DOCTYPE html>
<html lang="my">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POS Login</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">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0e1a;--card:#111827;--border:#1e293b;--accent:#f59e0b;--text:#f1f5f9;--muted:#64748b}
body{background:var(--bg);min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:'Noto Sans Myanmar',sans-serif;overflow:hidden}
.bg-grid{position:fixed;inset:0;z-index:0;background-image:linear-gradient(rgba(245,158,11,.04) 1px,transparent 1px),linear-gradient(90deg,rgba(245,158,11,.04) 1px,transparent 1px);background-size:40px 40px}
.bg-glow{position:fixed;width:600px;height:600px;background:radial-gradient(circle,rgba(245,158,11,.08) 0%,transparent 70%);top:50%;left:50%;transform:translate(-50%,-50%);z-index:0;animation:pulse 4s ease-in-out infinite}
@keyframes pulse{0%,100%{transform:translate(-50%,-50%) scale(1);opacity:.5}50%{transform:translate(-50%,-50%) scale(1.2);opacity:1}}
.card{position:relative;z-index:1;background:var(--card);border:1px solid var(--border);border-radius:20px;padding:48px 40px;width:100%;max-width:420px;margin:20px;box-shadow:0 25px 60px rgba(0,0,0,.5);animation:slideUp .5s ease}
@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}
.logo{text-align:center;margin-bottom:36px}
.logo-icon{width:64px;height:64px;background:linear-gradient(135deg,var(--accent),#d97706);border-radius:16px;display:flex;align-items:center;justify-content:center;font-size:28px;margin:0 auto 16px;box-shadow:0 8px 24px rgba(245,158,11,.3)}
.logo h1{color:var(--text);font-size:22px;font-weight:700}
.logo p{color:var(--muted);font-size:13px;margin-top:4px;font-family:'JetBrains Mono',monospace}
.form-group{margin-bottom:20px}
label{display:block;color:var(--muted);font-size:12px;font-weight:600;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}
input{width:100%;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:10px;padding:14px 16px;color:var(--text);font-size:15px;font-family:'Noto Sans Myanmar',sans-serif;transition:all .2s;outline:none}
input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(245,158,11,.1)}
.btn-login{width:100%;background:linear-gradient(135deg,var(--accent),#d97706);border:none;border-radius:10px;padding:15px;color:#000;font-size:15px;font-weight:700;font-family:'Noto Sans Myanmar',sans-serif;cursor:pointer;transition:all .2s;margin-top:8px}
.btn-login:hover{transform:translateY(-1px);box-shadow:0 8px 24px rgba(245,158,11,.4)}
.btn-login:disabled{opacity:.6;cursor:not-allowed;transform:none}
.error-msg{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);border-radius:8px;padding:12px 14px;color:#fca5a5;font-size:13px;margin-top:16px;display:none}
.demo-accounts{margin-top:28px;padding-top:24px;border-top:1px solid var(--border)}
.demo-accounts p{color:var(--muted);font-size:11px;text-align:center;margin-bottom:12px;text-transform:uppercase;letter-spacing:1px;font-family:'JetBrains Mono',monospace}
.demo-btns{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.demo-btn{background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:8px;padding:10px;color:var(--muted);font-size:12px;cursor:pointer;transition:all .2s;font-family:'Noto Sans Myanmar',sans-serif;text-align:center}
.demo-btn:hover{border-color:var(--accent);color:var(--accent)}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="bg-glow"></div>
<div class="card">
<div class="logo">
<div class="logo-icon">🛒</div>
<h1>ကုန်စုံဆိုင် POS</h1>
<p>Point of Sale System v1.0</p>
</div>
<div class="form-group">
<label>အသုံးပြုသူအမည်</label>
<input type="text" id="username" placeholder="username" autocomplete="username">
</div>
<div class="form-group">
<label>စကားဝှက်</label>
<input type="password" id="password" placeholder="••••••••" onkeydown="if(event.key==='Enter')doLogin()">
</div>
<button class="btn-login" id="loginBtn" onclick="doLogin()">၀င်ရောက်မည်</button>
<div class="error-msg" id="errMsg"></div>
<div class="demo-accounts">
<p>Demo Accounts</p>
<div class="demo-btns">
<div class="demo-btn" onclick="fillDemo('admin','admin123')">👑 Admin<br><small style="font-family:monospace">admin123</small></div>
<div class="demo-btn" onclick="fillDemo('cashier','cashier123')">💰 Cashier<br><small style="font-family:monospace">cashier123</small></div>
</div>
</div>
</div>
<script>
function fillDemo(u,p){document.getElementById('username').value=u;document.getElementById('password').value=p}
async function doLogin(){
const btn=document.getElementById('loginBtn'),err=document.getElementById('errMsg');
const u=document.getElementById('username').value.trim(),p=document.getElementById('password').value;
if(!u||!p){showErr('အသုံးပြုသူအမည်နှင့် စကားဝှက် ထည့်ပါ');return}
btn.disabled=true;btn.textContent='စစ်ဆေးနေသည်...';err.style.display='none';
try{
const res=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})});
const data=await res.json();
if(res.ok){btn.textContent='✓ ၀င်ရောက်ပြီး...';setTimeout(()=>{window.location.href=data.role==='admin'?'/admin':'/cashier'},400)}
else{showErr(data.error||'Login မအောင်မြင်ပါ');btn.disabled=false;btn.textContent='၀င်ရောက်မည်'}
}catch(e){showErr('ချိတ်ဆက်မှု ပြဿနာ');btn.disabled=false;btn.textContent='၀င်ရောက်မည်'}
}
function showErr(msg){const e=document.getElementById('errMsg');e.textContent=msg;e.style.display='block'}
document.getElementById('username').focus();
</script>
</body>
</html>
"""
CASHIER_HTML = r"""<!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;--blue:#3b82f6;--text:#f1f5f9;--muted:#64748b}
body{background:var(--bg);color:var(--text);font-family:'Noto Sans Myanmar',sans-serif;min-height:100vh;max-width:480px;margin:0 auto;padding-bottom:220px}
/* Header */
.header{background:var(--card);border-bottom:1px solid var(--border);padding:12px 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:14px 16px}
.scan-toggle{width:100%;background:linear-gradient(135deg,var(--blue),#2563eb);border:none;border-radius:12px;padding:14px;color:#fff;font-size:14px;font-weight:600;font-family:'Noto Sans Myanmar',sans-serif;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:10px;box-shadow:0 4px 16px rgba(59,130,246,.3)}
.scan-toggle.active{background:linear-gradient(135deg,var(--red),#dc2626);box-shadow:0 4px 16px rgba(239,68,68,.3)}
.scanner-wrap{display:none;margin-top:10px;border-radius:12px;overflow:hidden;border:2px solid var(--blue);position:relative}
.scanner-wrap.visible{display:block}
#reader{width:100%}
.scan-line-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none}
.scan-frame{width:220px;height:80px;border:2px solid var(--accent);border-radius:6px;position:relative;box-shadow:0 0 0 9999px rgba(0,0,0,.45)}
.scan-anim{position:absolute;left:0;right:0;height:2px;background:var(--accent);animation:scanMove 1.8s ease-in-out infinite}
@keyframes scanMove{0%{top:4px}100%{top:calc(100% - 4px)}}
.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:11px 14px;color:var(--text);font-size:14px;font-family:'JetBrains Mono',monospace;outline:none}
.manual-input input:focus{border-color:var(--blue)}
.btn-search{background:var(--blue);border:none;border-radius:10px;padding:11px 16px;color:#fff;font-size:18px;cursor:pointer}
/* ── Product Info Panel (NEW) ── */
.product-panel{margin:0 16px 14px;border-radius:14px;overflow:hidden;display:none}
.product-panel.show{display:block;animation:fadeSlide .3s ease}
@keyframes fadeSlide{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
.pp-found{background:linear-gradient(135deg,rgba(16,185,129,.12),rgba(16,185,129,.06));border:1px solid rgba(16,185,129,.3)}
.pp-notfound{background:linear-gradient(135deg,rgba(239,68,68,.12),rgba(239,68,68,.06));border:1px solid rgba(239,68,68,.3)}
.pp-header{padding:12px 14px;display:flex;align-items:center;gap:10px}
.pp-icon{font-size:24px}
.pp-title{flex:1}
.pp-name{font-size:14px;font-weight:700;color:var(--text)}
.pp-barcode{font-size:11px;color:var(--muted);font-family:'JetBrains Mono',monospace;margin-top:1px}
.pp-details{display:grid;grid-template-columns:1fr 1fr 1fr;border-top:1px solid rgba(255,255,255,.06)}
.pp-detail{padding:10px 14px;text-align:center}
.pp-detail-label{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.pp-detail-value{font-size:15px;font-weight:700;font-family:'JetBrains Mono',monospace}
.pp-detail-value.price{color:var(--accent)}
.pp-detail-value.stock{color:var(--blue)}
.pp-detail-value.added{color:var(--green)}
.pp-err{padding:12px 14px;display:flex;align-items:center;gap:10px}
.pp-err span{font-size:13px;color:#fca5a5}
.pp-err-hint{font-size:11px;color:var(--muted);margin-top:2px}
/* Cart */
.cart-section{padding:0 16px 14px}
.cart-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.cart-title{font-size:13px;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,.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:28px 16px;color:var(--muted);font-size:13px;background:var(--card2);border-radius:12px;border:1px dashed var(--border)}
.cart-empty span{font-size:32px;display:block;margin-bottom:8px}
.cart-item{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:10px 12px;margin-bottom:8px;display:flex;align-items:center;gap:8px;animation:slideIn .2s ease}
@keyframes slideIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
.item-info{flex:1;min-width:0}
.item-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.item-price{font-size:11px;color:var(--accent);font-family:'JetBrains Mono',monospace;margin-top:2px}
.qty-ctrl{display:flex;align-items:center;gap:6px;flex-shrink:0}
.qty-btn{width:28px;height:28px;border-radius:7px;border:1px solid var(--border);background:var(--card2);color:var(--text);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.qty-btn:active{transform:scale(.88)}
.qty-num{font-family:'JetBrains Mono',monospace;font-weight:700;font-size:14px;min-width:22px;text-align:center}
.item-total{font-family:'JetBrains Mono',monospace;font-weight:700;font-size:12px;color:var(--green);min-width:55px;text-align:right;flex-shrink:0}
.btn-del{background:transparent;border:none;color:var(--muted);font-size:15px;cursor:pointer;padding:2px}
.btn-del:hover{color:var(--red)}
/* Checkout Bar */
.checkout-bar{position:fixed;bottom:0;left:50%;transform:translateX(-50%);width:100%;max-width:480px;background:var(--card);border-top:1px solid var(--border);padding:14px 16px;z-index:100}
.total-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
.total-label{font-size:12px;color:var(--muted)}
.total-amount{font-family:'JetBrains Mono',monospace;font-size:20px;font-weight:700;color:var(--accent)}
.cash-row{display:flex;gap:8px;align-items:center;margin-bottom:10px}
.cash-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:9px 12px;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:none;justify-content:space-between;margin-bottom:10px;padding:7px 12px;background:rgba(16,185,129,.1);border-radius:8px}
.change-row.show{display:flex}
.change-row span{font-size:12px;color:var(--green)}
.change-row strong{font-family:'JetBrains Mono',monospace;font-size:14px;color:var(--green)}
.btn-checkout{width:100%;background:linear-gradient(135deg,var(--accent),#d97706);border:none;border-radius:12px;padding:14px;color:#000;font-size:15px;font-weight:700;font-family:'Noto Sans Myanmar',sans-serif;cursor:pointer;transition:all .2s}
.btn-checkout:disabled{opacity:.4;cursor:not-allowed}
/* Modal */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:200;display:none;align-items:flex-end;justify-content:center}
.modal-overlay.show{display:flex;animation:fadeIn .2s}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.receipt-modal{background:var(--card);border-radius:20px 20px 0 0;padding:22px 18px;width:100%;max-width:480px;max-height:90vh;overflow-y:auto;animation:slideUp2 .3s ease}
@keyframes slideUp2{from{transform:translateY(100%)}to{transform:translateY(0)}}
.receipt-header{text-align:center;margin-bottom:18px}
.receipt-header h2{font-size:17px;margin-bottom:4px}
.receipt-header p{color:var(--muted);font-size:11px;font-family:'JetBrains Mono',monospace}
.receipt-body{background:#0f172a;border-radius:12px;padding:14px;margin-bottom:14px;font-family:'JetBrains Mono',monospace}
.r-row{display:flex;justify-content:space-between;font-size:11px;padding:4px 0;border-bottom:1px dashed rgba(255,255,255,.06)}
.r-row:last-child{border:none}
.r-total{font-size:14px;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(--blue);border:none;border-radius:10px;padding:13px;color:#fff;font-size:13px;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:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:'Noto Sans Myanmar',sans-serif}
@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">...</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-line-overlay">
<div class="scan-frame"><div class="scan-anim"></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 Info Panel -->
<div class="product-panel" id="productPanel">
<!-- filled by JS -->
</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-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=[], scanner=null, scannerActive=false, lastSaleData=null, scanCooldown=false;
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(){!scannerActive?startScanner():stopScanner()}
function startScanner(){
document.getElementById('scannerWrap').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:240,height:80}},
(code)=>{
if(scanCooldown)return;
scanCooldown=true;
lookupBarcode(code);
setTimeout(()=>{scanCooldown=false},2000);
},
()=>{}
).catch(err=>showPanelError('unknown','ကင်မရာ ဖွင့်၍မရပါ'));
}
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){
if(navigator.vibrate)navigator.vibrate(60);
try{
const r=await fetch(`/api/products/scan/${encodeURIComponent(barcode)}`);
if(r.ok){
const p=await r.json();
showPanelFound(p);
addToCart(p);
}else{
showPanelError(barcode,'ကုန်ပစ္စည်း မတွေ့ပါ — Admin မှ ထည့်ပါ');
}
}catch(e){showPanelError(barcode,'ချိတ်ဆက်မှု ပြဿနာ')}
}
// ── Product Panel ──────────────────────────────────────────────────────────
function showPanelFound(product){
const panel=document.getElementById('productPanel');
// Count how many already in cart
const inCart=cart.find(i=>i.product_id===product.id);
const cartQty=(inCart?inCart.quantity:0)+1;
panel.className='product-panel show pp-found';
panel.innerHTML=`
<div class="pp-header">
<div class="pp-icon">✅</div>
<div class="pp-title">
<div class="pp-name">${product.name}</div>
<div class="pp-barcode">${product.barcode}</div>
</div>
</div>
<div class="pp-details">
<div class="pp-detail">
<div class="pp-detail-label">ဈေးနှုန်း</div>
<div class="pp-detail-value price">${product.price.toLocaleString()}<span style="font-size:10px;color:var(--muted)"> ကျပ်</span></div>
</div>
<div class="pp-detail">
<div class="pp-detail-label">Stock</div>
<div class="pp-detail-value stock">${product.stock}<span style="font-size:10px;color:var(--muted)"> ခု</span></div>
</div>
<div class="pp-detail">
<div class="pp-detail-label">Cart ထဲ</div>
<div class="pp-detail-value added">${cartQty}<span style="font-size:10px;color:var(--muted)"> ခု</span></div>
</div>
</div>
`;
clearTimeout(panel._t);
panel._t=setTimeout(()=>{panel.classList.remove('show')},4000);
}
function showPanelError(barcode,msg){
const panel=document.getElementById('productPanel');
panel.className='product-panel show pp-notfound';
panel.innerHTML=`
<div class="pp-err">
<div style="font-size:24px">❌</div>
<div>
<div class="pp-err span" style="font-size:13px;font-weight:700;color:#fca5a5">${msg}</div>
<div class="pp-err-hint">${barcode!=='unknown'?'Barcode: '+barcode:''}</div>
</div>
</div>
`;
clearTimeout(panel._t);
panel._t=setTimeout(()=>{panel.classList.remove('show')},4000);
}
// ── Cart ───────────────────────────────────────────────────────────────────
function addToCart(product){
const ex=cart.find(i=>i.product_id===product.id);
if(ex){ex.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'),empty=document.getElementById('cartEmpty');
const total=cart.reduce((s,i)=>s+i.price*i.quantity,0);
const count=cart.reduce((s,i)=>s+i.quantity,0);
document.getElementById('cartBadge').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" onclick="changeQty(${i},-1)">−</button>
<span class="qty-num">${item.quantity}</span>
<button class="qty-btn" 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 row=document.getElementById('changeRow');
if(cash>0&&cash>=total){document.getElementById('changeDisplay').textContent=(cash-total).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}
const btn=document.getElementById('checkoutBtn');
btn.disabled=true;btn.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('ချိတ်ဆက်မှု ပြဿနာ')}
btn.disabled=false;btn.textContent='💰 ငွေရှင်းမည်';
}
function showReceipt(data){
stopScanner();
document.getElementById('receiptDate').textContent=data.created_at;
document.getElementById('receiptBody').innerHTML=
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('')+
`<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:10px;margin-top:6px"><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;
document.getElementById('printArea').innerHTML=`
<div style="text-align:center;font-family:monospace;font-size:12px;padding:10px">
<h2>ကုန်စုံဆိုင် POS</h2><p>${d.created_at}</p><p>Sale #${d.sale_id}</p><hr>
<pre style="text-align:left">${d.items.map(i=>`${i.name.substring(0,16).padEnd(16)} x${i.quantity}\n${' '.repeat(16)} ${(i.price_at_time*i.quantity).toLocaleString().padStart(8)} ကျပ်`).join('\n')}</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>
"""
ADMIN_HTML = r"""<!DOCTYPE html>
<html lang="my">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - POS Dashboard</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;--blue:#3b82f6;--purple:#8b5cf6;--text:#f1f5f9;--muted:#64748b}
body{background:var(--bg);color:var(--text);font-family:'Noto Sans Myanmar',sans-serif;min-height:100vh}
/* ── Mobile-first: single column, no sidebar by default ── */
.sidebar{
position:fixed;left:0;top:0;bottom:0;width:240px;
background:var(--card);border-right:1px solid var(--border);
display:flex;flex-direction:column;z-index:200;
transform:translateX(-100%);transition:transform .3s ease;
}
.sidebar.open{transform:translateX(0)}
.overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:199}
.overlay.show{display:block}
/* Desktop: sidebar always visible */
@media(min-width:769px){
.sidebar{transform:translateX(0)}
.overlay{display:none!important}
.main{margin-left:240px}
.mob-header{display:none!important}
}
.sidebar-logo{padding:18px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
.logo-icon{width:40px;height:40px;background:linear-gradient(135deg,var(--accent),#d97706);border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.sidebar-logo h1{font-size:14px;font-weight:700;line-height:1.3}
.sidebar-logo p{font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace}
.nav{padding:10px 8px;flex:1}
.nav-item{display:flex;align-items:center;gap:10px;padding:11px 12px;border-radius:10px;cursor:pointer;color:var(--muted);font-size:13px;transition:all .15s;margin-bottom:2px;border:none;background:none;width:100%;font-family:'Noto Sans Myanmar',sans-serif;text-align:left}
.nav-item:hover{background:var(--card2);color:var(--text)}
.nav-item.active{background:rgba(245,158,11,.1);color:var(--accent)}
.nav-icon{font-size:16px}
.sidebar-footer{padding:10px 8px;border-top:1px solid var(--border)}
.user-card{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--card2);border-radius:10px;margin-bottom:8px}
.user-avatar{width:32px;height:32px;background:linear-gradient(135deg,var(--purple),#7c3aed);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
.user-card p{font-size:12px;font-weight:600}
.user-card span{font-size:10px;color:var(--accent)}
.btn-logout{width:100%;background:transparent;border:1px solid var(--border);border-radius:8px;padding:9px;color:var(--muted);font-size:12px;cursor:pointer;font-family:'Noto Sans Myanmar',sans-serif;transition:all .15s}
.btn-logout:hover{border-color:var(--red);color:var(--red)}
/* Mobile header */
.mob-header{display:flex;background:var(--card);border-bottom:1px solid var(--border);padding:12px 16px;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
.hamburger{background:none;border:none;color:var(--text);font-size:22px;cursor:pointer;padding:2px 6px}
/* Main */
.main{min-height:100vh;padding:20px 16px}
/* Page header */
.page-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.page-title{font-size:18px;font-weight:700}
.page-sub{font-size:11px;color:var(--muted);margin-top:2px}
/* Stats grid */
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px}
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:14px;position:relative;overflow:hidden}
.stat-card::after{content:'';position:absolute;top:0;right:0;width:50px;height:50px;border-radius:0 14px 0 50px;opacity:.12}
.stat-card.gold::after{background:var(--accent)}
.stat-card.green::after{background:var(--green)}
.stat-card.blue::after{background:var(--blue)}
.stat-card.purple::after{background:var(--purple)}
.stat-icon{font-size:20px;margin-bottom:8px}
.stat-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.stat-value{font-size:18px;font-weight:700;font-family:'JetBrains Mono',monospace;margin-top:3px;line-height:1}
.stat-card.gold .stat-value{color:var(--accent)}
.stat-card.green .stat-value{color:var(--green)}
.stat-card.blue .stat-value{color:var(--blue)}
.stat-card.purple .stat-value{color:var(--purple)}
.stat-sub{font-size:10px;color:var(--muted);margin-top:3px;font-family:'JetBrains Mono',monospace}
/* Chart */
.chart-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:20px}
.chart-title{font-size:12px;font-weight:600;margin-bottom:14px}
.bar-chart{display:flex;align-items:flex-end;gap:5px;height:90px}
.bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;height:100%;justify-content:flex-end}
.bar{width:100%;background:linear-gradient(to top,var(--accent),rgba(245,158,11,.4));border-radius:4px 4px 0 0;min-height:4px}
.bar-label{font-size:9px;color:var(--muted);font-family:'JetBrains Mono',monospace}
/* Table card */
.table-card{background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-bottom:20px}
.table-head{padding:12px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}
.table-head h3{font-size:12px;font-weight:600}
.btn-add{background:var(--accent);border:none;border-radius:8px;padding:8px 14px;color:#000;font-size:12px;font-weight:700;cursor:pointer;font-family:'Noto Sans Myanmar',sans-serif;display:flex;align-items:center;gap:6px}
.search-bar{padding:10px 14px;border-bottom:1px solid var(--border)}
.search-bar input{width:100%;background:var(--card2);border:1px solid var(--border);border-radius:8px;padding:9px 12px;color:var(--text);font-size:13px;font-family:'Noto Sans Myanmar',sans-serif;outline:none}
.search-bar input:focus{border-color:var(--blue)}
.product-row{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid rgba(30,41,59,.5);transition:background .1s}
.product-row:last-child{border:none}
.product-row:hover{background:rgba(255,255,255,.02)}
.prod-info{flex:1;min-width:0}
.prod-name{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.prod-barcode{font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;margin-top:1px}
.prod-price{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;color:var(--accent);min-width:70px;text-align:right}
.prod-stock{font-size:10px;color:var(--muted);text-align:center;min-width:32px}
.btn-edit,.btn-del2{border:none;border-radius:7px;padding:6px 9px;font-size:12px;cursor:pointer;transition:all .15s}
.btn-edit{background:rgba(59,130,246,.15);color:var(--blue)}
.btn-del2{background:rgba(239,68,68,.15);color:var(--red)}
/* Sales */
.period-tabs{display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap}
.period-tab{padding:7px 14px;border-radius:20px;font-size:12px;cursor:pointer;border:1px solid var(--border);color:var(--muted);background:none;font-family:'Noto Sans Myanmar',sans-serif;transition:all .15s}
.period-tab.active{background:var(--accent);border-color:var(--accent);color:#000;font-weight:700}
.sale-row{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid rgba(30,41,59,.5);cursor:pointer;transition:background .1s}
.sale-row:hover{background:rgba(255,255,255,.02)}
.sale-row:last-child{border:none}
.sale-id{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);min-width:36px}
.sale-info{flex:1}
.sale-time{font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace}
.sale-cnt{font-size:12px;color:var(--text)}
.sale-total{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;color:var(--green)}
.top-prod-row{display:flex;align-items:center;gap:8px;padding:9px 0;border-bottom:1px solid rgba(30,41,59,.5)}
.top-prod-row:last-child{border:none}
.top-rank{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);min-width:18px}
.top-name{flex:1;font-size:12px}
.top-qty{font-size:11px;color:var(--muted);min-width:36px;text-align:right}
.top-rev{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--accent);min-width:70px;text-align:right}
/* Modal */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:300;display:none;align-items:flex-end;justify-content:center;padding:0}
.modal-overlay.show{display:flex;animation:fadeIn .2s}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.modal{background:var(--card);border-radius:20px 20px 0 0;padding:22px 18px 30px;width:100%;max-width:480px;animation:slideUp .3s ease;max-height:92vh;overflow-y:auto}
@keyframes slideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}
.modal-handle{width:36px;height:4px;background:var(--border);border-radius:2px;margin:0 auto 18px}
.modal h3{font-size:15px;margin-bottom:18px;font-weight:700}
.form-group{margin-bottom:14px}
.form-group label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
.form-group input{width:100%;background:var(--card2);border:1px solid var(--border);border-radius:10px;padding:12px 14px;color:var(--text);font-size:14px;font-family:'Noto Sans Myanmar',sans-serif;outline:none}
.form-group input:focus{border-color:var(--accent)}
.form-group input[type=number]{font-family:'JetBrains Mono',monospace}
/* Barcode scan row in modal */
.barcode-row{display:flex;gap:8px;align-items:flex-end}
.barcode-row .form-group{flex:1;margin:0}
.btn-scan-barcode{background:var(--blue);border:none;border-radius:10px;padding:12px 14px;color:#fff;font-size:18px;cursor:pointer;flex-shrink:0;align-self:flex-end;height:46px;display:flex;align-items:center;justify-content:center}
.mini-scanner-wrap{display:none;margin-top:10px;border-radius:12px;overflow:hidden;border:2px solid var(--blue);position:relative}
.mini-scanner-wrap.visible{display:block}
.mini-scan-frame{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none}
.mini-frame-box{width:180px;height:60px;border:2px solid var(--accent);border-radius:5px;position:relative;box-shadow:0 0 0 9999px rgba(0,0,0,.4)}
.mini-scan-line{position:absolute;left:0;right:0;height:2px;background:var(--accent);animation:scanMove 1.8s ease-in-out infinite}
@keyframes scanMove{0%{top:3px}100%{top:calc(100% - 3px)}}
.modal-btns{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:18px}
.btn-cancel{background:var(--card2);border:1px solid var(--border);border-radius:10px;padding:13px;color:var(--text);font-size:13px;cursor:pointer;font-family:'Noto Sans Myanmar',sans-serif}
.btn-save{background:var(--accent);border:none;border-radius:10px;padding:13px;color:#000;font-size:13px;font-weight:700;cursor:pointer;font-family:'Noto Sans Myanmar',sans-serif}
.err-msg{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.3);border-radius:8px;padding:10px 12px;color:#fca5a5;font-size:12px;margin-top:10px;display:none}
/* Sale detail */
.sale-detail-item{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(30,41,59,.5);font-size:12px}
.sale-detail-item:last-child{border:none}
/* Empty / Loading */
.empty{text-align:center;padding:28px;color:var(--muted);font-size:12px}
.empty span{font-size:28px;display:block;margin-bottom:8px}
.loading{text-align:center;padding:20px;color:var(--muted);font-size:12px}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-logo">
<div class="logo-icon">🛒</div>
<div><h1>ကုန်စုံဆိုင် POS</h1><p>Admin Panel</p></div>
</div>
<nav class="nav">
<button class="nav-item active" data-page="dashboard" onclick="showPage('dashboard')"><span class="nav-icon">📊</span>Dashboard</button>
<button class="nav-item" data-page="products" onclick="showPage('products')"><span class="nav-icon">📦</span>ကုန်ပစ္စည်းများ</button>
<button class="nav-item" data-page="sales" onclick="showPage('sales')"><span class="nav-icon">🧾</span>ရောင်းချမှတ်တမ်း</button>
</nav>
<div class="sidebar-footer">
<div class="user-card">
<div class="user-avatar">👑</div>
<div><p id="adminName">Admin</p><span>Administrator</span></div>
</div>
<button class="btn-logout" onclick="logout()">🚪 ထွက်မည်</button>
</div>
</div>
<div class="overlay" id="overlay" onclick="closeSidebar()"></div>
<!-- Mobile Header -->
<div class="mob-header">
<button class="hamburger" onclick="openSidebar()">☰</button>
<span style="font-size:14px;font-weight:600" id="mobTitle">Dashboard</span>
<span style="font-size:20px">🛒</span>
</div>
<!-- Main -->
<main class="main">
<!-- DASHBOARD -->
<div id="page-dashboard">
<div class="page-header">
<div><div class="page-title">Dashboard</div><div class="page-sub" id="dashDate"></div></div>
<button onclick="loadDashboard()" style="background:none;border:none;color:var(--muted);font-size:20px;cursor:pointer">↻</button>
</div>
<div class="stats-grid">
<div class="stat-card gold"><div class="stat-icon">💰</div><div class="stat-label">ယနေ့ ရောင်းရငွေ</div><div class="stat-value" id="todayRev">-</div><div class="stat-sub" id="todayCnt">-</div></div>
<div class="stat-card green"><div class="stat-icon">📈</div><div class="stat-label">ဤသတ်ပတ် ရောင်းရငွေ</div><div class="stat-value" id="weekRev">-</div><div class="stat-sub" id="weekCnt">-</div></div>
<div class="stat-card blue"><div class="stat-icon">📦</div><div class="stat-label">ကုန်မျိုး</div><div class="stat-value" id="prodCount">-</div><div class="stat-sub">products</div></div>
<div class="stat-card purple"><div class="stat-icon">⭐</div><div class="stat-label">Top Seller</div><div class="stat-value" style="font-size:12px;font-family:'Noto Sans Myanmar'" id="topSeller">-</div><div class="stat-sub" id="topSellerQty">-</div></div>
</div>
<div class="chart-card">
<div class="chart-title">📊 ၇ ရက်အတွင်း ရောင်းချမှု</div>
<div class="bar-chart" id="barChart"><div class="loading">Loading...</div></div>
</div>
<div class="table-card">
<div class="table-head"><h3>🏆 ဤသတ်ပတ် အရောင်းကောင်းဆုံး</h3></div>
<div style="padding:0 14px" id="topProdsEl"><div class="loading">Loading...</div></div>
</div>
</div>
<!-- PRODUCTS -->
<div id="page-products" style="display:none">
<div class="page-header">
<div><div class="page-title">ကုန်ပစ္စည်းများ</div><div class="page-sub" id="prodCountLabel">-</div></div>
<button class="btn-add" onclick="openAddModal()">+ ထည့်မည်</button>
</div>
<div class="table-card">
<div class="search-bar">
<input type="text" id="prodSearch" placeholder="🔍 နာမည် သို့မဟုတ် Barcode ရှာပါ..." oninput="filterProducts()">
</div>
<div id="productsList"><div class="loading">Loading...</div></div>
</div>
</div>
<!-- SALES -->
<div id="page-sales" style="display:none">
<div class="page-header">
<div><div class="page-title">ရောင်းချမှတ်တမ်း</div></div>
</div>
<div class="period-tabs">
<button class="period-tab active" onclick="loadSales('today',this)">ယနေ့</button>
<button class="period-tab" onclick="loadSales('week',this)">ဤသတ်ပတ်</button>
<button class="period-tab" onclick="loadSales('month',this)">ဤလ</button>
<button class="period-tab" onclick="loadSales('all',this)">အားလုံး</button>
</div>
<div class="stats-grid" style="margin-bottom:14px">
<div class="stat-card gold"><div class="stat-label">ရောင်းရငွေ</div><div class="stat-value" id="salesRevEl">0</div></div>
<div class="stat-card green"><div class="stat-label">ရောင်းကြိမ်</div><div class="stat-value" id="salesCntEl">0</div></div>
</div>
<div class="table-card"><div id="salesList"><div class="loading">Loading...</div></div></div>
</div>
</main>
<!-- Add/Edit Product Modal (with camera scanner) -->
<div class="modal-overlay" id="productModal">
<div class="modal">
<div class="modal-handle"></div>
<h3 id="modalTitle">ကုန်ပစ္စည်းအသစ် ထည့်မည်</h3>
<!-- Barcode row with scan button -->
<div class="form-group">
<label>Barcode</label>
<div class="barcode-row">
<input type="text" id="fBarcode" placeholder="8850006111019" inputmode="numeric" style="font-family:'JetBrains Mono',monospace">
<button class="btn-scan-barcode" onclick="toggleModalScanner()" id="modalScanBtn" title="Camera Scan">📷</button>
</div>
<!-- Mini scanner in modal -->
<div class="mini-scanner-wrap" id="miniScannerWrap">
<div id="miniReader"></div>
<div class="mini-scan-frame">
<div class="mini-frame-box"><div class="mini-scan-line"></div></div>
</div>
</div>
</div>
<div class="form-group">
<label>ကုန်ပစ္စည်းနာမည်</label>
<input type="text" id="fName" placeholder="နာမည်ထည့်ပါ...">
</div>
<div class="form-group">
<label>ဈေးနှုန်း (ကျပ်)</label>
<input type="number" id="fPrice" placeholder="0" min="0">
</div>
<div class="form-group">
<label>Stock (ခု)</label>
<input type="number" id="fStock" placeholder="0" min="0">
</div>
<div class="err-msg" id="modalErr"></div>
<div class="modal-btns">
<button class="btn-cancel" onclick="closeProductModal()">ပယ်ဖျက်</button>
<button class="btn-save" onclick="saveProduct()">သိမ်းမည်</button>
</div>
</div>
</div>
<!-- Sale Detail Modal -->
<div class="modal-overlay" id="saleModal">
<div class="modal">
<div class="modal-handle"></div>
<h3>🧾 ရောင်းမှတ်တမ်း အသေးစိတ်</h3>
<div id="saleDetailContent"><div class="loading">Loading...</div></div>
<div style="margin-top:16px">
<button class="btn-cancel" style="width:100%" onclick="closeModal('saleModal')">ပိတ်မည်</button>
</div>
</div>
</div>
<script>
let allProducts=[], editingProductId=null, miniScanner=null, miniScannerActive=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();
if(d.role!=='admin'){window.location.href='/cashier';return}
document.getElementById('adminName').textContent=d.username;
document.getElementById('dashDate').textContent=new Date().toLocaleDateString('my-MM');
loadDashboard();
});
// ── Navigation ─────────────────────────────────────────────────────────────
function showPage(page){
['dashboard','products','sales'].forEach(p=>{
document.getElementById(`page-${p}`).style.display=p===page?'block':'none';
document.querySelector(`[data-page="${p}"]`).classList.toggle('active',p===page);
});
const titles={dashboard:'Dashboard',products:'ကုန်ပစ္စည်းများ',sales:'ရောင်းချမှတ်တမ်း'};
document.getElementById('mobTitle').textContent=titles[page];
closeSidebar();
if(page==='products')loadProducts();
if(page==='sales')loadSales('today');
}
function openSidebar(){document.getElementById('sidebar').classList.add('open');document.getElementById('overlay').classList.add('show')}
function closeSidebar(){document.getElementById('sidebar').classList.remove('open');document.getElementById('overlay').classList.remove('show')}
// ── Dashboard ──────────────────────────────────────────────────────────────
async function loadDashboard(){
try{
const r=await fetch('/api/dashboard');
const d=await r.json();
document.getElementById('todayRev').textContent=fmtK(d.today.rev);
document.getElementById('todayCnt').textContent=d.today.cnt+' ကြိမ်';
document.getElementById('weekRev').textContent=fmtK(d.week.rev);
document.getElementById('weekCnt').textContent=d.week.cnt+' ကြိမ်';
document.getElementById('prodCount').textContent=d.product_count;
if(d.top_products.length>0){
document.getElementById('topSeller').textContent=d.top_products[0].name.substring(0,12);
document.getElementById('topSellerQty').textContent=d.top_products[0].qty+' ခု';
}
renderBarChart(d.daily_chart);
const topEl=document.getElementById('topProdsEl');
topEl.innerHTML=d.top_products.length===0
?'<div class="empty"><span>📊</span>ဒေတာ မရှိသေးပါ</div>'
:d.top_products.map((p,i)=>`
<div class="top-prod-row">
<span class="top-rank">#${i+1}</span>
<span class="top-name">${p.name}</span>
<span class="top-qty">${p.qty}ခု</span>
<span class="top-rev">${fmtK(p.revenue)}</span>
</div>`).join('');
}catch(e){console.error(e)}
}
function renderBarChart(data){
const el=document.getElementById('barChart');
if(!data||data.length===0){el.innerHTML='<div class="loading" style="flex:1;text-align:center">ဒေတာ မရှိသေးပါ</div>';return}
const max=Math.max(...data.map(d=>d.rev),1);
el.innerHTML=data.map(d=>{
const h=Math.max(4,Math.round((d.rev/max)*84));
return`<div class="bar-wrap">
<div class="bar" style="height:${h}px" title="${d.day}: ${d.rev.toLocaleString()} ကျပ်"></div>
<div class="bar-label">${d.day.substring(5)}</div>
</div>`;
}).join('');
}
// ── Products ───────────────────────────────────────────────────────────────
async function loadProducts(){
document.getElementById('productsList').innerHTML='<div class="loading">Loading...</div>';
const r=await fetch('/api/products');
allProducts=await r.json();
document.getElementById('prodCountLabel').textContent=`စုစုပေါင်း ${allProducts.length} မျိုး`;
renderProducts(allProducts);
}
function filterProducts(){
const q=document.getElementById('prodSearch').value.toLowerCase();
renderProducts(allProducts.filter(p=>p.name.toLowerCase().includes(q)||p.barcode.includes(q)));
}
function renderProducts(list){
const el=document.getElementById('productsList');
if(list.length===0){el.innerHTML='<div class="empty"><span>📦</span>ကုန်ပစ္စည်း မတွေ့ပါ</div>';return}
el.innerHTML=list.map(p=>`
<div class="product-row">
<div class="prod-info">
<div class="prod-name">${p.name}</div>
<div class="prod-barcode">${p.barcode}</div>
</div>
<div class="prod-stock">${p.stock}<br><span style="font-size:9px">ခု</span></div>
<div class="prod-price">${p.price.toLocaleString()}<span style="font-size:9px;color:var(--muted)">ကျပ်</span></div>
<button class="btn-edit" onclick="openEditModal(${p.id})">✏️</button>
<button class="btn-del2" onclick="deleteProduct(${p.id},'${p.name.replace(/'/g,"\\'")}')">🗑️</button>
</div>`).join('');
}
// ── Modal Scanner ──────────────────────────────────────────────────────────
function toggleModalScanner(){
if(!miniScannerActive)startModalScanner();
else stopModalScanner();
}
function startModalScanner(){
document.getElementById('miniScannerWrap').classList.add('visible');
document.getElementById('modalScanBtn').textContent='⏹️';
miniScannerActive=true;
miniScanner=new Html5Qrcode("miniReader");
miniScanner.start(
{facingMode:"environment"},
{fps:10,qrbox:{width:200,height:60}},
(code)=>{
document.getElementById('fBarcode').value=code;
stopModalScanner();
document.getElementById('fName').focus();
if(navigator.vibrate)navigator.vibrate(60);
},
()=>{}
).catch(()=>{
document.getElementById('miniScannerWrap').classList.remove('visible');
miniScannerActive=false;
});
}
function stopModalScanner(){
if(miniScanner){miniScanner.stop().catch(()=>{});miniScanner=null}
miniScannerActive=false;
document.getElementById('miniScannerWrap').classList.remove('visible');
document.getElementById('modalScanBtn').textContent='📷';
}
function openAddModal(){
editingProductId=null;
stopModalScanner();
document.getElementById('modalTitle').textContent='ကုန်ပစ္စည်းအသစ် ထည့်မည်';
['fBarcode','fName','fPrice','fStock'].forEach(id=>document.getElementById(id).value='');
document.getElementById('modalErr').style.display='none';
document.getElementById('productModal').classList.add('show');
}
function openEditModal(id){
const p=allProducts.find(x=>x.id===id);
if(!p)return;
editingProductId=id;
stopModalScanner();
document.getElementById('modalTitle').textContent='ကုန်ပစ္စည်း ပြင်မည်';
document.getElementById('fBarcode').value=p.barcode;
document.getElementById('fName').value=p.name;
document.getElementById('fPrice').value=p.price;
document.getElementById('fStock').value=p.stock;
document.getElementById('modalErr').style.display='none';
document.getElementById('productModal').classList.add('show');
}
function closeProductModal(){
stopModalScanner();
document.getElementById('productModal').classList.remove('show');
}
async function saveProduct(){
const barcode=document.getElementById('fBarcode').value.trim();
const name=document.getElementById('fName').value.trim();
const price=document.getElementById('fPrice').value;
const stock=document.getElementById('fStock').value;
const errEl=document.getElementById('modalErr');
if(!barcode||!name||!price){errEl.textContent='Barcode, နာမည်နှင့် ဈေးနှုန်း ထည့်ပါ';errEl.style.display='block';return}
const url=editingProductId?`/api/products/${editingProductId}`:'/api/products';
const method=editingProductId?'PUT':'POST';
try{
const r=await fetch(url,{method,headers:{'Content-Type':'application/json'},body:JSON.stringify({barcode,name,price,stock:stock||0})});
const d=await r.json();
if(r.ok){closeProductModal();loadProducts()}
else{errEl.textContent=d.error||'Error ဖြစ်သည်';errEl.style.display='block'}
}catch(e){errEl.textContent='ချိတ်ဆက်မှု ပြဿနာ';errEl.style.display='block'}
}
async function deleteProduct(id,name){
if(!confirm(`"${name}" ကို ဖျက်မှာ သေချာပါသလား?`))return;
const r=await fetch(`/api/products/${id}`,{method:'DELETE'});
if(r.ok)loadProducts();
else alert('ဖျက်မရပါ');
}
// ── Sales ──────────────────────────────────────────────────────────────────
async function loadSales(period,btn){
if(btn){document.querySelectorAll('.period-tab').forEach(b=>b.classList.remove('active'));btn.classList.add('active')}
document.getElementById('salesList').innerHTML='<div class="loading">Loading...</div>';
try{
const r=await fetch(`/api/sales?period=${period}`);
const d=await r.json();
document.getElementById('salesRevEl').textContent=fmtK(d.summary.total_revenue);
document.getElementById('salesCntEl').textContent=d.summary.total_sales;
if(d.sales.length===0){document.getElementById('salesList').innerHTML='<div class="empty"><span>🧾</span>မှတ်တမ်း မရှိသေးပါ</div>';return}
document.getElementById('salesList').innerHTML=d.sales.map(s=>`
<div class="sale-row" onclick="showSaleDetail(${s.id})">
<div class="sale-id">#${s.id}</div>
<div class="sale-info">
<div class="sale-time">${s.created_at}</div>
<div class="sale-cnt">${s.item_count} မျိုး • ${s.cashier_name||'cashier'}</div>
</div>
<div class="sale-total">${s.total_amount.toLocaleString()} ကျပ်</div>
</div>`).join('');
}catch(e){document.getElementById('salesList').innerHTML='<div class="empty">ချိတ်ဆက်မှု ပြဿနာ</div>'}
}
async function showSaleDetail(id){
document.getElementById('saleModal').classList.add('show');
document.getElementById('saleDetailContent').innerHTML='<div class="loading">Loading...</div>';
try{
const r=await fetch(`/api/sales/${id}/items`);
const items=await r.json();
const total=items.reduce((s,i)=>s+i.price_at_time*i.quantity,0);
document.getElementById('saleDetailContent').innerHTML=`
<div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:10px">Sale #${id}</div>
${items.map(i=>`<div class="sale-detail-item"><span>${i.name} <span style="color:var(--muted)">×${i.quantity}</span></span><span style="color:var(--accent)">${(i.price_at_time*i.quantity).toLocaleString()} ကျပ်</span></div>`).join('')}
<div class="sale-detail-item" style="font-weight:700;margin-top:6px;padding-top:8px;border-top:1px solid var(--border)">
<span style="color:var(--accent)">စုစုပေါင်း</span>
<span style="color:var(--accent);font-family:'JetBrains Mono',monospace">${total.toLocaleString()} ကျပ်</span>
</div>`;
}catch(e){document.getElementById('saleDetailContent').innerHTML='<div class="empty">Error</div>'}
}
// ── Helpers ────────────────────────────────────────────────────────────────
function fmtK(n){if(n>=1000000)return(n/1000000).toFixed(1)+'M';if(n>=1000)return(n/1000).toFixed(0)+'K';return Math.round(n).toLocaleString()}
function closeModal(id){document.getElementById(id).classList.remove('show')}
function logout(){fetch('/api/logout',{method:'POST'}).then(()=>window.location.href='/login')}
// Close modals on overlay tap
document.querySelectorAll('.modal-overlay').forEach(el=>{
el.addEventListener('click',function(e){if(e.target===this){if(this.id==='productModal')closeProductModal();else this.classList.remove('show')}});
});
</script>
</body>
</html>
"""
# ─── Database Init ────────────────────────────────────────────────────────────
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'cashier'
)''')
c.execute('''CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
barcode TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now','localtime'))
)''')
c.execute('''CREATE TABLE IF NOT EXISTS sales (
id INTEGER PRIMARY KEY AUTOINCREMENT,
total_amount REAL NOT NULL,
cash_received REAL NOT NULL,
change_amount REAL NOT NULL,
cashier_id INTEGER,
created_at TEXT DEFAULT (datetime('now','localtime'))
)''')
c.execute('''CREATE TABLE IF NOT EXISTS sale_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sale_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price_at_time REAL NOT NULL,
FOREIGN KEY (sale_id) REFERENCES sales(id),
FOREIGN KEY (product_id) REFERENCES products(id)
)''')
# Default admin
c.execute("SELECT id FROM users WHERE username='admin'")
if not c.fetchone():
c.execute("INSERT INTO users (username, password, role) VALUES (?, ?, ?)",
('admin', 'admin123', 'admin'))
# Default cashier
c.execute("SELECT id FROM users WHERE username='cashier'")
if not c.fetchone():
c.execute("INSERT INTO users (username, password, role) VALUES (?, ?, ?)",
('cashier', 'cashier123', 'cashier'))
# Sample products
c.execute("SELECT COUNT(*) FROM products")
if c.fetchone()[0] == 0:
sample_products = [
('8850006111019', 'မာမာနူဒယ်လ် (ကြက်သား)', 500, 50),
('8852987122207', 'ကိုကာကိုလာ 330ml', 800, 100),
('8850006110302', 'ပေပ်စီ 500ml', 700, 80),
('8888888888881', 'ကြက်ဥ (တစ်လုံး)', 250, 200),
('8888888888882', 'ဆပ်ပြာတုံး (Lux)', 1200, 30),
('8888888888883', 'ဆန် ၁ပဲသား', 2500, 100),
('8888888888884', 'ကြက်သား ၁ပဲသား', 8000, 20),
('8888888888885', 'ငါးပိ (Shrimp Paste)', 1500, 40),
('8888888888886', 'သကြား ၁ပဲသား', 3000, 60),
('8888888888887', 'ကြော်ဆီ (Cooking Oil) 1L', 5500, 25),
]
c.executemany("INSERT INTO products (barcode, name, price, stock) VALUES (?,?,?,?)", sample_products)
conn.commit()
conn.close()
# ─── Auth Decorators ──────────────────────────────────────────────────────────
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Unauthorized'}), 401
return f(*args, **kwargs)
return decorated
def admin_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if 'user_id' not in session:
return jsonify({'error': 'Unauthorized'}), 401
if session.get('role') != 'admin':
return jsonify({'error': 'Admin only'}), 403
return f(*args, **kwargs)
return decorated
# ─── Page Routes ─────────────────────────────────────────────────────────────
@app.route('/')
def index():
if 'user_id' in session:
if session.get('role') == 'admin':
return redirect('/admin')
return redirect('/cashier')
return redirect('/login')
@app.route('/login')
def login_page():
return render_template_string(LOGIN_HTML)
@app.route('/cashier')
def cashier_page():
if 'user_id' not in session:
return redirect('/login')
return render_template_string(CASHIER_HTML)
@app.route('/admin')
def admin_page():
if 'user_id' not in session or session.get('role') != 'admin':
return redirect('/login')
return render_template_string(ADMIN_HTML)
# ─── Auth API ─────────────────────────────────────────────────────────────────
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
conn = get_db()
user = conn.execute("SELECT * FROM users WHERE username=? AND password=?",
(data['username'], data['password'])).fetchone()
conn.close()
if user:
session['user_id'] = user['id']
session['username'] = user['username']
session['role'] = user['role']
return jsonify({'role': user['role'], 'username': user['username']})
return jsonify({'error': 'အသုံးပြုသူအမည် သို့မဟုတ် စကားဝှက် မမှန်ပါ'}), 401
@app.route('/api/logout', methods=['POST'])
def logout():
session.clear()
return jsonify({'ok': True})
@app.route('/api/me')
def me():
if 'user_id' in session:
return jsonify({'username': session['username'], 'role': session['role']})
return jsonify({'error': 'Not logged in'}), 401
# ─── Products API ─────────────────────────────────────────────────────────────
@app.route('/api/products', methods=['GET'])
@login_required
def get_products():
conn = get_db()
products = conn.execute("SELECT * FROM products ORDER BY name").fetchall()
conn.close()
return jsonify([dict(p) for p in products])
@app.route('/api/products/scan/<barcode>', methods=['GET'])
@login_required
def scan_product(barcode):
conn = get_db()
product = conn.execute("SELECT * FROM products WHERE barcode=?", (barcode,)).fetchone()
conn.close()
if product:
return jsonify(dict(product))
return jsonify({'error': 'product_not_found'}), 404
@app.route('/api/products', methods=['POST'])
@admin_required
def add_product():
data = request.get_json()
try:
conn = get_db()
conn.execute("INSERT INTO products (barcode, name, price, stock) VALUES (?,?,?,?)",
(data['barcode'], data['name'], float(data['price']), int(data.get('stock', 0))))
conn.commit()
conn.close()
return jsonify({'ok': True})
except sqlite3.IntegrityError:
return jsonify({'error': 'Barcode ထပ်နေပါသည်'}), 400
@app.route('/api/products/<int:pid>', methods=['PUT'])
@admin_required
def update_product(pid):
data = request.get_json()
conn = get_db()
conn.execute("UPDATE products SET barcode=?, name=?, price=?, stock=? WHERE id=?",
(data['barcode'], data['name'], float(data['price']), int(data.get('stock', 0)), pid))
conn.commit()
conn.close()
return jsonify({'ok': True})
@app.route('/api/products/<int:pid>', methods=['DELETE'])
@admin_required
def delete_product(pid):
conn = get_db()
conn.execute("DELETE FROM products WHERE id=?", (pid,))
conn.commit()
conn.close()
return jsonify({'ok': True})
# ─── Sales API ────────────────────────────────────────────────────────────────
@app.route('/api/sales', methods=['POST'])
@login_required
def create_sale():
data = request.get_json()
items = data.get('items', [])
if not items:
return jsonify({'error': 'Cart မှာ ကုန်ပစ္စည်း မပါပါ'}), 400
total = sum(i['price'] * i['quantity'] for i in items)
cash = float(data.get('cash_received', total))
change = cash - total
conn = get_db()
cur = conn.execute(
"INSERT INTO sales (total_amount, cash_received, change_amount, cashier_id) VALUES (?,?,?,?)",
(total, cash, change, session['user_id'])
)
sale_id = cur.lastrowid
for item in items:
conn.execute(
"INSERT INTO sale_items (sale_id, product_id, quantity, price_at_time) VALUES (?,?,?,?)",
(sale_id, item['product_id'], item['quantity'], item['price'])
)
conn.commit()
sale_row = conn.execute("SELECT * FROM sales WHERE id=?", (sale_id,)).fetchone()
sale_items_rows = conn.execute("""
SELECT si.*, p.name FROM sale_items si
JOIN products p ON si.product_id = p.id
WHERE si.sale_id=?
""", (sale_id,)).fetchall()
conn.close()
return jsonify({
'sale_id': sale_id,
'total': total,
'cash': cash,
'change': change,
'created_at': dict(sale_row)['created_at'],
'items': [dict(r) for r in sale_items_rows]
})
@app.route('/api/sales', methods=['GET'])
@admin_required
def get_sales():
period = request.args.get('period', 'today')
conn = get_db()
if period == 'today':
where = "date(s.created_at) = date('now','localtime')"
elif period == 'week':
where = "s.created_at >= datetime('now','localtime','-7 days')"
elif period == 'month':
where = "s.created_at >= datetime('now','localtime','-30 days')"
else:
where = "1=1"
sales = conn.execute(f"""
SELECT s.*, u.username as cashier_name,
COUNT(si.id) as item_count
FROM sales s
LEFT JOIN users u ON s.cashier_id = u.id
LEFT JOIN sale_items si ON s.id = si.sale_id
WHERE {where}
GROUP BY s.id
ORDER BY s.created_at DESC
LIMIT 100
""").fetchall()
summary = conn.execute(f"""
SELECT COUNT(*) as total_sales,
COALESCE(SUM(total_amount), 0) as total_revenue
FROM sales s WHERE {where}
""").fetchone()
conn.close()
return jsonify({
'sales': [dict(s) for s in sales],
'summary': dict(summary)
})
@app.route('/api/sales/<int:sid>/items', methods=['GET'])
@admin_required
def get_sale_items(sid):
conn = get_db()
items = conn.execute("""
SELECT si.*, p.name, p.barcode FROM sale_items si
JOIN products p ON si.product_id = p.id
WHERE si.sale_id=?
""", (sid,)).fetchall()
conn.close()
return jsonify([dict(i) for i in items])
@app.route('/api/dashboard', methods=['GET'])
@admin_required
def dashboard():
conn = get_db()
today_sales = conn.execute("""
SELECT COALESCE(SUM(total_amount),0) as rev, COUNT(*) as cnt
FROM sales WHERE date(created_at)=date('now','localtime')
""").fetchone()
week_sales = conn.execute("""
SELECT COALESCE(SUM(total_amount),0) as rev, COUNT(*) as cnt
FROM sales WHERE created_at >= datetime('now','localtime','-7 days')
""").fetchone()
product_count = conn.execute("SELECT COUNT(*) as cnt FROM products").fetchone()
top_products = conn.execute("""
SELECT p.name, SUM(si.quantity) as qty, SUM(si.quantity * si.price_at_time) as revenue
FROM sale_items si JOIN products p ON si.product_id = p.id
WHERE si.sale_id IN (SELECT id FROM sales WHERE created_at >= datetime('now','localtime','-7 days'))
GROUP BY p.id ORDER BY qty DESC LIMIT 5
""").fetchall()
daily_chart = conn.execute("""
SELECT date(created_at) as day, SUM(total_amount) as rev, COUNT(*) as cnt
FROM sales
WHERE created_at >= datetime('now','localtime','-7 days')
GROUP BY day ORDER BY day
""").fetchall()
conn.close()
return jsonify({
'today': dict(today_sales),
'week': dict(week_sales),
'product_count': product_count['cnt'],
'top_products': [dict(p) for p in top_products],
'daily_chart': [dict(d) for d in daily_chart]
})
if __name__ == '__main__':
init_db()
# HuggingFace Spaces requires port 7860
port = int(os.environ.get('PORT', 7860))
print(f"✅ POS App started at http://localhost:{port}")
print(f" DB Path: {DB_PATH}")
print(" Admin: admin / admin123")
print(" Cashier: cashier / cashier123")
app.run(host='0.0.0.0', port=port, debug=False)