Download admin.html from Phoe2004/ttup: direct link, hf CLI and curl.
- Browser
- Download file 32.3 kB
-
https://huggingface.co/spaces/Phoe2004/ttup/resolve/main/admin.html
- Command line
-
hf download hf://spaces/Phoe2004/ttup/admin.html
-
curl -L -o admin.html https://huggingface.co/spaces/Phoe2004/ttup/resolve/main/admin.html
32.3 kB
| <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; | |
| } | |
| /* ββ Sidebar + Layout ββ */ | |
| .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 Content ββ */ | |
| .main { | |
| margin-left: 220px; | |
| flex: 1; | |
| padding: 24px; | |
| min-height: 100vh; | |
| } | |
| /* ββ Mobile Header ββ */ | |
| .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 ββ */ | |
| .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; } | |
| /* ββ Stat Cards ββ */ | |
| .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 ββ */ | |
| .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 ββ */ | |
| .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; } | |
| /* ββ Products Table ββ */ | |
| .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); } | |
| /* ββ Sales History ββ */ | |
| .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 Products ββ */ | |
| .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 Selector ββ */ | |
| .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 ββ */ | |
| .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 ββ */ | |
| .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 State ββ */ | |
| .empty { text-align: center; padding: 32px; color: var(--muted); font-size: 13px; } | |
| .empty span { font-size: 32px; display: block; margin-bottom: 8px; } | |
| /* ββ Loading ββ */ | |
| .loading { text-align: center; padding: 24px; color: var(--muted); font-size: 13px; } | |
| /* ββ Responsive ββ */ | |
| @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"> | |
| <!-- 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"> | |
| <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> | |
| <!-- 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 PAGE ββ --> | |
| <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> | |
| <!-- ββ PRODUCTS PAGE ββ --> | |
| <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> | |
| <!-- ββ SALES PAGE ββ --> | |
| <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> | |
| <!-- Add/Edit Product Modal --> | |
| <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> | |
| <!-- Sale Detail Modal --> | |
| <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'; | |
| // 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) { | |
| 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'); | |
| } | |
| // ββ 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, 10); | |
| document.getElementById('topSellerQty').textContent = d.top_products[0].qty + ' αα―'; | |
| } | |
| // Bar chart | |
| renderBarChart(d.daily_chart); | |
| // Top products | |
| 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); // MM-DD | |
| 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(''); | |
| } | |
| // ββ Products βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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> | |