webupdate

This commit is contained in:
Local Server
2026-01-18 02:22:05 -06:00
parent 6fc159051a
commit 2a2a3d99e5
135 changed files with 54897 additions and 9825 deletions

View File

@@ -0,0 +1,940 @@
<!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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
</script>
</body>
</html>