| <!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"> |
| <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; |
| } |
| |
| |
| .layout { display: flex; min-height: 100vh; } |
| |
| .sidebar { |
| width: 220px; |
| background: var(--card); |
| border-right: 1px solid var(--border); |
| display: flex; flex-direction: column; |
| position: fixed; left: 0; top: 0; bottom: 0; |
| z-index: 50; |
| transition: transform 0.3s; |
| } |
| |
| .sidebar-logo { |
| padding: 20px 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: 12px 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 0.15s; |
| margin-bottom: 2px; |
| } |
| |
| .nav-item:hover { background: var(--card2); color: var(--text); } |
| .nav-item.active { background: rgba(245,158,11,0.1); color: var(--accent); } |
| .nav-item .nav-icon { font-size: 16px; } |
| |
| .sidebar-footer { |
| padding: 12px 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 0.15s; |
| } |
| |
| .btn-logout:hover { border-color: var(--red); color: var(--red); } |
| |
| |
| .main { |
| margin-left: 220px; |
| flex: 1; |
| padding: 24px; |
| min-height: 100vh; |
| } |
| |
| |
| .mob-header { |
| display: none; |
| 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: 60; |
| } |
| |
| .hamburger { |
| background: none; border: none; |
| color: var(--text); font-size: 20px; cursor: pointer; |
| } |
| |
| .sidebar-overlay { |
| display: none; |
| position: fixed; inset: 0; background: rgba(0,0,0,0.6); |
| z-index: 49; |
| } |
| |
| |
| .page-header { |
| display: flex; align-items: center; justify-content: space-between; |
| margin-bottom: 24px; |
| } |
| |
| .page-title { font-size: 20px; font-weight: 700; } |
| .page-sub { font-size: 12px; color: var(--muted); margin-top: 2px; } |
| |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 14px; |
| margin-bottom: 24px; |
| } |
| |
| .stat-card { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 14px; |
| padding: 16px; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .stat-card::before { |
| content: ''; |
| position: absolute; |
| top: 0; right: 0; |
| width: 60px; height: 60px; |
| border-radius: 0 14px 0 60px; |
| opacity: 0.15; |
| } |
| |
| .stat-card.gold::before { background: var(--accent); } |
| .stat-card.green::before { background: var(--green); } |
| .stat-card.blue::before { background: var(--blue); } |
| .stat-card.purple::before { background: var(--purple); } |
| |
| .stat-icon { font-size: 22px; margin-bottom: 10px; } |
| .stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; } |
| .stat-value { |
| font-size: 20px; font-weight: 700; |
| font-family: 'JetBrains Mono', monospace; |
| margin-top: 4px; |
| 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: 4px; font-family: 'JetBrains Mono', monospace; } |
| |
| |
| .chart-card { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 14px; |
| padding: 18px; |
| margin-bottom: 24px; |
| } |
| |
| .chart-title { font-size: 13px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; } |
| |
| .bar-chart { display: flex; align-items: flex-end; gap: 6px; height: 100px; } |
| .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,0.4)); |
| border-radius: 4px 4px 0 0; |
| min-height: 4px; |
| transition: height 0.6s ease; |
| position: relative; |
| } |
| .bar:hover { background: linear-gradient(to top, #fbbf24, var(--accent)); } |
| .bar-label { font-size: 9px; color: var(--muted); font-family: 'JetBrains Mono', monospace; white-space: nowrap; } |
| |
| |
| .tabs { display: flex; gap: 4px; margin-bottom: 16px; background: var(--card2); border-radius: 10px; padding: 4px; } |
| .tab { |
| flex: 1; text-align: center; padding: 8px 4px; |
| border-radius: 7px; font-size: 12px; cursor: pointer; |
| color: var(--muted); transition: all 0.2s; |
| } |
| .tab.active { background: var(--card); color: var(--text); font-weight: 600; } |
| |
| |
| .table-card { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 14px; |
| overflow: hidden; |
| margin-bottom: 24px; |
| } |
| |
| .table-head { |
| padding: 14px 16px; |
| display: flex; align-items: center; justify-content: space-between; |
| border-bottom: 1px solid var(--border); |
| } |
| |
| .table-head h3 { font-size: 13px; 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 16px; |
| 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: 10px; |
| padding: 12px 16px; |
| border-bottom: 1px solid rgba(30,41,59,0.5); |
| transition: background 0.1s; |
| } |
| |
| .product-row:last-child { border: none; } |
| .product-row:hover { background: rgba(255,255,255,0.02); } |
| |
| .prod-info { flex: 1; min-width: 0; } |
| .prod-name { font-size: 13px; 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: 2px; } |
| .prod-price { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 13px; font-weight: 700; |
| color: var(--accent); |
| min-width: 80px; text-align: right; |
| } |
| .prod-stock { font-size: 11px; color: var(--muted); text-align: center; min-width: 40px; } |
| |
| .btn-edit, .btn-del { |
| border: none; border-radius: 7px; |
| padding: 6px 10px; font-size: 12px; |
| cursor: pointer; transition: all 0.15s; |
| } |
| |
| .btn-edit { background: rgba(59,130,246,0.15); color: var(--blue); } |
| .btn-edit:hover { background: rgba(59,130,246,0.25); } |
| .btn-del { background: rgba(239,68,68,0.15); color: var(--red); } |
| .btn-del:hover { background: rgba(239,68,68,0.25); } |
| |
| |
| .sale-row { |
| display: flex; align-items: center; gap: 10px; |
| padding: 12px 16px; |
| border-bottom: 1px solid rgba(30,41,59,0.5); |
| cursor: pointer; transition: background 0.1s; |
| } |
| .sale-row:hover { background: rgba(255,255,255,0.02); } |
| .sale-row:last-child { border: none; } |
| .sale-id { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--muted); min-width: 40px; } |
| .sale-info { flex: 1; } |
| .sale-time { font-size: 11px; color: var(--muted); font-family: 'JetBrains Mono', monospace; } |
| .sale-items-count { font-size: 12px; color: var(--text); } |
| .sale-total { font-family: 'JetBrains Mono', monospace; font-size: 14px; font-weight: 700; color: var(--green); } |
| |
| |
| .top-prod-row { |
| display: flex; align-items: center; gap: 10px; |
| padding: 10px 0; |
| border-bottom: 1px solid rgba(30,41,59,0.5); |
| } |
| .top-prod-row:last-child { border: none; } |
| .top-rank { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--muted); min-width: 20px; } |
| .top-name { flex: 1; font-size: 12px; } |
| .top-qty { font-size: 11px; color: var(--muted); min-width: 40px; text-align: right; } |
| .top-rev { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: var(--accent); min-width: 80px; text-align: right; } |
| |
| |
| .period-tabs { display: flex; gap: 4px; margin-bottom: 16px; } |
| .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 0.15s; |
| } |
| .period-tab.active { background: var(--accent); border-color: var(--accent); color: #000; font-weight: 700; } |
| |
| |
| .modal-overlay { |
| position: fixed; inset: 0; |
| background: rgba(0,0,0,0.7); |
| z-index: 200; display: none; |
| align-items: center; justify-content: center; |
| padding: 16px; |
| } |
| .modal-overlay.show { display: flex; animation: fadeIn 0.2s; } |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } |
| |
| .modal { |
| background: var(--card); |
| border: 1px solid var(--border); |
| border-radius: 18px; |
| padding: 24px; |
| width: 100%; max-width: 420px; |
| animation: scaleIn 0.25s ease; |
| max-height: 90vh; overflow-y: auto; |
| } |
| |
| @keyframes scaleIn { |
| from { opacity: 0; transform: scale(0.9); } |
| to { opacity: 1; transform: scale(1); } |
| } |
| |
| .modal h3 { font-size: 16px; margin-bottom: 20px; } |
| |
| .form-group { margin-bottom: 16px; } |
| .form-group label { display: block; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 7px; } |
| .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; } |
| |
| .modal-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; } |
| .btn-cancel { |
| background: var(--card2); border: 1px solid var(--border); |
| border-radius: 10px; padding: 12px; color: var(--text); |
| font-size: 14px; cursor: pointer; |
| font-family: 'Noto Sans Myanmar', sans-serif; |
| } |
| .btn-save { |
| background: var(--accent); border: none; |
| border-radius: 10px; padding: 12px; color: #000; |
| font-size: 14px; font-weight: 700; cursor: pointer; |
| font-family: 'Noto Sans Myanmar', sans-serif; |
| } |
| |
| .error-msg { |
| background: rgba(239,68,68,0.1); |
| border: 1px solid rgba(239,68,68,0.3); |
| border-radius: 8px; padding: 10px 12px; |
| color: #fca5a5; font-size: 12px; |
| margin-top: 12px; display: none; |
| } |
| |
| |
| .sale-detail-item { |
| display: flex; justify-content: space-between; |
| padding: 8px 0; border-bottom: 1px solid rgba(30,41,59,0.5); |
| font-size: 13px; |
| } |
| .sale-detail-item:last-child { border: none; } |
| |
| |
| .empty { text-align: center; padding: 32px; color: var(--muted); font-size: 13px; } |
| .empty span { font-size: 32px; display: block; margin-bottom: 8px; } |
| |
| |
| .loading { text-align: center; padding: 24px; color: var(--muted); font-size: 13px; } |
| |
| |
| @media (max-width: 768px) { |
| .sidebar { transform: translateX(-100%); } |
| .sidebar.open { transform: translateX(0); } |
| .sidebar-overlay.show { display: block; } |
| .main { margin-left: 0; padding: 16px; } |
| .mob-header { display: flex; } |
| .stats-grid { grid-template-columns: 1fr 1fr; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="layout"> |
|
|
| |
| <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"> |
| <div class="nav-item active" data-page="dashboard" onclick="showPage('dashboard')"> |
| <span class="nav-icon">π</span> Dashboard |
| </div> |
| <div class="nav-item" data-page="products" onclick="showPage('products')"> |
| <span class="nav-icon">π¦</span> αα―ααΊαα
αΉα
ααΊαΈαα»α¬αΈ |
| </div> |
| <div class="nav-item" data-page="sales" onclick="showPage('sales')"> |
| <span class="nav-icon">π§Ύ</span> αα±α¬ααΊαΈαα»ααΎααΊαααΊαΈ |
| </div> |
| </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="sidebar-overlay" id="overlay" onclick="closeSidebar()"></div> |
|
|
| |
| <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 class="main"> |
|
|
| |
| <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:18px;cursor:pointer">β»</button> |
| </div> |
|
|
| <div class="stats-grid" id="statsGrid"> |
| <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 16px" id="topProdsEl"> |
| <div class="loading">Loading...</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="page-products" style="display:none"> |
| <div class="page-header"> |
| <div> |
| <div class="page-title">αα―ααΊαα
αΉα
ααΊαΈαα»α¬αΈ</div> |
| <div class="page-sub" id="prodCountLabel">Loading...</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> |
|
|
| |
| <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="grid-template-columns:1fr 1fr;margin-bottom:16px"> |
| <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> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="productModal"> |
| <div class="modal"> |
| <h3 id="modalTitle">αα―ααΊαα
αΉα
ααΊαΈα‘αα
αΊ ααα·αΊαααΊ</h3> |
| <div class="form-group"> |
| <label>Barcode</label> |
| <input type="text" id="fBarcode" placeholder="8850006111019" inputmode="numeric" style="font-family:'JetBrains Mono',monospace"> |
| </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="error-msg" id="modalErr"></div> |
| <div class="modal-btns"> |
| <button class="btn-cancel" onclick="closeModal('productModal')">αααΊαα»ααΊ</button> |
| <button class="btn-save" onclick="saveProduct()">ααααΊαΈαααΊ</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="saleModal"> |
| <div class="modal"> |
| <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 = []; |
| let editingProductId = null; |
| let currentPage = 'dashboard'; |
| |
| |
| 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(); |
| }); |
| |
| |
| function showPage(page) { |
| currentPage = 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'); |
| } |
| |
| |
| 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, 10); |
| document.getElementById('topSellerQty').textContent = d.top_products[0].qty + ' αα―'; |
| } |
| |
| |
| renderBarChart(d.daily_chart); |
| |
| |
| const topEl = document.getElementById('topProdsEl'); |
| if (d.top_products.length === 0) { |
| topEl.innerHTML = '<div class="empty"><span>π</span>αα±αα¬ αααΎααα±αΈαα«</div>'; |
| } else { |
| topEl.innerHTML = 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="text-align:center;flex:1">αα±αα¬ αααΎααα±αΈαα«</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) * 90)); |
| const label = d.day.substring(5); |
| return ` |
| <div class="bar-wrap"> |
| <div class="bar" style="height:${h}px" title="${d.day}: ${d.rev.toLocaleString()} αα»ααΊ"></div> |
| <div class="bar-label">${label}</div> |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| |
| async function loadProducts() { |
| const el = document.getElementById('productsList'); |
| el.innerHTML = '<div class="loading">Loading...</div>'; |
| try { |
| const r = await fetch('/api/products'); |
| allProducts = await r.json(); |
| document.getElementById('prodCountLabel').textContent = `α
α―α
α―αα±α«ααΊαΈ ${allProducts.length} αα»αα―αΈ`; |
| renderProducts(allProducts); |
| } catch(e) { el.innerHTML = '<div class="empty">αα»αααΊαααΊααΎα― ααΌαΏαα¬</div>'; } |
| } |
| |
| function filterProducts() { |
| const q = document.getElementById('prodSearch').value.toLowerCase(); |
| const filtered = allProducts.filter(p => |
| p.name.toLowerCase().includes(q) || p.barcode.includes(q) |
| ); |
| renderProducts(filtered); |
| } |
| |
| 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:10px;color:var(--muted)"> αα»ααΊ</span></div> |
| <button class="btn-edit" onclick="openEditModal(${p.id})">βοΈ</button> |
| <button class="btn-del" onclick="deleteProduct(${p.id},'${p.name.replace(/'/g,"\\'")}')">ποΈ</button> |
| </div> |
| `).join(''); |
| } |
| |
| function openAddModal() { |
| editingProductId = null; |
| document.getElementById('modalTitle').textContent = 'αα―ααΊαα
αΉα
ααΊαΈα‘αα
αΊ ααα·αΊαααΊ'; |
| document.getElementById('fBarcode').value = ''; |
| document.getElementById('fName').value = ''; |
| document.getElementById('fPrice').value = ''; |
| document.getElementById('fStock').value = ''; |
| document.getElementById('modalErr').style.display = 'none'; |
| document.getElementById('productModal').classList.add('show'); |
| setTimeout(() => document.getElementById('fBarcode').focus(), 100); |
| } |
| |
| function openEditModal(id) { |
| const p = allProducts.find(x => x.id === id); |
| if (!p) return; |
| editingProductId = id; |
| 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'); |
| } |
| |
| 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) { |
| closeModal('productModal'); |
| 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'); |
| } |
| const el = document.getElementById('salesList'); |
| el.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) { |
| el.innerHTML = '<div class="empty"><span>π§Ύ</span>ααΎααΊαααΊαΈ αααΎααα±αΈαα«</div>'; |
| return; |
| } |
| el.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-items-count">${s.item_count} αα»αα―αΈ β’ ${s.cashier_name || 'cashier'}</div> |
| </div> |
| <div class="sale-total">${s.total_amount.toLocaleString()} αα»ααΊ</div> |
| </div> |
| `).join(''); |
| } catch(e) { el.innerHTML = '<div class="empty">αα»αααΊαααΊααΎα― ααΌαΏαα¬</div>'; } |
| } |
| |
| async function showSaleDetail(id) { |
| document.getElementById('saleModal').classList.add('show'); |
| const el = document.getElementById('saleDetailContent'); |
| el.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); |
| el.innerHTML = ` |
| <div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:12px">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:8px;border-top:1px solid var(--border);padding-top:10px"> |
| <span style="color:var(--accent)">α
α―α
α―αα±α«ααΊαΈ</span> |
| <span style="color:var(--accent);font-family:'JetBrains Mono',monospace">${total.toLocaleString()} αα»ααΊ</span> |
| </div> |
| `; |
| } catch(e) { el.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 modal on overlay click |
| document.querySelectorAll('.modal-overlay').forEach(el => { |
| el.addEventListener('click', function(e) { |
| if (e.target === this) this.classList.remove('show'); |
| }); |
| }); |
| </script> |
| </body> |
| </html> |
|
|