941 lines
30 KiB
HTML
941 lines
30 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<html lang="en">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8" />
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
|
|
<title>Customer Management - Sky Art Shop</title>
|
||
|
|
<link
|
||
|
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||
|
|
rel="stylesheet"
|
||
|
|
/>
|
||
|
|
<link
|
||
|
|
rel="stylesheet"
|
||
|
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||
|
|
/>
|
||
|
|
<link rel="stylesheet" href="/admin/css/admin-style.css" />
|
||
|
|
<style>
|
||
|
|
.stats-row {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(4, 1fr);
|
||
|
|
gap: 1rem;
|
||
|
|
margin-bottom: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card {
|
||
|
|
background: white;
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1.25rem;
|
||
|
|
text-align: center;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
|
|
transition: transform 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card:hover {
|
||
|
|
transform: translateY(-3px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card i {
|
||
|
|
font-size: 1.75rem;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card .stat-number {
|
||
|
|
font-size: 1.75rem;
|
||
|
|
font-weight: 700;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card .stat-label {
|
||
|
|
color: #666;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card.total i {
|
||
|
|
color: #9c27b0;
|
||
|
|
}
|
||
|
|
.stat-card.verified i {
|
||
|
|
color: #2196f3;
|
||
|
|
}
|
||
|
|
.stat-card.newsletter i {
|
||
|
|
color: #e91e63;
|
||
|
|
}
|
||
|
|
.stat-card.active i {
|
||
|
|
color: #4caf50;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-bar {
|
||
|
|
background: white;
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1rem 1.25rem;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
|
|
display: flex;
|
||
|
|
gap: 1rem;
|
||
|
|
align-items: center;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-bar .search-box {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 200px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.content-card {
|
||
|
|
background: white;
|
||
|
|
border-radius: 12px;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.content-card table {
|
||
|
|
margin-bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.content-card th {
|
||
|
|
background: #f8f9fa;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #333;
|
||
|
|
border-bottom: 2px solid #e0e0e0;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.content-card td {
|
||
|
|
vertical-align: middle;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.customer-avatar {
|
||
|
|
width: 36px;
|
||
|
|
height: 36px;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: linear-gradient(135deg, #e91e63 0%, #9c27b0 100%);
|
||
|
|
color: white;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.badge-newsletter {
|
||
|
|
font-size: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pagination-bar {
|
||
|
|
padding: 1rem;
|
||
|
|
background: #f8f9fa;
|
||
|
|
border-top: 1px solid #e0e0e0;
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-state {
|
||
|
|
text-align: center;
|
||
|
|
padding: 3rem 2rem;
|
||
|
|
color: #666;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-state i {
|
||
|
|
font-size: 3rem;
|
||
|
|
color: #e0e0e0;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-export {
|
||
|
|
background: linear-gradient(135deg, #e91e63 0%, #9c27b0 100%);
|
||
|
|
border: none;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.btn-export:hover {
|
||
|
|
color: white;
|
||
|
|
opacity: 0.9;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 992px) {
|
||
|
|
.stats-row {
|
||
|
|
grid-template-columns: repeat(2, 1fr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 576px) {
|
||
|
|
.stats-row {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<!-- Sidebar -->
|
||
|
|
<div class="sidebar" id="sidebar">
|
||
|
|
<div class="sidebar-brand">Sky Art Shop</div>
|
||
|
|
<ul class="sidebar-menu">
|
||
|
|
<li>
|
||
|
|
<a href="/admin/dashboard"
|
||
|
|
><i class="bi bi-speedometer2"></i> Dashboard</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/homepage"
|
||
|
|
><i class="bi bi-house"></i> Homepage Editor</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/pages"
|
||
|
|
><i class="bi bi-file-text"></i> Custom Pages</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/media-library"
|
||
|
|
><i class="bi bi-images"></i> Media Library</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
|
||
|
|
</li>
|
||
|
|
<li>
|
||
|
|
<a href="/admin/customers" class="active"
|
||
|
|
><i class="bi bi-person-hearts"></i> Customers</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Main Content -->
|
||
|
|
<div class="main-content">
|
||
|
|
<!-- Top Bar -->
|
||
|
|
<div class="top-bar">
|
||
|
|
<div>
|
||
|
|
<h3>Customer Management</h3>
|
||
|
|
<p class="mb-0 text-muted">View and manage registered customers</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<button class="btn btn-export me-2" onclick="exportNewsletterList()">
|
||
|
|
<i class="bi bi-download"></i> Export Newsletter
|
||
|
|
</button>
|
||
|
|
<button class="btn-logout" id="btnLogout">
|
||
|
|
<i class="bi bi-box-arrow-right"></i> Logout
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Stats Cards -->
|
||
|
|
<div class="stats-row">
|
||
|
|
<div class="stat-card total">
|
||
|
|
<i class="bi bi-people"></i>
|
||
|
|
<div class="stat-number" id="statTotal">-</div>
|
||
|
|
<div class="stat-label">Total Customers</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card verified">
|
||
|
|
<i class="bi bi-patch-check"></i>
|
||
|
|
<div class="stat-number" id="statVerified">-</div>
|
||
|
|
<div class="stat-label">Verified</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card newsletter">
|
||
|
|
<i class="bi bi-envelope-heart"></i>
|
||
|
|
<div class="stat-number" id="statNewsletter">-</div>
|
||
|
|
<div class="stat-label">Newsletter</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat-card active">
|
||
|
|
<i class="bi bi-person-check"></i>
|
||
|
|
<div class="stat-number" id="statActive">-</div>
|
||
|
|
<div class="stat-label">Active</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Filter Bar -->
|
||
|
|
<div class="filter-bar">
|
||
|
|
<div class="search-box">
|
||
|
|
<div class="input-group">
|
||
|
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
class="form-control"
|
||
|
|
id="searchInput"
|
||
|
|
placeholder="Search by name or email..."
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<select class="form-select" id="statusFilter" style="width: auto">
|
||
|
|
<option value="all">All Status</option>
|
||
|
|
<option value="verified">Verified</option>
|
||
|
|
<option value="unverified">Unverified</option>
|
||
|
|
</select>
|
||
|
|
<select class="form-select" id="newsletterFilter" style="width: auto">
|
||
|
|
<option value="all">All Customers</option>
|
||
|
|
<option value="subscribed">Newsletter Subscribed</option>
|
||
|
|
<option value="unsubscribed">Not Subscribed</option>
|
||
|
|
</select>
|
||
|
|
<button class="btn btn-outline-secondary" onclick="loadCustomers()">
|
||
|
|
<i class="bi bi-funnel"></i> Filter
|
||
|
|
</button>
|
||
|
|
<span class="text-muted ms-auto" id="resultCount">Loading...</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Customers Table -->
|
||
|
|
<div class="content-card">
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table table-hover mb-0">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th style="width: 50px"></th>
|
||
|
|
<th>Name</th>
|
||
|
|
<th>Email</th>
|
||
|
|
<th class="text-center">Verified</th>
|
||
|
|
<th class="text-center">Newsletter</th>
|
||
|
|
<th class="text-center">Cart</th>
|
||
|
|
<th class="text-center">Wishlist</th>
|
||
|
|
<th class="text-center">Logins</th>
|
||
|
|
<th>Joined</th>
|
||
|
|
<th class="text-center">Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="customersTableBody">
|
||
|
|
<tr>
|
||
|
|
<td colspan="10" class="text-center py-5">
|
||
|
|
<div class="spinner-border text-primary" role="status">
|
||
|
|
<span class="visually-hidden">Loading...</span>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Pagination -->
|
||
|
|
<div class="pagination-bar">
|
||
|
|
<span class="text-muted" id="paginationInfo">Showing 0 of 0</span>
|
||
|
|
<nav>
|
||
|
|
<ul
|
||
|
|
class="pagination pagination-sm mb-0"
|
||
|
|
id="paginationControls"
|
||
|
|
></ul>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Customer Detail Modal -->
|
||
|
|
<div class="modal fade" id="customerModal" tabindex="-1">
|
||
|
|
<div class="modal-dialog modal-lg">
|
||
|
|
<div class="modal-content">
|
||
|
|
<div class="modal-header">
|
||
|
|
<h5 class="modal-title">Customer Details</h5>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
class="btn-close"
|
||
|
|
data-bs-dismiss="modal"
|
||
|
|
></button>
|
||
|
|
</div>
|
||
|
|
<div class="modal-body">
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-md-4 text-center">
|
||
|
|
<div
|
||
|
|
class="customer-avatar mx-auto mb-2"
|
||
|
|
style="width: 80px; height: 80px; font-size: 2rem"
|
||
|
|
id="modalAvatar"
|
||
|
|
>
|
||
|
|
?
|
||
|
|
</div>
|
||
|
|
<h5 id="modalName">-</h5>
|
||
|
|
<p class="text-muted" id="modalEmail">-</p>
|
||
|
|
<div class="d-flex justify-content-center gap-3 mb-3">
|
||
|
|
<div>
|
||
|
|
<div class="fw-bold" id="modalLogins">0</div>
|
||
|
|
<small class="text-muted">Logins</small>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div id="modalNewsletter">-</div>
|
||
|
|
<small class="text-muted">Newsletter</small>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div id="modalStatus">-</div>
|
||
|
|
<small class="text-muted">Status</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<p class="small text-muted mb-1">
|
||
|
|
Member Since: <span id="modalJoined">-</span>
|
||
|
|
</p>
|
||
|
|
<p class="small text-muted">
|
||
|
|
Last Login: <span id="modalLastLogin">-</span>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-8">
|
||
|
|
<ul class="nav nav-tabs mb-3" id="customerTabs">
|
||
|
|
<li class="nav-item">
|
||
|
|
<a
|
||
|
|
class="nav-link active"
|
||
|
|
data-bs-toggle="tab"
|
||
|
|
href="#tabCart"
|
||
|
|
>Cart (<span id="cartCount">0</span>)</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link" data-bs-toggle="tab" href="#tabWishlist"
|
||
|
|
>Wishlist (<span id="wishlistCount">0</span>)</a
|
||
|
|
>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
<div class="tab-content">
|
||
|
|
<div class="tab-pane fade show active" id="tabCart">
|
||
|
|
<div
|
||
|
|
id="cartItems"
|
||
|
|
class="list-group list-group-flush"
|
||
|
|
style="max-height: 300px; overflow-y: auto"
|
||
|
|
>
|
||
|
|
<p class="text-muted text-center py-3">
|
||
|
|
No items in cart
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="tab-pane fade" id="tabWishlist">
|
||
|
|
<div
|
||
|
|
id="wishlistItems"
|
||
|
|
class="list-group list-group-flush"
|
||
|
|
style="max-height: 300px; overflow-y: auto"
|
||
|
|
>
|
||
|
|
<p class="text-muted text-center py-3">
|
||
|
|
No items in wishlist
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="modal-footer">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
class="btn btn-secondary"
|
||
|
|
data-bs-dismiss="modal"
|
||
|
|
>
|
||
|
|
Close
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
class="btn btn-warning"
|
||
|
|
id="modalToggleBtn"
|
||
|
|
onclick="toggleCustomerStatus()"
|
||
|
|
>
|
||
|
|
Deactivate
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
|
<script>
|
||
|
|
// State
|
||
|
|
let currentPage = 1;
|
||
|
|
let totalPages = 1;
|
||
|
|
let currentCustomerId = null;
|
||
|
|
let currentCustomerActive = true;
|
||
|
|
|
||
|
|
// Initialize
|
||
|
|
document.addEventListener("DOMContentLoaded", function () {
|
||
|
|
loadStats();
|
||
|
|
loadCustomers();
|
||
|
|
|
||
|
|
// Search on Enter
|
||
|
|
document
|
||
|
|
.getElementById("searchInput")
|
||
|
|
.addEventListener("keyup", (e) => {
|
||
|
|
if (e.key === "Enter") loadCustomers();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Filter change
|
||
|
|
document
|
||
|
|
.getElementById("newsletterFilter")
|
||
|
|
.addEventListener("change", loadCustomers);
|
||
|
|
document
|
||
|
|
.getElementById("statusFilter")
|
||
|
|
.addEventListener("change", loadCustomers);
|
||
|
|
|
||
|
|
// Logout button
|
||
|
|
document.getElementById("btnLogout").addEventListener("click", logout);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Logout function
|
||
|
|
async function logout() {
|
||
|
|
try {
|
||
|
|
await fetch("/api/admin/logout", { method: "POST" });
|
||
|
|
window.location.href = "/admin/login";
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Logout failed:", error);
|
||
|
|
window.location.href = "/admin/login";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load statistics
|
||
|
|
async function loadStats() {
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/admin/customers/stats/overview");
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
document.getElementById("statTotal").textContent = data.stats.total;
|
||
|
|
document.getElementById("statVerified").textContent =
|
||
|
|
data.stats.verified;
|
||
|
|
document.getElementById("statNewsletter").textContent =
|
||
|
|
data.stats.newsletterSubscribed;
|
||
|
|
document.getElementById("statActive").textContent =
|
||
|
|
data.stats.active;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load stats:", error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load customers
|
||
|
|
async function loadCustomers() {
|
||
|
|
const search = document.getElementById("searchInput").value;
|
||
|
|
const newsletter = document.getElementById("newsletterFilter").value;
|
||
|
|
const status = document.getElementById("statusFilter").value;
|
||
|
|
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
page: currentPage,
|
||
|
|
limit: 20,
|
||
|
|
newsletter,
|
||
|
|
status,
|
||
|
|
search,
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/admin/customers?${params}`);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
renderCustomers(data.customers);
|
||
|
|
updatePagination(data.pagination);
|
||
|
|
document.getElementById("resultCount").textContent =
|
||
|
|
`${data.pagination.total} customers`;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load customers:", error);
|
||
|
|
document.getElementById("customersTableBody").innerHTML = `
|
||
|
|
<tr>
|
||
|
|
<td colspan="10">
|
||
|
|
<div class="empty-state">
|
||
|
|
<i class="bi bi-exclamation-triangle"></i>
|
||
|
|
<h5>Failed to load customers</h5>
|
||
|
|
<p>Please try refreshing the page.</p>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render customers table
|
||
|
|
function renderCustomers(customers) {
|
||
|
|
const tbody = document.getElementById("customersTableBody");
|
||
|
|
|
||
|
|
if (customers.length === 0) {
|
||
|
|
tbody.innerHTML = `
|
||
|
|
<tr>
|
||
|
|
<td colspan="10">
|
||
|
|
<div class="empty-state">
|
||
|
|
<i class="bi bi-people"></i>
|
||
|
|
<h5>No customers found</h5>
|
||
|
|
<p>No customers match your search criteria.</p>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
tbody.innerHTML = customers
|
||
|
|
.map(
|
||
|
|
(customer) => `
|
||
|
|
<tr>
|
||
|
|
<td>
|
||
|
|
<div class="customer-avatar">
|
||
|
|
${customer.first_name.charAt(0).toUpperCase()}
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<strong>${escapeHtml(customer.first_name)} ${escapeHtml(
|
||
|
|
customer.last_name,
|
||
|
|
)}</strong>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<a href="mailto:${escapeHtml(customer.email)}">${escapeHtml(
|
||
|
|
customer.email,
|
||
|
|
)}</a>
|
||
|
|
</td>
|
||
|
|
<td class="text-center">
|
||
|
|
${
|
||
|
|
customer.email_verified
|
||
|
|
? '<span class="badge bg-success"><i class="bi bi-check-circle"></i></span>'
|
||
|
|
: '<span class="badge bg-warning text-dark"><i class="bi bi-clock"></i></span>'
|
||
|
|
}
|
||
|
|
</td>
|
||
|
|
<td class="text-center">
|
||
|
|
${
|
||
|
|
customer.newsletter_subscribed
|
||
|
|
? '<span class="badge bg-success badge-newsletter"><i class="bi bi-check"></i></span>'
|
||
|
|
: '<span class="badge bg-secondary badge-newsletter"><i class="bi bi-x"></i></span>'
|
||
|
|
}
|
||
|
|
</td>
|
||
|
|
<td class="text-center"><span class="badge bg-info">${
|
||
|
|
customer.cart_count || 0
|
||
|
|
}</span></td>
|
||
|
|
<td class="text-center"><span class="badge bg-pink">${
|
||
|
|
customer.wishlist_count || 0
|
||
|
|
}</span></td>
|
||
|
|
<td class="text-center">${customer.login_count || 0}</td>
|
||
|
|
<td>${formatDate(customer.created_at)}</td>
|
||
|
|
<td class="text-center">
|
||
|
|
<button class="btn btn-sm btn-outline-primary" onclick="viewCustomer('${
|
||
|
|
customer.id
|
||
|
|
}')">
|
||
|
|
<i class="bi bi-eye"></i>
|
||
|
|
</button>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
`,
|
||
|
|
)
|
||
|
|
.join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update pagination
|
||
|
|
function updatePagination(pagination) {
|
||
|
|
totalPages = pagination.totalPages || 1;
|
||
|
|
const start =
|
||
|
|
pagination.total > 0
|
||
|
|
? (pagination.page - 1) * pagination.limit + 1
|
||
|
|
: 0;
|
||
|
|
const end = Math.min(
|
||
|
|
pagination.page * pagination.limit,
|
||
|
|
pagination.total,
|
||
|
|
);
|
||
|
|
|
||
|
|
document.getElementById("paginationInfo").textContent =
|
||
|
|
`Showing ${start}-${end} of ${pagination.total}`;
|
||
|
|
|
||
|
|
const controls = document.getElementById("paginationControls");
|
||
|
|
let html = "";
|
||
|
|
|
||
|
|
if (totalPages > 1) {
|
||
|
|
html += `
|
||
|
|
<li class="page-item ${pagination.page === 1 ? "disabled" : ""}">
|
||
|
|
<a class="page-link" href="#" onclick="goToPage(${
|
||
|
|
pagination.page - 1
|
||
|
|
}); return false;">
|
||
|
|
<i class="bi bi-chevron-left"></i>
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
`;
|
||
|
|
|
||
|
|
for (let i = 1; i <= totalPages; i++) {
|
||
|
|
if (
|
||
|
|
i === 1 ||
|
||
|
|
i === totalPages ||
|
||
|
|
(i >= pagination.page - 1 && i <= pagination.page + 1)
|
||
|
|
) {
|
||
|
|
html += `
|
||
|
|
<li class="page-item ${i === pagination.page ? "active" : ""}">
|
||
|
|
<a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a>
|
||
|
|
</li>
|
||
|
|
`;
|
||
|
|
} else if (i === pagination.page - 2 || i === pagination.page + 2) {
|
||
|
|
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
html += `
|
||
|
|
<li class="page-item ${
|
||
|
|
pagination.page === totalPages ? "disabled" : ""
|
||
|
|
}">
|
||
|
|
<a class="page-link" href="#" onclick="goToPage(${
|
||
|
|
pagination.page + 1
|
||
|
|
}); return false;">
|
||
|
|
<i class="bi bi-chevron-right"></i>
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
controls.innerHTML = html;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Go to page
|
||
|
|
function goToPage(page) {
|
||
|
|
if (page < 1 || page > totalPages) return;
|
||
|
|
currentPage = page;
|
||
|
|
loadCustomers();
|
||
|
|
}
|
||
|
|
|
||
|
|
// View customer details
|
||
|
|
async function viewCustomer(id) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/admin/customers/${id}`);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
const customer = data.customer;
|
||
|
|
currentCustomerId = customer.id;
|
||
|
|
currentCustomerActive = customer.is_active;
|
||
|
|
|
||
|
|
document.getElementById("modalAvatar").textContent =
|
||
|
|
customer.first_name.charAt(0).toUpperCase();
|
||
|
|
document.getElementById("modalName").textContent =
|
||
|
|
`${customer.first_name} ${customer.last_name}`;
|
||
|
|
document.getElementById("modalEmail").textContent = customer.email;
|
||
|
|
document.getElementById("modalLogins").textContent =
|
||
|
|
customer.login_count || 0;
|
||
|
|
document.getElementById("modalNewsletter").innerHTML =
|
||
|
|
customer.newsletter_subscribed
|
||
|
|
? '<span class="text-success"><i class="bi bi-check-circle"></i></span>'
|
||
|
|
: '<span class="text-muted"><i class="bi bi-x-circle"></i></span>';
|
||
|
|
document.getElementById("modalStatus").innerHTML =
|
||
|
|
customer.is_active
|
||
|
|
? '<span class="text-success">Active</span>'
|
||
|
|
: '<span class="text-danger">Inactive</span>';
|
||
|
|
document.getElementById("modalJoined").textContent = formatDate(
|
||
|
|
customer.created_at,
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
document.getElementById("modalLastLogin").textContent =
|
||
|
|
customer.last_login
|
||
|
|
? formatDate(customer.last_login, true)
|
||
|
|
: "Never";
|
||
|
|
|
||
|
|
document.getElementById("modalToggleBtn").textContent =
|
||
|
|
customer.is_active ? "Deactivate" : "Activate";
|
||
|
|
document.getElementById("modalToggleBtn").className =
|
||
|
|
customer.is_active ? "btn btn-warning" : "btn btn-success";
|
||
|
|
|
||
|
|
// Load cart and wishlist
|
||
|
|
loadCustomerCart(id);
|
||
|
|
loadCustomerWishlist(id);
|
||
|
|
|
||
|
|
new bootstrap.Modal(
|
||
|
|
document.getElementById("customerModal"),
|
||
|
|
).show();
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load customer:", error);
|
||
|
|
alert("Failed to load customer details");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load customer cart
|
||
|
|
async function loadCustomerCart(customerId) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(
|
||
|
|
`/api/admin/customers/${customerId}/cart`,
|
||
|
|
);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
const container = document.getElementById("cartItems");
|
||
|
|
const countEl = document.getElementById("cartCount");
|
||
|
|
|
||
|
|
if (data.success && data.items && data.items.length > 0) {
|
||
|
|
countEl.textContent = data.items.length;
|
||
|
|
container.innerHTML = data.items
|
||
|
|
.map(
|
||
|
|
(item) => `
|
||
|
|
<div class="list-group-item d-flex align-items-center">
|
||
|
|
<img src="${
|
||
|
|
item.image || "/assets/images/products/placeholder.jpg"
|
||
|
|
}"
|
||
|
|
alt="${escapeHtml(item.name)}"
|
||
|
|
style="width: 50px; height: 50px; object-fit: cover; border-radius: 8px;"
|
||
|
|
class="me-3">
|
||
|
|
<div class="flex-grow-1">
|
||
|
|
<div class="fw-semibold">${escapeHtml(item.name)}</div>
|
||
|
|
<small class="text-muted">Qty: ${item.quantity}</small>
|
||
|
|
</div>
|
||
|
|
<div class="text-end">
|
||
|
|
<div class="fw-bold">$${(item.price * item.quantity).toFixed(
|
||
|
|
2,
|
||
|
|
)}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`,
|
||
|
|
)
|
||
|
|
.join("");
|
||
|
|
} else {
|
||
|
|
countEl.textContent = "0";
|
||
|
|
container.innerHTML =
|
||
|
|
'<p class="text-muted text-center py-3">No items in cart</p>';
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load cart:", error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load customer wishlist
|
||
|
|
async function loadCustomerWishlist(customerId) {
|
||
|
|
try {
|
||
|
|
const response = await fetch(
|
||
|
|
`/api/admin/customers/${customerId}/wishlist`,
|
||
|
|
);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
const container = document.getElementById("wishlistItems");
|
||
|
|
const countEl = document.getElementById("wishlistCount");
|
||
|
|
|
||
|
|
if (data.success && data.items && data.items.length > 0) {
|
||
|
|
countEl.textContent = data.items.length;
|
||
|
|
container.innerHTML = data.items
|
||
|
|
.map(
|
||
|
|
(item) => `
|
||
|
|
<div class="list-group-item d-flex align-items-center">
|
||
|
|
<img src="${
|
||
|
|
item.image || "/assets/images/products/placeholder.jpg"
|
||
|
|
}"
|
||
|
|
alt="${escapeHtml(item.name)}"
|
||
|
|
style="width: 50px; height: 50px; object-fit: cover; border-radius: 8px;"
|
||
|
|
class="me-3">
|
||
|
|
<div class="flex-grow-1">
|
||
|
|
<div class="fw-semibold">${escapeHtml(item.name)}</div>
|
||
|
|
<small class="text-muted">Added: ${formatDate(
|
||
|
|
item.added_at,
|
||
|
|
)}</small>
|
||
|
|
</div>
|
||
|
|
<div class="text-end">
|
||
|
|
<div class="fw-bold">$${parseFloat(item.price).toFixed(
|
||
|
|
2,
|
||
|
|
)}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`,
|
||
|
|
)
|
||
|
|
.join("");
|
||
|
|
} else {
|
||
|
|
countEl.textContent = "0";
|
||
|
|
container.innerHTML =
|
||
|
|
'<p class="text-muted text-center py-3">No items in wishlist</p>';
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to load wishlist:", error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Toggle customer status
|
||
|
|
async function toggleCustomerStatus() {
|
||
|
|
if (!currentCustomerId) return;
|
||
|
|
|
||
|
|
const newStatus = !currentCustomerActive;
|
||
|
|
const action = newStatus ? "activate" : "deactivate";
|
||
|
|
|
||
|
|
if (!confirm(`Are you sure you want to ${action} this customer?`))
|
||
|
|
return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(
|
||
|
|
`/api/admin/customers/${currentCustomerId}/status`,
|
||
|
|
{
|
||
|
|
method: "PATCH",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({ is_active: newStatus }),
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
bootstrap.Modal.getInstance(
|
||
|
|
document.getElementById("customerModal"),
|
||
|
|
).hide();
|
||
|
|
loadCustomers();
|
||
|
|
loadStats();
|
||
|
|
} else {
|
||
|
|
alert(data.message || "Failed to update status");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to update status:", error);
|
||
|
|
alert("Failed to update customer status");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Export newsletter list
|
||
|
|
async function exportNewsletterList() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(
|
||
|
|
"/api/admin/customers/export/newsletter",
|
||
|
|
);
|
||
|
|
const data = await response.json();
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
const headers = ["First Name", "Last Name", "Email"];
|
||
|
|
const rows = data.customers.map((c) => [
|
||
|
|
c.first_name,
|
||
|
|
c.last_name,
|
||
|
|
c.email,
|
||
|
|
]);
|
||
|
|
|
||
|
|
let csv = headers.join(",") + "\n";
|
||
|
|
csv += rows.map((r) => r.map((v) => `"${v}"`).join(",")).join("\n");
|
||
|
|
|
||
|
|
const blob = new Blob([csv], { type: "text/csv" });
|
||
|
|
const url = window.URL.createObjectURL(blob);
|
||
|
|
const a = document.createElement("a");
|
||
|
|
a.href = url;
|
||
|
|
a.download = `newsletter-subscribers-${
|
||
|
|
new Date().toISOString().split("T")[0]
|
||
|
|
}.csv`;
|
||
|
|
a.click();
|
||
|
|
window.URL.revokeObjectURL(url);
|
||
|
|
|
||
|
|
alert(`Exported ${data.count} newsletter subscribers`);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to export:", error);
|
||
|
|
alert("Failed to export newsletter list");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Format date
|
||
|
|
function formatDate(dateStr, full = false) {
|
||
|
|
if (!dateStr) return "-";
|
||
|
|
const date = new Date(dateStr);
|
||
|
|
if (full) {
|
||
|
|
return date.toLocaleString("en-US", {
|
||
|
|
year: "numeric",
|
||
|
|
month: "short",
|
||
|
|
day: "numeric",
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return date.toLocaleDateString("en-US", {
|
||
|
|
year: "numeric",
|
||
|
|
month: "short",
|
||
|
|
day: "numeric",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Escape HTML
|
||
|
|
function escapeHtml(str) {
|
||
|
|
if (!str) return "";
|
||
|
|
return str
|
||
|
|
.replace(/&/g, "&")
|
||
|
|
.replace(/</g, "<")
|
||
|
|
.replace(/>/g, ">")
|
||
|
|
.replace(/"/g, """);
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|