| 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__) |
| |
| app.secret_key = os.environ.get('SECRET_KEY', 'pos-secret-key-change-in-production') |
| CORS(app) |
|
|
| |
| |
| 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') |
|
|
| |
|
|
| 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> |
| """ |
|
|
|
|
|
|
| |
|
|
| 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) |
| )''') |
|
|
| |
| 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')) |
|
|
| |
| 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')) |
|
|
| |
| 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() |
|
|
| |
|
|
| 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 |
|
|
| |
|
|
| @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) |
|
|
| |
|
|
| @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 |
|
|
| |
|
|
| @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}) |
|
|
| |
|
|
| @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() |
| |
| 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) |
|
|