TestX / control-panel.html
school44s's picture
Upload 13 files
cee43ad verified
Raw
History Blame Contribute Delete
68.2 kB
<!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; }
/* ---------- Sidebar ---------- */
.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); }
/* ---------- Main ---------- */
.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; }
/* ---------- Cards / stats / tables ---------- */
.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 modal ---------- */
.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); }
/* ---------- Mobile ---------- */
@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>
<!-- Sidebar -->
<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">&#128200;</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">&#128101;</span> Người dùng</a>
<a class="cp-nav-item" data-panel="content"><span class="ico">&#128230;</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">&#127891;</span> Sản phẩm</a>
<a class="cp-nav-item" data-panel="orders"><span class="ico">&#128722;</span> Đơn hàng</a>
<a class="cp-nav-item" data-panel="customers"><span class="ico">&#128101;</span> Khách hàng</a>
<a class="cp-nav-item" data-panel="reports"><span class="ico">&#128200;</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">&#128260;</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">&#127968;</span> Trang chủ</a>
<a class="cp-nav-item" href="sfw/index.html"><span class="ico">&#127916;</span> Xem Anime</a>
<a class="cp-nav-item" href="shop/"><span class="ico">&#128722;</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 -->
<main class="cp-main">
<div class="cp-topbar">
<div style="display:flex;align-items:center;gap:12px;">
<button class="cp-burger" id="cpBurger">&#9776;</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/">&#128722; Cửa hàng</a>
<a class="cp-btn" href="index.html">&#127968; Trang chủ</a>
<a class="cp-btn" href="sfw/index.html">&#127916; 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">
<!-- Panel: Thống kê -->
<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">&#128260; 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>
<!-- Panel: Người dùng -->
<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>
<!-- Panel: Nội dung -->
<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>
<!-- Panel: Sản phẩm -->
<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">&#128260; 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">&#128517;</div>
Không có sản phẩm phù hợp.
</div>
</div>
</div>
<!-- Panel: Đơn hàng -->
<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">&#128260; 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>
<!-- Panel: Khách hàng -->
<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">&#128260; 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>
<!-- Panel: Báo cáo -->
<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">&#128260; 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>
<!-- Panel: Cập nhật -->
<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 &#8212; Lu&#7891;ng thanh to&#225;n &amp; h&#7911;y &#273;&#417;n</h3>
<p class="update-date">17/08/2026</p>
<ul>
<li>&#272;&#417;n h&#224;ng c&#243; th&#234;m tr&#7841;ng th&#225;i <code>Ch&#7901; thanh to&#225;n</code> &#8212; kh&#225;ch ph&#7843;i b&#7845;m &#8220;T&#244;i &#273;&#227; chuy&#7875;n kho&#7843;n&#8221; &#273;&#7875; &#273;&#417;n v&#224;o <code>Ch&#7901; x&#225;c nh&#7853;n</code>; n&#7871;u kh&#244;ng thanh to&#225;n trong 5 ph&#250;t, &#273;&#417;n t&#7921; &#273;&#7897;ng h&#7911;y.</li>
<li>Trang thanh to&#225;n m&#7899;i: QR + th&#244;ng tin &#273;&#417;n (m&#227; &#273;&#417;n, ng&#432;&#7901;i mua, s&#7843;n ph&#7849;m, t&#7893;ng ti&#7873;n), n&#250;t &#8220;T&#244;i &#273;&#227; chuy&#7875;n kho&#7843;n&#8221; ch&#7901; 5 gi&#226;y r&#7891;i quay v&#7873;.</li>
<li>Kh&#225;ch c&#243; th&#7875; h&#7911;y &#273;&#417;n (<code>Ch&#7901; thanh to&#225;n</code> / <code>Ch&#7901; x&#225;c nh&#7853;n</code>) k&#232;m l&#237; do b&#7857;ng popup; admin h&#7911;y b&#7845;t k&#7923; &#273;&#417;n n&#224;o k&#232;m l&#237; do.</li>
<li>&#272;&#417;n <code>&#272;&#227; h&#7911;y</code> hi&#7875;n l&#237; do h&#7911;y; stock &#273;&#432;&#7907;c ho&#224;n l&#7841;i.</li>
<li>Pop-up thay cho alert tr&#236;nh duy&#7879;t khi x&#225;c nh&#7853;n / h&#7911;y &#273;&#417;n; &#273;&#417;n h&#224;ng trong shop ch&#7881; hi&#7875;n &#273;&#417;n c&#7911;a ch&#237;nh m&#236;nh.</li>
<li>S&#7917;a l&#7895;i hi&#7875;n th&#7883; &#8220;s&#7843;n ph&#7849;m&#8221; trong gi&#7887; h&#224;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 &#8212; Thanh to&#225;n &amp; x&#225;c nh&#7853;n &#273;&#417;n h&#224;ng</h3>
<p class="update-date">17/08/2026</p>
<ul>
<li>Sau khi &#273;&#7863;t h&#224;ng, kh&#225;ch chuy&#7875;n qua trang thanh to&#225;n (QR &#7911;ng h&#7897; + m&#227; &#273;&#417;n + &#273;&#7871;m ng&#432;&#7907;c th&#7901;i gian thanh to&#225;n).</li>
<li>Admin b&#7845;m &#8220;X&#225;c nh&#7853;n&#8221; tr&#234;n &#273;&#417;n &#7903; tr&#7841;ng th&#225;i <code>Ch&#7901; x&#225;c nh&#7853;n</code> &#8594; &#273;&#417;n chuy&#7875;n sang <code>&#272;ang giao h&#224;ng</code>, t&#7921; &#273;&#7897;ng th&#224;nh <code>&#272;&#227; giao</code> sau 5 ph&#250;t.</li>
<li>Trang shop t&#7921; c&#7853;p nh&#7853;t tr&#7841;ng th&#225;i &#273;&#417;n h&#224;ng theo th&#7901;i gian th&#7921;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 &#8212; Shop d&#7915; li&#7879;u th&#7921;c</h3>
<p class="update-date">17/08/2026</p>
<ul>
<li>S&#7843;n ph&#7849;m v&#224; &#273;&#417;n h&#224;ng l&#432;u tr&#432; tr&#234;n DB (worker <code>shop-system</code> + D1): danh m&#7909;c s&#7843;n ph&#7849;m c&#243; t&#7891;n kho (stock), mua h&#224;ng tr&#7915; stock.</li>
<li>Kh&#225;ch h&#224;ng = t&#224;i kho&#7843;n: m&#7895;i user l&#224; 1 kh&#225;ch h&#224;ng, th&#7889;ng k&#234; &#273;&#417;n h&#224;ng / m&#243;n / t&#7893;ng chi ti&#234;u t&#237;nh t&#7915; DB.</li>
<li>T&#7893;ng quan &amp; B&#225;o c&#225;o: doanh thu, &#273;&#417;n h&#224;ng, m&#243;n &#273;&#227; b&#225;n l&#7845;y s&#7889; li&#7879;u th&#7921;c t&#7915; DB (kh&#244;ng c&#242;n s&#7889; d&#7921; ki&#7871;n).</li>
<li>Control Panel: panel &#272;&#417;n h&#224;ng m&#7899;i + qu&#7843;n l&#253; t&#7891;n kho s&#7843;n ph&#7849;m (t&#259;ng/gi&#7843;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 &#8212; Shop &amp; 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 &amp; 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 &#8212; 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 &quot;Xem feature&quot; thành &quot;Xem phim nổi bật&quot;.</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 &#8212; 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 &amp; 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">&#9888;</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>
<!-- Popup xác nhận / hủy đơn -->
<div class="confirm-overlay" id="orderModal">
<div class="confirm-box">
<div class="confirm-icon">&#128722;</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") + "&#273;"; }
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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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();
});
}
// ---------- Shop: Sản phẩm ----------
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">&#128517;</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));
});
// ---------- Shop: Khách hàng ----------
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>&#11088; ' + 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 = '&#8987; Đang đồng bộ…';
loadShopCustomers();
setTimeout(function () { btn.innerHTML = '&#128260; Làm mới'; }, 1200);
});
// ---------- Shop: Đơn hàng ----------
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">&#128666; 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)">&#10060; 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)">&#10060; 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;">&#128666; Đang giao&hellip;</span>' +
'<button class="cp-btn cp-btn-sm cp-btn-danger" data-cancel="' + o.id + '" title="Hủy đơn (kèm lí do)">&#10060; Hủy</button>' +
'</div>';
} else if (o.status === 'Đã giao') {
action = '<span style="font-size:11px;color:#2abdbb;">&#9989; Đã giao</span>';
} else if (o.status === 'Đã hủy') {
action = '<span style="font-size:11px;color:#ff8a8a;">&#10060; Đã 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>';
});
});
// ---------- Popup xác nhận / hủy đơn ----------
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' ? '&#128666; Xác nhận' : '&#10060; 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));
});
// ---------- Shop: Cập nhật ----------
function renderUpdates() {}
// ---------- Shop: Báo cáo ----------
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();
}
// ---------- Overview ----------
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>';
});
}
// ---------- Boot ----------
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'; });
});
// Panel switching
$$('.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'); });
// Products filter (chips are built in loadShopProducts; wire up clicks here)
(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>