| <!DOCTYPE html> |
| <html lang="vi"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> |
| <meta http-equiv="Pragma" content="no-cache"> |
| <meta http-equiv="Expires" content="0"> |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='22' fill='%23151515'/><text x='50' y='68' font-size='52' font-family='Arial' font-weight='bold' fill='%23ff5e00' text-anchor='middle'>A</text></svg>"> |
| <title>Control Panel - ANIVIETDB</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> |
| <link rel="stylesheet" href="style.css"> |
| <style> |
| :root { |
| --cp-bg: #101216; |
| --cp-panel: #16181f; |
| --cp-panel-2: #1d2029; |
| --cp-border: #262a35; |
| --cp-ink: #e8eaf2; |
| --cp-muted: #8b90a2; |
| --cp-accent: #ff5e00; |
| --cp-accent-2: #2abdbb; |
| --cp-purple: #a3419f; |
| } |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| html, body { height: 100%; } |
| body { |
| font-family: 'Inter', 'Roboto', sans-serif; |
| background: var(--cp-bg);
|
| min-height: 100vh; color: var(--cp-ink);
|
| display: flex;
|
| flex-direction: row;
|
| padding: 0;
|
| } |
| ::-webkit-scrollbar { width: 10px; height: 10px; } |
| ::-webkit-scrollbar-thumb { background: #2e3342; border-radius: 8px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| |
| |
| .cp-side { |
| width: 248px; min-width: 248px; |
| background: var(--cp-panel); |
| border-right: 1px solid var(--cp-border); |
| display: flex; flex-direction: column; |
| position: sticky; top: 0; height: 100vh; |
| z-index: 40; |
| } |
| .cp-logo { |
| display: flex; align-items: center; gap: 10px; |
| padding: 18px 20px; border-bottom: 1px solid var(--cp-border); |
| } |
| .cp-logo-mark { |
| width: 38px; height: 38px; border-radius: 10px; |
| background: linear-gradient(135deg, #ff5e00, #a3419f); |
| display: grid; place-items: center; |
| font-weight: 800; font-size: 18px; color: #fff; |
| box-shadow: 0 4px 14px rgba(255,94,0,.35); |
| } |
| .cp-logo-name { font-weight: 700; font-size: 14px; line-height: 1.15; } |
| .cp-logo-sub { font-size: 11px; color: var(--cp-muted); } |
| .cp-nav { padding: 12px 12px; flex: 1; overflow-y: auto; } |
| .cp-nav-label { |
| font-size: 10px; text-transform: uppercase; letter-spacing: .12em; |
| color: #5d6275; padding: 12px 12px 6px; |
| } |
| .cp-nav-item { |
| display: flex; align-items: center; gap: 12px; |
| padding: 10px 12px; margin: 2px 0; |
| border-radius: 10px; color: var(--cp-muted); |
| font-size: 13.5px; font-weight: 500; |
| cursor: pointer; text-decoration: none; transition: all .15s; |
| border: 1px solid transparent; |
| } |
| .cp-nav-item .ico { width: 18px; text-align: center; font-size: 15px; } |
| .cp-nav-item:hover { background: var(--cp-panel-2); color: var(--cp-ink); } |
| .cp-nav-item.active { |
| background: linear-gradient(135deg, rgba(255,94,0,.16), rgba(163,65,159,.14)); |
| border-color: rgba(255,94,0,.35); |
| color: #fff; |
| } |
| .cp-nav-foot { |
| padding: 14px 16px; border-top: 1px solid var(--cp-border); |
| display: flex; align-items: center; gap: 10px; |
| } |
| .cp-avatar { |
| width: 36px; height: 36px; border-radius: 50%; |
| background: linear-gradient(135deg, #2abdbb, #2b1d54); |
| display: grid; place-items: center; font-weight: 700; color: #fff; |
| flex-shrink: 0; |
| } |
| .cp-nav-foot-info { min-width: 0; flex: 1; } |
| .cp-nav-foot-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .cp-nav-foot-sub { font-size: 11px; color: var(--cp-muted); } |
| .cp-auth-btn { |
| background: none; border: 1px solid var(--cp-border); color: var(--cp-muted); |
| border-radius: 8px; padding: 5px 10px; font-size: 11px; font-weight: 600; |
| cursor: pointer; text-decoration: none; font-family: inherit; transition: all .15s; |
| } |
| .cp-auth-btn:hover { color: var(--cp-accent); border-color: rgba(255,94,0,.5); } |
| |
| |
| .cp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; } |
| .cp-topbar { |
| display: flex; align-items: center; justify-content: space-between; gap: 16px; |
| padding: 14px 24px; |
| background: rgba(16,18,22,.86); |
| backdrop-filter: blur(12px); |
| -webkit-backdrop-filter: blur(12px); |
| border-bottom: 1px solid var(--cp-border); |
| position: sticky; top: 0; z-index: 30; |
| } |
| .cp-topbar-title { font-size: 16px; font-weight: 700; } |
| .cp-topbar-title small { display: block; font-size: 11.5px; color: var(--cp-muted); font-weight: 500; } |
| .cp-topbar-links { display: flex; align-items: center; gap: 8px; } |
| .cp-btn { |
| padding: 8px 14px; border-radius: 10px; border: 1px solid var(--cp-border); |
| background: var(--cp-panel-2); color: var(--cp-ink); font-size: 12.5px; font-weight: 600; |
| cursor: pointer; text-decoration: none; display: inline-block; font-family: inherit; |
| transition: all .15s; |
| } |
| .cp-btn:hover { border-color: var(--cp-accent); color: var(--cp-accent); }
|
| .cp-btn-sm { padding: 5px 10px; font-size: 11.5px; border-radius: 8px; }
|
| .cp-btn-danger { border-color: rgba(255,138,138,.4); color: #ff8a8a; } |
| .cp-btn-danger:hover { background: rgba(255,138,138,.08); border-color: #ff8a8a; } |
| .cp-burger { display: none; background: none; border: 1px solid var(--cp-border); color: var(--cp-ink); border-radius: 10px; padding: 8px 12px; font-size: 16px; } |
| |
| .cp-content { padding: 24px; flex: 1; }
|
| |
| .card { |
| background: var(--cp-panel); |
| border: 1px solid var(--cp-border); |
| border-radius: 16px; padding: 22px; margin-bottom: 24px; |
| } |
| .card h2 { font-size: 16px; margin-bottom: 4px; } |
| .card .sub { font-size: 12px; color: var(--cp-muted); margin-bottom: 16px; } |
| table { width: 100%; border-collapse: collapse; font-size: 13px; } |
| th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid rgba(120,100,230,0.15); } |
| th { color: #9a9ab0; font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: .4px; } |
| tr:hover td { background: rgba(120,100,230,0.06); } |
| .badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 700; } |
| .badge-admin { background: rgba(126,231,135,0.15); color: #7ee787; } |
| .badge-user { background: rgba(120,100,230,0.15); color: #b9adff; } |
| .btn-del { |
| padding: 5px 12px; border-radius: 8px; border: 1px solid rgba(255,138,138,0.5); |
| background: transparent; color: #ff8a8a; font-size: 12px; font-weight: 600; cursor: pointer; |
| } |
| .btn-del:hover { background: rgba(255,138,138,0.15); } |
| .btn-del[disabled] { opacity: .4; cursor: not-allowed; } |
| .stat-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); |
| gap: 14px; |
| margin-bottom: 24px; |
| } |
| .stat-card { |
| background: var(--cp-panel); |
| border: 1px solid var(--cp-border); |
| border-radius: 14px; |
| padding: 16px 18px; |
| display: flex; flex-direction: column; gap: 6px; |
| transition: border-color .2s; |
| } |
| .stat-card:hover { border-color: rgba(255,94,0,.4); } |
| .stat-label { font-size: 12px; color: var(--cp-muted); display: flex; align-items: center; gap: 8px; } |
| .stat-label .dot { width: 8px; height: 8px; border-radius: 50%; } |
| .stat-value { font-size: 22px; font-weight: 800; letter-spacing: -.01em; } |
| .stat-value small { font-size: 12px; color: var(--cp-muted); font-weight: 500; } |
| .stat-delta { font-size: 11.5px; font-weight: 600; } |
| .stat-delta.up { color: #2abdbb; } |
| .stat-delta.down { color: #ff5e00; } |
| .content-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } |
| @media (max-width: 720px) { .content-grid { grid-template-columns: 1fr; } } |
| .content-box { |
| border: 1px solid rgba(120,100,230,0.2); border-radius: 12px; padding: 14px; |
| background: rgba(255,255,255,0.02); |
| } |
| .content-box h3 { font-size: 14px; margin-bottom: 10px; color: #b9adff; } |
| .content-box ul { list-style: none; max-height: 320px; overflow-y: auto; } |
| .content-box li { |
| padding: 7px 10px; border-radius: 8px; font-size: 12.5px; |
| display: flex; justify-content: space-between; gap: 10px; |
| } |
| .content-box li:nth-child(odd) { background: rgba(120,100,230,0.07); } |
| .content-box li .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| .content-box li .s { color: #9a9ab0; flex-shrink: 0; font-size: 11px; } |
| .nsfw-tag { color: #ff8a8a; font-weight: 700; } |
| .sfw-tag { color: #7ee787; font-weight: 700; } |
| .cat-tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; } |
| .cat-tab { |
| padding: 7px 16px; border-radius: 20px; border: 1px solid rgba(120,100,230,0.3); |
| background: transparent; color: #9a9ab0; font-size: 13px; font-weight: 600; cursor: pointer; |
| transition: all .2s; font-family: inherit; |
| } |
| .cat-tab:hover { color: #fff; border-color: #8b7bff; } |
| .cat-tab.active { background: linear-gradient(135deg, #7c5cff, #5a3bd6); color: #fff; border-color: transparent; } |
| .btn-go { |
| display: inline-block; padding: 4px 12px; border-radius: 8px; font-size: 12px; font-weight: 600; |
| border: 1px solid rgba(126,231,135,0.45); color: #7ee787; text-decoration: none; text-align: center; |
| } |
| .btn-go:hover { background: rgba(126,231,135,0.15); } |
| .table-wrap { max-height: 420px; overflow-y: auto; border: 1px solid rgba(120,100,230,0.15); border-radius: 12px; } |
| .msg { color: #9a9ab0; font-size: 13px; padding: 8px 0; } |
| .ok { color: #7ee787; } |
| .err { color: #ff8a8a; } |
| |
| .chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; } |
| .chip { |
| background: var(--cp-panel); |
| border: 1px solid var(--cp-border); |
| color: var(--cp-muted); |
| border-radius: 999px; |
| padding: 7px 16px; |
| font-size: 12.5px; font-weight: 600; |
| cursor: pointer; transition: all .15s; |
| font-family: inherit; |
| } |
| .chip:hover { color: var(--cp-ink); border-color: #3a4052; } |
| .chip.active { background: linear-gradient(135deg, #ff5e00, #e04a00); color: #fff; border-color: transparent; box-shadow: 0 4px 14px rgba(255,94,0,.3); } |
| |
| .product-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); |
| gap: 14px; |
| } |
| .product-card { |
| background: var(--cp-panel); |
| border: 1px solid var(--cp-border); |
| border-radius: 14px; |
| overflow: hidden; |
| display: flex; flex-direction: column; |
| transition: transform .22s ease, border-color .22s ease, box-shadow .22s ease; |
| } |
| .product-card:hover { transform: translateY(-6px); border-color: rgba(255,94,0,.45); box-shadow: 0 14px 30px rgba(0,0,0,.45); } |
| .product-thumb { position: relative; aspect-ratio: 3/3.6; overflow: hidden; background: #0d0f14; } |
| .product-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } |
| .product-thumb::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to top, rgba(13,15,20,.55), transparent 40%); pointer-events: none; } |
| .product-tag { |
| position: absolute; top: 10px; left: 10px; z-index: 2; |
| background: rgba(13,15,20,.8); backdrop-filter: blur(4px); |
| color: #ffb48a; font-size: 10px; font-weight: 700; letter-spacing: .03em; |
| border-radius: 999px; padding: 4px 10px; |
| border: 1px solid rgba(255,94,0,.4); |
| } |
| .product-body { padding: 10px 12px 12px; display: flex; flex-direction: column; gap: 5px; flex: 1; } |
| .product-anime { font-size: 10px; color: var(--cp-muted); text-transform: uppercase; letter-spacing: .06em; font-weight: 600; } |
| .product-name { font-size: 12.5px; font-weight: 600; line-height: 1.35; color: var(--cp-ink); } |
| .product-stock { font-size: 11px; font-weight: 700; }
|
| .product-stock.low { color: #ff8a8a; }
|
| .product-stock.ok { color: #2abdbb; }
|
| .product-price { display: flex; align-items: baseline; gap: 8px; }
|
| .price-now { font-size: 15px; font-weight: 800; color: #e84393; }
|
| .price-old { font-size: 12px; color: var(--cp-muted); text-decoration: line-through; } |
| |
| .empty-shop { text-align: center; padding: 50px 20px; color: var(--cp-muted); } |
| .empty-shop .big { font-size: 34px; margin-bottom: 10px; }
|
| |
| .confirm-overlay { |
| position: fixed; inset: 0; z-index: 1000; display: none; |
| align-items: center; justify-content: center; padding: 20px; |
| background: rgba(8, 9, 18, 0.72); backdrop-filter: blur(4px); |
| } |
| .confirm-overlay.active { display: flex; } |
| .confirm-box { |
| width: 100%; max-width: 380px; background: #16182b; |
| border: 1px solid rgba(255,138,138,0.35); border-radius: 16px; |
| padding: 24px; box-shadow: 0 18px 50px rgba(0,0,0,.55); |
| animation: confirmPop .22s ease; |
| } |
| @keyframes confirmPop { |
| from { transform: scale(.92); opacity: 0; } |
| to { transform: scale(1); opacity: 1; } |
| } |
| .confirm-icon { |
| width: 52px; height: 52px; margin: 0 auto 14px; border-radius: 50%; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 24px; color: #ff8a8a; background: rgba(255,138,138,0.12); |
| border: 1px solid rgba(255,138,138,0.3); |
| } |
| .confirm-title { font-size: 17px; font-weight: 700; text-align: center; color: #fff; margin-bottom: 8px; } |
| .confirm-text { font-size: 13px; text-align: center; color: #9a9ab0; margin-bottom: 22px; line-height: 1.6; } |
| .confirm-text b { color: #ff8a8a; } |
| .confirm-actions { display: flex; gap: 10px; } |
| .confirm-btn { |
| flex: 1; padding: 11px 0; border-radius: 10px; font-size: 14px; font-weight: 700; cursor: pointer; |
| border: 1px solid transparent; transition: all .2s; font-family: inherit; |
| } |
| .confirm-cancel { background: transparent; border-color: rgba(120,100,230,0.4); color: #b9adff; } |
| .confirm-cancel:hover { background: rgba(120,100,230,0.1); } |
| .confirm-ok { background: linear-gradient(135deg, #ff6b81, #f0334a); color: #fff; }
|
| .confirm-ok:hover { filter: brightness(1.1); }
|
| .cp-reason {
|
| width: 100%; margin: 0 0 18px; padding: 11px 13px;
|
| background: #0f1122; border: 1px solid rgba(255,138,138,0.35);
|
| border-radius: 10px; color: #e8eaf2; font-size: 13px; font-family: inherit;
|
| resize: vertical; outline: none;
|
| }
|
| .cp-reason:focus { border-color: #ff8a8a; }
|
| .cp-reason-err { display: none; font-size: 12px; color: #ff8a8a; margin: -10px 0 14px; }
|
| .cp-reason-err.show { display: block; }
|
| #orderModal .confirm-ok.ok-ship { background: linear-gradient(135deg, #2abdbb, #1d9a99); }
|
| #orderModal .confirm-ok.ok-ship:hover { filter: brightness(1.1); } |
| |
| |
| @media (max-width: 992px) { |
| .cp-side { position: fixed; left: 0; top: 0; transform: translateX(-100%); transition: transform .25s; } |
| .cp-side.open { transform: translateX(0); box-shadow: 0 0 40px rgba(0,0,0,.6); } |
| .cp-burger { display: block; } |
| .cp-content { padding: 16px; } |
| .stat-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; } |
| } |
| </style> |
| </head> |
| <body>
|
| |
| <aside class="cp-side" id="cpSide"> |
| <div class="cp-logo"> |
| <div class="cp-logo-mark">A</div> |
| <div> |
| <div class="cp-logo-name">ANIVIETDB Control</div> |
| <div class="cp-logo-sub">Panel · Quản trị</div> |
| </div> |
| </div> |
| <nav class="cp-nav"> |
| <div class="cp-nav-label">Tổng quan</div> |
| <a class="cp-nav-item active" data-panel="overview"><span class="ico">📈</span> Thống kê</a> |
| <div class="cp-nav-label">Quản trị</div> |
| <a class="cp-nav-item" data-panel="users"><span class="ico">👥</span> Người dùng</a> |
| <a class="cp-nav-item" data-panel="content"><span class="ico">📦</span> Nội dung</a> |
| <div class="cp-nav-label">Cửa hàng</div>
|
| <a class="cp-nav-item" data-panel="products"><span class="ico">🎓</span> Sản phẩm</a>
|
| <a class="cp-nav-item" data-panel="orders"><span class="ico">🛒</span> Đơn hàng</a>
|
| <a class="cp-nav-item" data-panel="customers"><span class="ico">👥</span> Khách hàng</a> |
| <a class="cp-nav-item" data-panel="reports"><span class="ico">📈</span> Báo cáo</a>
|
| <div class="cp-nav-label">Hệ thống</div>
|
| <a class="cp-nav-item" data-panel="updates"><span class="ico">🔄</span> Cập nhật</a> |
| <div class="cp-nav-label">Liên kết</div>
|
| <a class="cp-nav-item" href="index.html"><span class="ico">🏠</span> Trang chủ</a>
|
| <a class="cp-nav-item" href="sfw/index.html"><span class="ico">🎬</span> Xem Anime</a>
|
| <a class="cp-nav-item" href="shop/"><span class="ico">🛒</span> Cửa hàng</a> |
| </nav> |
| <div class="cp-nav-foot"> |
| <div class="cp-avatar" id="userAvatar">A</div> |
| <div class="cp-nav-foot-info"> |
| <div class="cp-nav-foot-name" id="who">—</div> |
| <div class="cp-nav-foot-sub" id="userRole">Chưa xác thực</div> |
| </div> |
| <button class="cp-auth-btn" id="logoutBtn">Thoát</button> |
| </div> |
| </aside> |
|
|
| |
| <main class="cp-main"> |
| <div class="cp-topbar"> |
| <div style="display:flex;align-items:center;gap:12px;"> |
| <button class="cp-burger" id="cpBurger">☰</button> |
| <div class="cp-topbar-title"> |
| Thống kê |
| <small>ANIVIETDB Control Panel</small> |
| </div> |
| </div> |
| <div class="cp-topbar-links">
|
| <a class="cp-btn" href="shop/">🛒 Cửa hàng</a>
|
| <a class="cp-btn" href="index.html">🏠 Trang chủ</a>
|
| <a class="cp-btn" href="sfw/index.html">🎬 Xem Anime</a> |
| </div> |
| </div> |
|
|
| <div class="cp-content"> |
| <div id="accessDenied" style="display:none;"> |
| <div class="card"> |
| <div class="msg" id="deniedMsg">Bạn không có quyền truy cập.</div> |
| </div> |
| </div> |
|
|
| <div id="panel"> |
|
|
| <div id="panel-overview">
|
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
|
| <span style="color:var(--cp-muted);font-size:13px;" id="overviewInfo"></span>
|
| <button class="cp-btn" id="refreshOverview">🔄 Làm mới</button>
|
| </div>
|
| <div class="stat-grid" id="overviewStats"></div> |
| <div class="card"> |
| <h2>Nội dung trang web</h2> |
| <p class="sub">Số lượng theo từng phân loại (anime / manga / light novel / gallery)</p> |
| <div class="stat-grid" id="contentStats"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="panel-users" style="display:none;"> |
| <div class="card"> |
| <h2>Người dùng</h2> |
| <p class="sub">Tất cả tài khoản và lần đăng nhập gần nhất</p> |
| <div class="msg" id="usersMsg"></div> |
| <table> |
| <thead> |
| <tr> |
| <th>ID</th> |
| <th>Tên đăng nhập</th> |
| <th>Email</th> |
| <th>Vai trò</th> |
| <th>Số lần tham gia</th> |
| <th>Đăng nhập gần nhất</th> |
| <th>Ngày tạo</th> |
| <th></th> |
| </tr> |
| </thead> |
| <tbody id="usersBody"></tbody> |
| </table> |
| </div> |
| </div> |
|
|
| |
| <div id="panel-content" style="display:none;"> |
| <div class="card"> |
| <h2>Quản lý trang web</h2> |
| <p class="sub">Nội dung đã upload (anime / manga)</p> |
| <div class="cat-tabs" id="catTabs"></div> |
| <div class="table-wrap"> |
| <table> |
| <thead> |
| <tr> |
| <th style="width:50px;">#</th> |
| <th>Tên</th> |
| <th style="width:90px;">Phân loại</th> |
| <th style="width:140px;">Trạng thái</th> |
| <th style="width:70px;"></th> |
| </tr> |
| </thead> |
| <tbody id="contentBody"></tbody> |
| </table> |
| </div> |
| <div class="msg" id="contentMsg"></div> |
| </div> |
| </div>
|
| |
| <div id="panel-products" style="display:none;">
|
| <div class="card">
|
| <h2>Quản lý sản phẩm</h2>
|
| <p class="sub">Figure, đồ lưu niệm, manga & Blu-ray đang bán trên ANIVIETDB Shop</p>
|
| <div style="display:flex;align-items:center;justify-content:space-between;margin:16px 0 10px;">
|
| <span style="color:var(--cp-muted);font-size:13px;" id="cpProductsInfo">Đang tải...</span>
|
| <button class="cp-btn" id="refreshProducts">🔄 Làm mới</button>
|
| </div>
|
| <div class="chip-row" id="cpCatChips">
|
| <button class="chip active" data-cat="all">Tất cả</button>
|
| </div>
|
| <div class="product-grid" id="cpProductGrid"></div>
|
| <div class="empty-shop" id="cpEmptyProducts" style="display:none;">
|
| <div class="big">😅</div>
|
| Không có sản phẩm phù hợp.
|
| </div>
|
| </div>
|
| </div> |
|
|
|
|
| <div id="panel-orders" style="display:none;">
|
| <div class="card">
|
| <h2>Đơn hàng</h2>
|
| <p class="sub">Đơn hàng từ khách hàng (từ DB shop-system)</p>
|
| <div style="display:flex;align-items:center;justify-content:space-between;margin:16px 0 10px;">
|
| <span style="color:var(--cp-muted);font-size:13px;" id="ordersInfo">Đang tải...</span>
|
| <button class="cp-btn" id="refreshOrders">🔄 Làm mới</button>
|
| </div>
|
| <div class="table-wrap">
|
| <table>
|
| <thead>
|
| <tr>
|
| <th>Mã đơn</th>
|
| <th>Khách hàng</th>
|
| <th>Sản phẩm</th>
|
| <th style="width:90px;">Số món</th>
|
| <th style="width:110px;">Tổng tiền</th>
|
| <th style="width:90px;">Trạng thái</th>
|
| <th style="width:150px;">Thời gian</th>
|
| <th style="width:110px;">Thao tác</th>
|
| </tr>
|
| </thead>
|
| <tbody id="ordersBody"></tbody> |
| </table>
|
| </div>
|
| <div class="msg" id="ordersMsg"></div>
|
| </div>
|
| </div>
|
|
|
| |
| <div id="panel-customers" style="display:none;"> |
| <div class="card"> |
| <h2>Khách hàng</h2>
|
| <p class="sub">Mỗi tài khoản là 1 khách hàng — số đơn / món / chi tiêu lấy từ DB shop-system</p>
|
| <div class="stat-grid" id="customerStats"></div>
|
| <div style="display:flex;align-items:center;justify-content:space-between;margin:16px 0 10px;">
|
| <span style="color:var(--cp-muted);font-size:13px;">Danh sách từ máy chủ <code>shop-system</code></span>
|
| <button class="cp-btn" id="refreshCustomers">🔄 Làm mới</button>
|
| </div> |
| <table>
|
| <thead>
|
| <tr>
|
| <th>ID</th>
|
| <th>Khách hàng</th>
|
| <th>Đơn hàng</th>
|
| <th>Món đã mua</th>
|
| <th>Tổng chi tiêu</th>
|
| <th>Điểm</th>
|
| <th>Ngày tạo</th>
|
| </tr>
|
| </thead>
|
| <tbody id="customersBody"></tbody>
|
| </table> |
| </div> |
| </div> |
|
|
|
|
| <div id="panel-reports" style="display:none;">
|
| <div class="card">
|
| <div style="display:flex;align-items:center;justify-content:space-between;">
|
| <div>
|
| <h2>Báo cáo doanh thu</h2>
|
| <p class="sub">Tổng hợp doanh thu cửa hàng</p>
|
| </div>
|
| <button class="cp-btn" id="refreshReports">🔄 Làm mới</button>
|
| </div>
|
| <div class="stat-grid" id="reportStats"></div>
|
| <div class="content-box">
|
| <h3>Doanh thu 7 ngày gần nhất</h3>
|
| <canvas id="revenueChart" width="800" height="260" style="width:100%;height:auto;display:block;background:#0d0f14;border-radius:10px;"></canvas>
|
| <div class="msg" style="margin-top:10px;">Biểu đồ cập nhật khi mở panel Báo cáo.</div>
|
| </div>
|
| </div>
|
| </div> |
|
|
|
|
| <div id="panel-updates" style="display:none;">
|
| <div class="card">
|
| <h2>Cập nhật hệ thống</h2>
|
| <p class="sub">Lịch sử cập nhật phiên bản ANIVIETDB</p>
|
| <div class="content-box" style="line-height:1.7;">
|
| <div class="update-item">
|
| <h3 style="margin:0 0 4px;">v2.4 — Luồng thanh toán & hủy đơn</h3>
|
| <p class="update-date">17/08/2026</p>
|
| <ul>
|
| <li>Đơn hàng có thêm trạng thái <code>Chờ thanh toán</code> — khách phải bấm “Tôi đã chuyển khoản” để đơn vào <code>Chờ xác nhận</code>; nếu không thanh toán trong 5 phút, đơn tự động hủy.</li>
|
| <li>Trang thanh toán mới: QR + thông tin đơn (mã đơn, người mua, sản phẩm, tổng tiền), nút “Tôi đã chuyển khoản” chờ 5 giây rồi quay về.</li>
|
| <li>Khách có thể hủy đơn (<code>Chờ thanh toán</code> / <code>Chờ xác nhận</code>) kèm lí do bằng popup; admin hủy bất kỳ đơn nào kèm lí do.</li>
|
| <li>Đơn <code>Đã hủy</code> hiển lí do hủy; stock được hoàn lại.</li>
|
| <li>Pop-up thay cho alert trình duyệt khi xác nhận / hủy đơn; đơn hàng trong shop chỉ hiển đơn của chính mình.</li>
|
| <li>Sửa lỗi hiển thị “sản phẩm” trong giỏ hàng.</li>
|
| </ul>
|
| </div>
|
| <hr style="border:none;border-top:1px solid var(--cp-border);margin:16px 0;">
|
| <div class="update-item">
|
| <h3 style="margin:0 0 4px;">v2.3 — Thanh toán & xác nhận đơn hàng</h3>
|
| <p class="update-date">17/08/2026</p>
|
| <ul>
|
| <li>Sau khi đặt hàng, khách chuyển qua trang thanh toán (QR ủng hộ + mã đơn + đếm ngược thời gian thanh toán).</li>
|
| <li>Admin bấm “Xác nhận” trên đơn ở trạng thái <code>Chờ xác nhận</code> → đơn chuyển sang <code>Đang giao hàng</code>, tự động thành <code>Đã giao</code> sau 5 phút.</li>
|
| <li>Trang shop tự cập nhật trạng thái đơn hàng theo thời gian thực.</li>
|
| </ul>
|
| </div>
|
| <hr style="border:none;border-top:1px solid var(--cp-border);margin:16px 0;">
|
| <div class="update-item">
|
| <h3 style="margin:0 0 4px;">v2.2 — Shop dừ liệu thực</h3>
|
| <p class="update-date">17/08/2026</p>
|
| <ul>
|
| <li>Sản phẩm và đơn hàng lưu trư trên DB (worker <code>shop-system</code> + D1): danh mục sản phẩm có tồn kho (stock), mua hàng trừ stock.</li>
|
| <li>Khách hàng = tài khoản: mỗi user là 1 khách hàng, thống kê đơn hàng / món / tổng chi tiêu tính từ DB.</li>
|
| <li>Tổng quan & Báo cáo: doanh thu, đơn hàng, món đã bán lấy số liệu thực từ DB (không còn số dự kiến).</li>
|
| <li>Control Panel: panel Đơn hàng mới + quản lý tồn kho sản phẩm (tăng/giảm stock).</li>
|
| </ul>
|
| </div>
|
| <hr style="border:none;border-top:1px solid var(--cp-border);margin:16px 0;">
|
| <div class="update-item">
|
| <h3 style="margin:0 0 4px;">v2.1 — Shop & Control Panel</h3>
|
| <p class="update-date">16/08/2026</p>
|
| <ul>
|
| <li>Thêm cửa hàng figure, đồ lưu niệm, manga & Blu-ray (shop/index.html) — giỏ hàng, đặt hàng.</li>
|
| <li>Control Panel: sidebar điều hướng, thống kê, quản lý sản phẩm, khách hàng, báo cáo, cài đặt.</li>
|
| <li>Trang chủ thêm stage 3 giới thiệu shop với 4 sản phẩm tiêu biểu.</li>
|
| <li>Đồng bộ khách hàng vào DB D1 riêng <b>shop-system</b> qua worker <code>shop-system</code>.</li>
|
| </ul>
|
| </div>
|
| <hr style="border:none;border-top:1px solid var(--cp-border);margin:16px 0;">
|
| <div class="update-item">
|
| <h3 style="margin:0 0 4px;">v2.0 — Giao diện mới</h3>
|
| <p class="update-date">15/08/2026</p>
|
| <ul>
|
| <li>Trang chủ Crunchyroll-style với hero 3D + poster grid + phim nổi bật.</li>
|
| <li>Nền poster-grid stage 1 áp dụng toàn bộ trang (style.css).</li>
|
| <li>Đổi tên nút "Xem feature" thành "Xem phim nổi bật".</li>
|
| </ul>
|
| </div>
|
| <hr style="border:none;border-top:1px solid var(--cp-border);margin:16px 0;">
|
| <div class="update-item">
|
| <h3 style="margin:0 0 4px;">v1.x — Nền tảng</h3>
|
| <p class="update-date">Trước 15/08/2026</p>
|
| <ul>
|
| <li>Đăng ký / đăng nhập qua worker auth (sginup-loginsystem).</li>
|
| <li>Sưu tầm anime, manga, manhwa, manhua, light novel & gallery (SFW + NSFW).</li>
|
| </ul>
|
| </div>
|
| </div>
|
| </div>
|
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| <div class="confirm-overlay" id="confirmOverlay">
|
| <div class="confirm-box">
|
| <div class="confirm-icon">⚠</div>
|
| <div class="confirm-title">Xóa người dùng?</div>
|
| <div class="confirm-text">Bạn có chắc muốn xóa tài khoản <b id="confirmTarget"></b>? Hành động này không thể hoàn tác.</div>
|
| <div class="confirm-actions">
|
| <button class="confirm-btn confirm-cancel" id="confirmCancel">Hủy</button>
|
| <button class="confirm-btn confirm-ok" id="confirmOk">Xóa</button>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="confirm-overlay" id="orderModal">
|
| <div class="confirm-box">
|
| <div class="confirm-icon">🛒</div>
|
| <div class="confirm-title" id="orderModalTitle">Xác nhận đơn</div>
|
| <div class="confirm-text" id="orderModalText"></div>
|
| <textarea id="orderModalReason" class="cp-reason" placeholder="Nhập lí do hủy (bắt buộc)..." rows="3" style="display:none;"></textarea>
|
| <div class="cp-reason-err" id="orderModalReasonErr">⚠️ Vui lòng nhập lí do hủy đơn</div>
|
| <div class="confirm-actions">
|
| <button class="confirm-btn confirm-cancel" id="orderModalCancel">Đóng</button>
|
| <button class="confirm-btn confirm-ok" id="orderModalOk">Xác nhận</button>
|
| </div>
|
| </div>
|
| </div> |
| <script src="signup-loginsystem.js?v=9"></script> |
| <script> |
| var whoEl = document.getElementById('who'); |
| var roleEl = document.getElementById('userRole'); |
| var avatarEl = document.getElementById('userAvatar'); |
| var panel = document.getElementById('panel'); |
| var denied = document.getElementById('accessDenied'); |
| |
| var confirmOverlay = document.getElementById('confirmOverlay');
|
| var confirmCancel = document.getElementById('confirmCancel');
|
| var confirmOk = document.getElementById('confirmOk');
|
|
|
| var PANELS = ['overview', 'users', 'content', 'products', 'orders', 'customers', 'reports', 'updates']; |
| var currentCat = 'anime';
|
|
|
| var IMG = "https://phim.nguonc.com/public/images/";
|
| var PRODUCTS = [];
|
| var SHOP_API = localStorage.getItem("anivietdb-shop-api") || "https://shop-system.npgia-himlam.workers.dev/";
|
| var shopCustomers = [];
|
| var shopOrders = [];
|
| var cpStats = null;
|
|
|
| function api(url, init) {
|
| var base = SHOP_API.replace(/\/$/, "");
|
| var token = AuthSystem.getToken();
|
| init = init || {};
|
| init.headers = init.headers || {};
|
| if (token) init.headers["Authorization"] = "Bearer " + token;
|
| return fetch(base + url, init).then(function (r) {
|
| return r.json().then(function (data) {
|
| if (!r.ok) { var e = new Error(data.error || ("HTTP " + r.status)); e.status = r.status; e.data = data; throw e; }
|
| return data;
|
| });
|
| });
|
| } |
| |
| function fmt(n) { return n.toLocaleString("vi-VN") + "đ"; } |
| function $(s) { return document.querySelector(s); } |
| function $$(s) { return document.querySelectorAll(s); } |
| |
| function closeConfirm() { |
| confirmOverlay.classList.remove('active'); |
| confirmOk.onclick = null; |
| } |
| confirmCancel.addEventListener('click', closeConfirm); |
| confirmOverlay.addEventListener('click', function (e) { if (e.target === confirmOverlay) closeConfirm(); }); |
| document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeConfirm(); }); |
| |
| function switchPanel(name) { |
| $$(".cp-nav-item[data-panel]").forEach(function (a) { a.classList.toggle('active', a.dataset.panel === name); }); |
| PANELS.forEach(function (n) { |
| var el = $('#panel-' + n); |
| if (el) el.style.display = n === name ? 'block' : 'none'; |
| }); |
| var t = document.querySelector('.cp-topbar-title'); |
| if (t) { |
| var map = { overview: 'Thống kê', users: 'Người dùng', content: 'Nội dung', products: 'Sản phẩm', orders: 'Đơn hàng', customers: 'Khách hàng', reports: 'Báo cáo', updates: 'Cập nhật' };
|
| t.childNodes[0].textContent = map[name] || 'Thống kê';
|
| }
|
| if (name === 'overview') renderOverview();
|
| if (name === 'products') loadShopProducts();
|
| if (name === 'orders') loadShopOrders();
|
| if (name === 'customers') loadShopCustomers();
|
| if (name === 'reports') renderReports();
|
| if (name === 'updates') renderUpdates();
|
| } |
| |
| function fmtTime(iso) { |
| if (!iso) return '—'; |
| try { |
| var d = new Date(iso); |
| return d.toLocaleString('vi-VN', { hour12: false }); |
| } catch (e) { return iso; } |
| } |
| |
| function esc(s) { |
| return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { |
| return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; |
| }); |
| }
|
| function renderUsers(users) { |
| var body = document.getElementById('usersBody'); |
| body.innerHTML = users.map(function (u) { |
| var isSelf = u.id === (AuthSystem.getUser() || {}).id; |
| var badge = u.role === 'admin' ? '<span class="badge badge-admin">ADMIN</span>' : '<span class="badge badge-user">USER</span>'; |
| var del = u.role === 'admin' |
| ? '<button class="btn-del" disabled title="Không thể xóa Admin">—</button>' |
| : '<button class="btn-del" data-id="' + u.id + '">Xóa</button>'; |
| return '<tr>' + |
| '<td>' + u.id + '</td>' + |
| '<td>' + esc(u.username) + (isSelf ? ' <span style="color:#7ee787;font-size:11px">(bạn)</span>' : '') + '</td>' + |
| '<td>' + esc(u.email) + '</td>' + |
| '<td>' + badge + '</td>' + |
| '<td>' + (u.login_count || 0) + '</td>' + |
| '<td>' + fmtTime(u.last_login_at) + '</td>' + |
| '<td>' + fmtTime(u.created_at) + '</td>' + |
| '<td>' + del + '</td>' + |
| '</tr>'; |
| }).join(''); |
| body.querySelectorAll('.btn-del[data-id]').forEach(function (btn) { |
| btn.addEventListener('click', function () { |
| var id = btn.getAttribute('data-id'); |
| var row = btn.closest('tr'); |
| var name = row ? row.cells[1].textContent.trim() : ''; |
| confirmDelete(id, name); |
| }); |
| }); |
| } |
| |
| function confirmDelete(id, name) { |
| var u = AuthSystem.getUser(); |
| if (u && String(u.id) === String(id)) { return; } |
| document.getElementById('confirmTarget').textContent = name || 'ID ' + id; |
| confirmOverlay.classList.add('active'); |
| confirmOk.onclick = function () { |
| confirmOverlay.classList.remove('active'); |
| deleteUser(id); |
| }; |
| } |
| |
| function deleteUser(id) { |
| AuthSystem.adminDeleteUser(id).then(function () { |
| loadUsers(); |
| }).catch(function (e) { |
| if (!handleAuthFail(e)) document.getElementById('usersMsg').textContent = 'Lỗi: ' + e.message; |
| }); |
| } |
| |
| function handleAuthFail(e) { |
| console.log('[control-panel] FAIL status=', e && e.status, 'msg=', e && e.message, 'data=', e && e.data, 'base=', AuthSystem.baseUrl); |
| if (e && e.status === 401) { |
| AuthSystem.clearSession(); |
| return false; |
| } |
| return false; |
| } |
| |
| function loadUsers() { |
| var msg = document.getElementById('usersMsg'); |
| msg.textContent = 'Đang tải...'; |
| AuthSystem.adminUsers().then(function (data) { |
| msg.textContent = 'Tổng cộng ' + data.users.length + ' tài khoản.'; |
| renderUsers(data.users); |
| }).catch(function (e) { |
| msg.textContent = 'Lỗi tải danh sách: ' + e.message + |
| ' (base=' + AuthSystem.baseUrl + ', token=' + (AuthSystem.getToken() ? 'có' : 'KHÔNG') + ')'; |
| handleAuthFail(e); |
| }); |
| } |
| |
| function loadContent() { |
| var CATS = [ |
| { key: 'anime', label: 'Anime' }, |
| { key: 'manga', label: 'Manga' }, |
| { key: 'manhwa', label: 'Manhwa' }, |
| { key: 'manhua', label: 'Manhua' }, |
| { key: 'light-novel', label: 'Light Novel' }, |
| { key: 'gallery', label: 'Galleries' } |
| ]; |
| var byCat = {}; |
| CATS.forEach(function (c) { byCat[c.key] = []; }); |
| var animeErr = null; |
| var mangaErr = null; |
| var metaErr = null; |
| var metaById = {}; |
| |
| function push(cat, name, nsfw, status, slug, animePages, id) { |
| byCat[cat].push({ |
| id: id || '', |
| name: name || '(không tên)', |
| nsfw: !!nsfw, |
| status: status || '', |
| slug: slug || '', |
| animePages: animePages || [] |
| }); |
| } |
| |
| var p1 = fetch('data/manga_map.json').then(function (r) { |
| if (!r.ok) throw new Error('HTTP ' + r.status); |
| return r.json(); |
| }) |
| .then(function (arr) { |
| (Array.isArray(arr) ? arr : []).forEach(function (a) { |
| push('anime', a.titleVN || a.titleEN || a.slug, a.nsfw, a.status, a.slug, a.animePages); |
| }); |
| }) |
| .catch(function (e) { animeErr = e; }); |
| var p2 = fetch('data/manga-sources.json').then(function (r) { |
| if (!r.ok) throw new Error('HTTP ' + r.status); |
| return r.json(); |
| }) |
| .then(function (arr) { |
| (Array.isArray(arr) ? arr : []).forEach(function (m) { |
| var cat = m.type === 'gallery' ? 'gallery' : 'manga'; |
| push(cat, m.titleVN || m.titleEN || m.slug, m.nsfw, m.status, m.slug, [], m.id); |
| }); |
| }) |
| .catch(function (e) { mangaErr = e; }); |
| var p3 = fetch('data/manga-metadata.json').then(function (r) { |
| if (!r.ok) throw new Error('HTTP ' + r.status); |
| return r.json(); |
| }) |
| .then(function (arr) { |
| (Array.isArray(arr) ? arr : []).forEach(function (e) { |
| if (e && e.metadata && e.metadata.status) metaById[e.id] = e.metadata.status; |
| }); |
| }) |
| .catch(function (e) { metaErr = e; }); |
| |
| Promise.all([p1, p2, p3]).then(function () { |
| Object.keys(byCat).forEach(function (cat) { |
| byCat[cat].forEach(function (it) { |
| if (!it.status && metaById[it.id]) it.status = metaById[it.id]; |
| }); |
| }); |
| var msg = document.getElementById('contentMsg'); |
| if (animeErr || mangaErr || metaErr) { |
| msg.textContent = 'Lỗi tải nội dung: ' + |
| (animeErr ? ('manga_map.json → ' + animeErr.message) : '') + |
| (animeErr && (mangaErr || metaErr) ? ' | ' : '') + |
| (mangaErr ? ('manga-sources.json → ' + mangaErr.message) : '') + |
| (mangaErr && metaErr ? ' | ' : '') + |
| (metaErr ? ('manga-metadata.json → ' + metaErr.message) : '') + |
| '. Nếu mở file trực tiếp (file://), hãy dùng Live Server.'; |
| msg.style.color = '#f85149'; |
| } else { |
| msg.textContent = ''; |
| msg.style.color = ''; |
| } |
| |
| var statsEl = document.getElementById('contentStats'); |
| if (statsEl) { |
| statsEl.innerHTML = CATS.map(function (c) { |
| return '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#a3419f;"></span> ' + c.label + '</span><span class="stat-value">' + byCat[c.key].length + '</span></div>'; |
| }).join(''); |
| } |
| |
| var tabsWrap = document.getElementById('catTabs'); |
| tabsWrap.innerHTML = CATS.map(function (c, i) { |
| return '<button class="cat-tab' + (i === 0 ? ' active' : '') + '" data-cat="' + c.key + '">' + c.label + '</button>'; |
| }).join(''); |
| |
| var body = document.getElementById('contentBody'); |
| |
| function itemLink(cat, item) { |
| if (cat === 'anime') { |
| var p = item.animePages && item.animePages[0]; |
| if (p) return (p.indexOf('nsfw/') === 0 ? '' : 'sfw/') + p; |
| return (item.nsfw ? 'nsfw/' : 'sfw/') + 'anime/' + item.slug + '/'; |
| } |
| if (cat === 'gallery') return 'sfw/galleries/' + item.slug + '/index.html'; |
| return 'sfw/manga/' + item.slug + '/index.html'; |
| } |
| |
| function renderTable() { |
| var STATUS_VN = { ongoing: 'Đang ra', completed: 'Hoàn thành', hiatus: 'Tạm ngưng', cancelled: 'Bị hủy' }; |
| var items = byCat[currentCat]; |
| body.innerHTML = items.length ? items.map(function (it, i) { |
| var tag = it.nsfw |
| ? '<span class="nsfw-tag">NSFW</span>' |
| : '<span class="sfw-tag">SFW</span>'; |
| var status = it.status ? (STATUS_VN[it.status] || it.status) : '—'; |
| return '<tr>' + |
| '<td>' + (i + 1) + '</td>' + |
| '<td>' + esc(it.name) + '</td>' + |
| '<td>' + tag + '</td>' + |
| '<td>' + esc(status) + '</td>' + |
| '<td style="width:70px;"><a class="btn-go" href="' + esc(itemLink(currentCat, it)) + '" target="_blank">Xem</a></td>' + |
| '</tr>'; |
| }).join('') : '<tr><td colspan="5" class="msg">Chưa có dữ liệu</td></tr>'; |
| } |
| |
| tabsWrap.addEventListener('click', function (e) { |
| var btn = e.target.closest('.cat-tab'); |
| if (!btn) return; |
| tabsWrap.querySelectorAll('.cat-tab').forEach(function (t) { t.classList.remove('active'); }); |
| btn.classList.add('active'); |
| currentCat = btn.getAttribute('data-cat'); |
| renderTable(); |
| }); |
| |
| renderTable(); |
| }); |
| }
|
|
|
| var cpCurrentCat = 'all';
|
| var CATS = [];
|
|
|
| function cpCardHTML(p) {
|
| var stockCls = p.stock <= 3 ? 'low' : 'ok';
|
| var stockTxt = p.stock <= 3 ? 'Sắp hết (' + p.stock + ')' : 'Còn ' + p.stock;
|
| var tag = '<span class="product-tag">' + p.cat + '</span>';
|
| var sale = p.old ? ' <span class="price-old">' + fmt(p.old) + '</span>' : '';
|
| return '<div class="product-card" data-id="' + p.id + '">' +
|
| '<div class="product-thumb">' + tag +
|
| '<img src="' + p.img + '" alt="' + p.name + '" loading="lazy">' +
|
| '</div>' +
|
| '<div class="product-body">' +
|
| '<div class="product-anime">' + p.anime + '</div>' +
|
| '<div class="product-name">' + p.name + '</div>' +
|
| '<div class="product-price"><span class="price-now">' + fmt(p.price) + '</span>' + sale + '</div>' +
|
| '<div class="product-stock ' + stockCls + '" id="cpStock-' + p.id + '">' + stockTxt + '</div>' +
|
| '<div class="product-stock" style="display:flex;gap:8px;align-items:center;margin-top:2px;">' +
|
| '<button class="cp-btn" data-stock="' + p.id + '" data-delta="-1" style="padding:3px 10px;font-size:12px;">-1</button>' +
|
| '<button class="cp-btn" data-stock="' + p.id + '" data-delta="1" style="padding:3px 10px;font-size:12px;">+1</button>' +
|
| '<button class="cp-btn" data-stock="' + p.id + '" data-delta="5" style="padding:3px 10px;font-size:12px;">+5</button>' +
|
| '</div>' +
|
| '</div></div>';
|
| }
|
|
|
| function renderCpProducts(filterCat, query) {
|
| filterCat = filterCat || 'all';
|
| query = (query || '').toLowerCase();
|
| var list = PRODUCTS.filter(function (p) {
|
| return (filterCat === 'all' || p.cat === filterCat) &&
|
| (!query || p.name.toLowerCase().indexOf(query) !== -1 || p.anime.toLowerCase().indexOf(query) !== -1);
|
| });
|
| $('#cpProductGrid').innerHTML = list.map(cpCardHTML).join('');
|
| $('#cpEmptyProducts').style.display = list.length ? 'none' : 'block';
|
| }
|
|
|
| function loadShopProducts() {
|
| var info = $('#cpProductsInfo');
|
| if (info) info.textContent = 'Đang tải...';
|
| api('/api/shop/products').then(function (d) {
|
| PRODUCTS = Array.isArray(d.products) ? d.products : [];
|
| var seen = [];
|
| PRODUCTS.forEach(function (p) { if (seen.indexOf(p.cat) === -1) seen.push(p.cat); });
|
| CATS = seen;
|
| if (info) info.textContent = PRODUCTS.length + ' sản phẩm';
|
| var wrap = $('#cpCatChips');
|
| wrap.innerHTML = '<button class="chip active" data-cat="all">Tất cả <span style="opacity:.7">(' + PRODUCTS.length + ')</span></button>';
|
| CATS.forEach(function (c) {
|
| var n = PRODUCTS.filter(function (p) { return p.cat === c; }).length;
|
| var b = document.createElement('button');
|
| b.className = 'chip';
|
| b.dataset.cat = c;
|
| b.innerHTML = c + " <span style='opacity:.7'>(" + n + ")</span>";
|
| wrap.appendChild(b);
|
| });
|
| renderCpProducts('all', '');
|
| }).catch(function (e) {
|
| $('#cpEmptyProducts').style.display = 'block';
|
| $('#cpEmptyProducts').innerHTML = '<div class="big">😅</div>Lỗi tải sản phẩm: ' + esc(e.message);
|
| });
|
| }
|
|
|
| function setStock(id, delta) {
|
| var btn = $('#cpProductGrid').querySelector('[data-stock="' + id + '"][data-delta="' + delta + '"]');
|
| if (btn) { btn.disabled = true; }
|
| api('/api/shop/products/' + id, {
|
| method: 'PATCH',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ delta: delta })
|
| }).then(function (d) {
|
| var p = PRODUCTS.find(function (x) { return x.id === d.product.id; });
|
| if (p) { p.stock = d.product.stock; }
|
| var el = $('#cpStock-' + id);
|
| if (el) {
|
| var cls = d.product.stock <= 3 ? 'low' : 'ok';
|
| var txt = d.product.stock <= 3 ? 'Sắp hết (' + d.product.stock + ')' : 'Còn ' + d.product.stock;
|
| el.className = 'product-stock ' + cls;
|
| el.textContent = txt;
|
| }
|
| }).catch(function (e) {
|
| alert('Lỗi cập nhật stock: ' + e.message);
|
| }).finally(function () {
|
| if (btn) { btn.disabled = false; }
|
| });
|
| }
|
|
|
| $('#cpProductGrid').addEventListener('click', function (e) {
|
| var b = e.target.closest('[data-stock]');
|
| if (!b) return;
|
| setStock(Number(b.dataset.stock), Number(b.dataset.delta));
|
| });
|
|
|
|
|
| function renderCustomers() {
|
| var bought = shopCustomers.length ? shopCustomers.filter(function (c) { return (c.orders_count || 0) > 0; }).length : 0;
|
| var totalSpent = shopCustomers.reduce(function (s, c) { return s + (c.total_spent || 0); }, 0);
|
| $('#customerStats').innerHTML =
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#2abdbb;"></span> Tổng khách hàng (user)</span><span class="stat-value">' + (shopCustomers.length || 0) + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#ff5e00;"></span> Đã mua hàng</span><span class="stat-value">' + bought + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#a3419f;"></span> Tổng chi tiêu</span><span class="stat-value">' + fmt(totalSpent) + '</span></div>';
|
|
|
| if (!shopCustomers.length) {
|
| $('#customersBody').innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--cp-muted);padding:24px;">Chưa có dữ liệu khách hàng từ máy chủ shop-system. Bấm nút đồng bộ phía dưới.</td></tr>';
|
| return;
|
| }
|
| $('#customersBody').innerHTML = shopCustomers.map(function (c, i) {
|
| var name = esc(c.name || 'Khách');
|
| var badge = c.role === 'admin' ? '<span class="badge badge-admin">ADMIN</span>' : '<span class="badge badge-user">USER</span>';
|
| var spent = (c.total_spent || 0) > 0 ? fmt(c.total_spent) : '<span style="color:var(--cp-muted);">N/A</span>';
|
| var items = c.items_count || 0;
|
| return '<tr>' +
|
| '<td>' + c.id + '</td>' +
|
| '<td>' + name + badge + (c.email ? '<div style="font-size:11px;color:var(--cp-muted);">' + esc(c.email) + '</div>' : '') + '</td>' +
|
| '<td>' + (c.orders_count || 0) + '</td>' +
|
| '<td>' + items + '</td>' +
|
| '<td>' + spent + '</td>' +
|
| '<td>⭐ ' + Number(c.points || 0).toLocaleString('vi-VN') + '</td>' +
|
| '<td>' + fmtTime(c.created_at) + '</td>' +
|
| '</tr>';
|
| }).join('');
|
| }
|
|
|
| function loadShopCustomers() {
|
| if (!AuthSystem.getToken()) return;
|
| api('/api/shop/customers').then(function (d) {
|
| shopCustomers = Array.isArray(d.customers) ? d.customers : [];
|
| renderCustomers();
|
| }).catch(function () { renderCustomers(); });
|
| }
|
|
|
| $('#refreshCustomers').addEventListener('click', function () {
|
| var btn = this;
|
| btn.innerHTML = '⌛ Đang đồng bộ…';
|
| loadShopCustomers();
|
| setTimeout(function () { btn.innerHTML = '🔄 Làm mới'; }, 1200);
|
| });
|
|
|
|
|
| function renderShopOrders() {
|
| $('#ordersInfo').textContent = shopOrders.length ? 'Tổng cộng ' + shopOrders.length + ' đơn hàng.' : 'Chưa có đơn hàng nào.';
|
| if (!shopOrders.length) {
|
| $('#ordersBody').innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--cp-muted);padding:24px;">Chưa có đơn hàng.</td></tr>';
|
| return;
|
| }
|
| $('#ordersBody').innerHTML = shopOrders.map(function (o) {
|
| var items = (o.items || []).map(function (it) { return it.name + ' x' + it.q; }).join(', ');
|
| var action = '';
|
| if (o.status === 'Chờ xác nhận') {
|
| action = '<div style="display:flex;gap:6px;flex-wrap:wrap;">' +
|
| '<button class="cp-btn cp-btn-sm" data-ship="' + o.id + '" title="Xác nhận và chuyển sang giao hàng">🚚 Xác nhận</button>' +
|
| '<button class="cp-btn cp-btn-sm cp-btn-danger" data-cancel="' + o.id + '" title="Hủy đơn (kèm lí do)">❌ Hủy</button>' +
|
| '</div>';
|
| } else if (o.status === 'Chờ thanh toán') {
|
| action = '<button class="cp-btn cp-btn-sm cp-btn-danger" data-cancel="' + o.id + '" title="Hủy đơn (kèm lí do)">❌ Hủy</button>';
|
| } else if (o.status === 'Đang giao hàng') {
|
| action = '<div style="display:flex;gap:6px;flex-wrap:wrap;">' +
|
| '<span style="font-size:11px;color:var(--cp-muted);align-self:center;">🚚 Đang giao…</span>' +
|
| '<button class="cp-btn cp-btn-sm cp-btn-danger" data-cancel="' + o.id + '" title="Hủy đơn (kèm lí do)">❌ Hủy</button>' +
|
| '</div>';
|
| } else if (o.status === 'Đã giao') {
|
| action = '<span style="font-size:11px;color:#2abdbb;">✅ Đã giao</span>';
|
| } else if (o.status === 'Đã hủy') {
|
| action = '<span style="font-size:11px;color:#ff8a8a;">❌ Đã hủy</span>';
|
| }
|
| var reasonRow = (o.status === 'Đã hủy' && o.cancel_reason)
|
| ? '<div style="font-size:11px;color:#ff8a8a;margin-top:3px;">Lí do: ' + esc(o.cancel_reason) + '</div>'
|
| : '';
|
| return '<tr>' +
|
| '<td>' + o.code + '</td>' +
|
| '<td>' + esc(o.customer_name) + (o.customer_email ? '<div style="font-size:11px;color:var(--cp-muted);">' + esc(o.customer_email) + '</div>' : '') + '</td>' +
|
| '<td>' + esc(items) + '</td>' +
|
| '<td>' + o.item_count + '</td>' +
|
| '<td>' + fmt(o.total) + '</td>' +
|
| '<td>' + esc(o.status) + reasonRow + '</td>' +
|
| '<td>' + fmtTime(o.created_at) + '</td>' +
|
| '<td>' + action + '</td>' +
|
| '</tr>';
|
| }).join('');
|
| }
|
|
|
| function loadShopOrders() {
|
| if (!AuthSystem.getToken()) return;
|
| $('#ordersMsg').textContent = 'Đang tải...';
|
| api('/api/shop/orders/all').then(function (d) {
|
| shopOrders = Array.isArray(d.orders) ? d.orders : [];
|
| renderShopOrders();
|
| $('#ordersMsg').textContent = '';
|
| }).catch(function (e) {
|
| $('#ordersMsg').textContent = 'Lỗi tải đơn hàng: ' + e.message;
|
| });
|
| }
|
|
|
| $('#refreshOrders').addEventListener('click', loadShopOrders);
|
| $('#refreshProducts').addEventListener('click', loadShopProducts);
|
| $('#refreshOverview').addEventListener('click', renderOverview);
|
| $('#refreshReports').addEventListener('click', function () {
|
| api('/api/shop/stats').then(function (d) {
|
| cpStats = d.stats;
|
| renderReports();
|
| }).catch(function (e) {
|
| $('#reportStats').innerHTML = '<div class="msg err">Lỗi tải lại: ' + esc(e.message) + '</div>';
|
| });
|
| });
|
|
|
|
|
| function openOrderModal(type, id) {
|
| var order = shopOrders.filter(function (o) { return o.id === id; })[0];
|
| var label = order ? order.code : ('#' + id);
|
| var modal = document.getElementById('orderModal');
|
| document.getElementById('orderModalTitle').textContent =
|
| type === 'ship' ? 'Xác nhận đơn ' + label : 'Hủy đơn ' + label;
|
| document.getElementById('orderModalText').textContent =
|
| type === 'ship'
|
| ? 'Xác nhận đơn ' + label + ' đã thanh toán và chuyển sang giao hàng?'
|
| : 'Đơn ' + label + ' sẽ bị hủy và trả lại số lượng sản phẩm vào kho. Nhập lí do hủy:';
|
| document.getElementById('orderModalOk').innerHTML =
|
| type === 'ship' ? '🚚 Xác nhận' : '❌ Xác nhận hủy';
|
| document.getElementById('orderModalOk').className =
|
| 'confirm-btn confirm-ok' + (type === 'ship' ? ' ok-ship' : '');
|
| document.getElementById('orderModalReason').style.display =
|
| type === 'ship' ? 'none' : 'block';
|
| document.getElementById('orderModalReason').value = '';
|
| document.getElementById('orderModalReasonErr').classList.remove('show');
|
| modal.classList.add('active');
|
| modal.dataset.type = type;
|
| modal.dataset.id = id;
|
| setTimeout(function () {
|
| if (type === 'cancel') document.getElementById('orderModalReason').focus();
|
| }, 120);
|
| }
|
| function closeOrderModal() {
|
| document.getElementById('orderModal').classList.remove('active');
|
| }
|
|
|
| document.getElementById('orderModalCancel').addEventListener('click', closeOrderModal);
|
| document.getElementById('orderModalReason').addEventListener('input', function () {
|
| if (this.value.trim()) document.getElementById('orderModalReasonErr').classList.remove('show');
|
| });
|
| document.getElementById('orderModal').addEventListener('click', function (e) {
|
| if (e.target === this) closeOrderModal();
|
| });
|
| document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeOrderModal(); });
|
| document.getElementById('orderModalOk').addEventListener('click', function () {
|
| var modal = document.getElementById('orderModal');
|
| var type = modal.dataset.type;
|
| var id = Number(modal.dataset.id);
|
| var order = shopOrders.filter(function (o) { return o.id === id; })[0];
|
| var label = order ? order.code : ('#' + id);
|
| var btn = this;
|
| btn.disabled = true;
|
| var url = type === 'ship'
|
| ? '/api/shop/orders/' + id + '/ship'
|
| : '/api/shop/orders/' + id + '/admin-cancel';
|
| var init = { method: 'PATCH' };
|
| if (type === 'cancel') {
|
| var reason = document.getElementById('orderModalReason').value.trim();
|
| if (!reason) {
|
| document.getElementById('orderModalReasonErr').classList.add('show');
|
| document.getElementById('orderModalReason').focus();
|
| btn.disabled = false;
|
| return;
|
| }
|
| init.headers = { 'Content-Type': 'application/json' };
|
| init.body = JSON.stringify({ reason: reason });
|
| }
|
| api(url, init).then(function (d) {
|
| closeOrderModal();
|
| $('#ordersMsg').textContent = type === 'ship'
|
| ? 'Đã xác nhận ' + label + ' → Đang giao hàng.'
|
| : 'Đã hủy ' + label + '.';
|
| setTimeout(function () { $('#ordersMsg').textContent = ''; }, 3000);
|
| loadShopOrders();
|
| }).catch(function (e2) {
|
| $('#ordersMsg').textContent = 'Lỗi: ' + e2.message;
|
| }).finally(function () { btn.disabled = false; });
|
| });
|
|
|
| $('#ordersBody').addEventListener('click', function (e) {
|
| var ship = e.target.closest('[data-ship]');
|
| var cancel = e.target.closest('[data-cancel]');
|
| if (ship) openOrderModal('ship', Number(ship.dataset.ship));
|
| if (cancel) openOrderModal('cancel', Number(cancel.dataset.cancel));
|
| });
|
|
|
|
|
| function renderUpdates() {}
|
|
|
|
|
| function renderReports() {
|
| if (!cpStats) { $('#reportStats').innerHTML = '<div class="msg">Đang tải... (bấm "Thống kê" để tải lại)</div>'; return; }
|
| var avg = cpStats.orders > 0 ? Math.round(cpStats.revenue / cpStats.orders) : 0;
|
| $('#reportStats').innerHTML =
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#ff5e00;"></span> Doanh thu (tổng)</span><span class="stat-value">' + fmt(cpStats.revenue) + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#2abdbb;"></span> Đơn hàng</span><span class="stat-value">' + cpStats.orders + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#f5c518;"></span> Món đã bán</span><span class="stat-value">' + cpStats.items_sold + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#ff8a8a;"></span> Giá trị đơn TB</span><span class="stat-value">' + avg + '<small>đ</small></span></div>';
|
| api('/api/shop/reports').then(function (d) {
|
| drawRevenueChart(d.series || []);
|
| }).catch(function () {
|
| var cv = $('#revenueChart');
|
| if (cv) { cv.style.display = 'none'; }
|
| });
|
| }
|
|
|
| function drawRevenueChart(series) {
|
| var cv = $('#revenueChart');
|
| if (!cv || !series.length) return;
|
| cv.style.display = 'block';
|
| var ctx = cv.getContext('2d');
|
| var W = cv.width, H = cv.height;
|
| ctx.clearRect(0, 0, W, H);
|
| ctx.fillStyle = '#0d0f14';
|
| ctx.fillRect(0, 0, W, H);
|
|
|
| var max = 1;
|
| series.forEach(function (s) { if (s.revenue > max) max = s.revenue; });
|
| var padL = 64, padR = 12, padT = 18, padB = 34;
|
| var bw = (W - padL - padR) / series.length;
|
| var barW = Math.min(44, bw * 0.6);
|
| ctx.font = '11px Segoe UI, sans-serif';
|
| series.forEach(function (s, i) {
|
| var x = padL + i * bw + (bw - barW) / 2;
|
| var h = max > 0 ? (s.revenue / max) * (H - padT - padB) : 2;
|
| var y = H - padB - h;
|
| ctx.fillStyle = s.revenue > 0 ? 'rgba(255,94,0,.85)' : 'rgba(255,255,255,.08)';
|
| ctx.beginPath();
|
| if (ctx.roundRect) ctx.roundRect(x, y, barW, h, 5); else ctx.rect(x, y, barW, h);
|
| ctx.fill();
|
| if (s.revenue > 0) {
|
| ctx.fillStyle = '#fff';
|
| ctx.textAlign = 'center';
|
| ctx.fillText((s.revenue / 1000).toFixed(0) + 'K', x + barW / 2, y - 5);
|
| }
|
| ctx.fillStyle = '#8b90a5';
|
| ctx.fillText(s.day.slice(5), x + barW / 2, H - 12);
|
| });
|
| ctx.strokeStyle = 'rgba(255,255,255,.08)';
|
| ctx.beginPath();
|
| ctx.moveTo(padL - 6, padT);
|
| ctx.lineTo(padL - 6, H - padB);
|
| ctx.stroke();
|
| }
|
|
|
|
|
| function renderOverview() {
|
| $('#overviewStats').innerHTML = '<div class="msg">Đang tải thống kê...</div>';
|
| if (!AuthSystem.getToken()) { $('#overviewStats').innerHTML = '<div class="msg">Cần đăng nhập để xem thống kê.</div>'; return; }
|
| api('/api/shop/stats').then(function (d) {
|
| cpStats = d.stats;
|
| var s = d.stats;
|
| var lowCls = s.low_stock > 0 ? 'down' : 'up';
|
| var lowTxt = s.low_stock > 0 ? '▼ ' + s.low_stock + ' sắp hết' : '▲ đủ hàng';
|
| $('#overviewInfo').textContent = 'Cập nhật: ' + new Date().toLocaleTimeString('vi-VN');
|
| $('#overviewStats').innerHTML =
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#ff5e00;"></span> Doanh thu (tổng)</span><span class="stat-value">' + fmt(s.revenue) + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#2abdbb;"></span> Đơn hàng</span><span class="stat-value">' + s.orders + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#a3419f;"></span> Sản phẩm trong kho</span><span class="stat-value">' + s.products + '</span><span class="stat-delta ' + lowCls + '">' + lowTxt + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#f5c518;"></span> Khách hàng (user)</span><span class="stat-value">' + s.customers + '</span></div>' +
|
| '<div class="stat-card"><span class="stat-label"><span class="dot" style="background:#f9d423;"></span> Tổng điểm đã tích</span><span class="stat-value">' + Number(s.total_points || 0).toLocaleString('vi-VN') + '</span></div>';
|
| }).catch(function (e) {
|
| $('#overviewStats').innerHTML = '<div class="msg err">Lỗi tải thống kê: ' + esc(e.message) + '</div>';
|
| });
|
| } |
| |
| |
| function boot() { |
| var hasToken = !!AuthSystem.getToken(); |
| console.log('[control-panel] boot, hasToken=', hasToken); |
| AuthSystem.me().then(function (user) { |
| console.log('[control-panel] me() ->', user ? user.username + '/' + user.role : null); |
| if (!user) { |
| panel.style.display = 'none'; |
| denied.style.display = 'block'; |
| var msg = hasToken |
| ? 'Phiên đã hết hạn hoặc không thể kết nối máy chủ. <a href="login.html">Đăng nhập lại</a> hoặc <a href="#" id="retryLink">thử lại</a>.' |
| : 'Bạn chưa đăng nhập. <a href="login.html">Đăng nhập bằng tài khoản Admin</a>.'; |
| document.getElementById('deniedMsg').innerHTML = msg; |
| var retry = document.getElementById('retryLink'); |
| if (retry) retry.addEventListener('click', function (e) { e.preventDefault(); boot(); }); |
| return; |
| } |
| whoEl.textContent = user.username || user.email || ''; |
| roleEl.textContent = user.role === 'admin' ? 'Admin' : 'Thành viên'; |
| avatarEl.textContent = (user.username || 'A').charAt(0).toUpperCase(); |
| if (user.role !== 'admin') { |
| denied.style.display = 'block'; |
| return; |
| } |
| panel.style.display = 'block';
|
| renderOverview();
|
| loadUsers();
|
| loadContent();
|
| loadShopCustomers();
|
| loadShopOrders();
|
| });
|
| } |
| |
| boot(); |
| window.addEventListener('pageshow', function (e) { |
| if (e.persisted) boot(); |
| }); |
| |
| document.getElementById('logoutBtn').addEventListener('click', function () { |
| AuthSystem.logout().then(function () { window.location.href = 'login.html'; }); |
| }); |
| |
| |
| $$('.cp-nav-item[data-panel]').forEach(function (a) { |
| a.addEventListener('click', function () { |
| switchPanel(a.dataset.panel); |
| $('#cpSide').classList.remove('open'); |
| }); |
| }); |
| $('#cpBurger').addEventListener('click', function () { $('#cpSide').classList.toggle('open'); }); |
| |
|
|
| (function () {
|
| $('#cpCatChips').addEventListener('click', function (e) {
|
| var chip = e.target.closest('.chip');
|
| if (!chip) return;
|
| $('#cpCatChips .chip').forEach(function (c) { c.classList.remove('active'); });
|
| chip.classList.add('active');
|
| cpCurrentCat = chip.dataset.cat;
|
| renderCpProducts(cpCurrentCat, '');
|
| });
|
| })(); |
| </script> |
| </body> |
| </html>
|
|
|