mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-06-30 17:50:32 -05:00
Initial commit
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$pageTitle = 'About Us Content';
|
||||
$currentPage = 'about-us';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
/* ── Actions ─────────────────────────────────────── */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$sectionId = $_POST['section_id'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$body = trim($_POST['body'] ?? '');
|
||||
if ($body) {
|
||||
db()->insert('about_us_sections', [
|
||||
'section_id' => generateId('sec_'),
|
||||
'heading' => trim($_POST['heading'] ?? '') ?: null,
|
||||
'body' => $body,
|
||||
'sort_order' => intval($_POST['sort_order'] ?? 0),
|
||||
'is_active' => 1,
|
||||
]);
|
||||
setFlash('success', 'Section added');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'update' && $sectionId) {
|
||||
$body = trim($_POST['body'] ?? '');
|
||||
if ($body) {
|
||||
db()->update('about_us_sections', [
|
||||
'heading' => trim($_POST['heading'] ?? '') ?: null,
|
||||
'body' => $body,
|
||||
'sort_order' => intval($_POST['sort_order'] ?? 0),
|
||||
'is_active' => isset($_POST['is_active']) ? 1 : 0,
|
||||
], 'section_id = :id', ['id' => $sectionId]);
|
||||
setFlash('success', 'Section updated');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $sectionId) {
|
||||
db()->delete('about_us_sections', 'section_id = :id', ['id' => $sectionId]);
|
||||
setFlash('success', 'Section deleted');
|
||||
}
|
||||
|
||||
if ($action === 'reorder') {
|
||||
$ids = json_decode($_POST['order'] ?? '[]', true);
|
||||
foreach ($ids as $pos => $sid) {
|
||||
db()->update('about_us_sections', ['sort_order' => $pos + 1],
|
||||
'section_id = :id', ['id' => $sid]);
|
||||
}
|
||||
echo json_encode(['ok' => true]); exit;
|
||||
}
|
||||
|
||||
header('Location: /admin/about-us.php'); exit;
|
||||
}
|
||||
|
||||
$sections = db()->fetchAll(
|
||||
"SELECT * FROM about_us_sections ORDER BY sort_order ASC, id ASC"
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">About Us Content</h1>
|
||||
<button class="btn btn-primary" onclick="openModal()">
|
||||
<i class="fas fa-plus"></i> Add Section
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-card" style="margin-bottom:1.5rem">
|
||||
<div class="admin-card-body" style="padding:.85rem 1.5rem">
|
||||
<p class="text-muted" style="margin:0">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
These sections appear on the homepage between <strong>Our Story</strong> and the <strong>Explore Our Coffee</strong> button.
|
||||
Drag rows to reorder. Blank lines in the text become paragraph breaks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Preview -->
|
||||
<div class="admin-card" style="margin-bottom:1.5rem">
|
||||
<div class="admin-card-header">
|
||||
<h3><i class="fas fa-eye"></i> Homepage Preview</h3>
|
||||
<a href="/" target="_blank" class="btn btn-sm btn-secondary"><i class="fas fa-external-link-alt"></i> View Live</a>
|
||||
</div>
|
||||
<div class="admin-card-body" style="background:var(--admin-bg);border-radius:var(--radius-md);padding:2rem">
|
||||
<h2 style="font-family:Georgia,serif;font-size:1.75rem;margin:0 0 1rem">Our Story</h2>
|
||||
<div id="livePreview" style="font-size:.9375rem;color:var(--admin-text-muted);line-height:1.7">
|
||||
<?php foreach ($sections as $sec): if (!$sec['is_active']) continue; ?>
|
||||
<?php if (!empty($sec['heading'])): ?>
|
||||
<h3 style="font-size:1.1rem;font-weight:600;margin:.5rem 0 .4rem"><?= htmlspecialchars($sec['heading']) ?></h3>
|
||||
<?php endif; ?>
|
||||
<?php foreach (array_filter(array_map('trim', preg_split('/\n{2,}/', $sec['body']))) as $para): ?>
|
||||
<p style="margin:0 0 1rem"><?= nl2br(htmlspecialchars($para)) ?></p>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<a href="#" class="btn btn-primary" style="margin-top:.5rem;pointer-events:none;opacity:.8">Explore Our Coffee →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section List -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding:0">
|
||||
<?php if (empty($sections)): ?>
|
||||
<div class="text-center text-muted" style="padding:3rem">No sections yet. Click <strong>Add Section</strong> to get started.</div>
|
||||
<?php else: ?>
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px"></th>
|
||||
<th>Heading</th>
|
||||
<th>Body Text</th>
|
||||
<th style="width:60px">Order</th>
|
||||
<th style="width:80px">Status</th>
|
||||
<th style="width:100px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sectionTbody">
|
||||
<?php foreach ($sections as $sec): ?>
|
||||
<tr data-id="<?= $sec['section_id'] ?>">
|
||||
<td style="color:var(--admin-text-muted);text-align:center;cursor:grab"><i class="fas fa-grip-vertical"></i></td>
|
||||
<td style="font-weight:600;min-width:120px"><?= htmlspecialchars($sec['heading'] ?? '—') ?></td>
|
||||
<td style="color:var(--admin-text-muted);font-size:.875rem">
|
||||
<div style="max-width:420px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
<?= htmlspecialchars($sec['body']) ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="sort-cell"><?= $sec['sort_order'] ?></td>
|
||||
<td>
|
||||
<?= $sec['is_active']
|
||||
? '<span class="badge badge-success">Active</span>'
|
||||
: '<span class="badge badge-error">Hidden</span>' ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openModal(<?= json_encode($sec, JSON_HEX_APOS | JSON_HEX_QUOT) ?>)' title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" style="display:inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="section_id" value="<?= $sec['section_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this section?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal ──────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="sectionModal">
|
||||
<div class="modal" style="max-width:680px;width:95vw">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modalTitle">Add Section</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('sectionModal')">×</button>
|
||||
</div>
|
||||
<form method="POST" id="sectionForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="formAction" value="create">
|
||||
<input type="hidden" name="section_id" id="formSectionId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
Heading <span class="text-muted" style="font-weight:400">(optional sub-heading above this block)</span>
|
||||
</label>
|
||||
<input type="text" name="heading" id="formHeading" class="form-input"
|
||||
placeholder="e.g. Our Mission, From Farm to Cup…">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Body Text *</label>
|
||||
<textarea name="body" id="formBody" class="form-input" rows="10"
|
||||
style="resize:vertical;font-size:.9375rem;line-height:1.6"
|
||||
placeholder="Type your text here. Leave a blank line between paragraphs to create separate paragraph blocks."
|
||||
required oninput="updatePreview()"></textarea>
|
||||
<small class="text-muted"><i class="fas fa-paragraph"></i> Blank line = new paragraph | <i class="fas fa-level-down-alt fa-rotate-90"></i> Single line break = <br></small>
|
||||
</div>
|
||||
|
||||
<!-- Inline preview inside modal -->
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label class="form-label" style="display:flex;justify-content:space-between">
|
||||
<span>Text Preview</span>
|
||||
<span class="text-muted" style="font-weight:400;font-size:.8rem">Updates as you type</span>
|
||||
</label>
|
||||
<div id="inlinePreview"
|
||||
style="border:1px solid var(--color-border);border-radius:var(--radius-md);padding:1rem 1.25rem;
|
||||
background:var(--admin-bg);min-height:80px;font-size:.9375rem;color:var(--admin-text-muted);line-height:1.7">
|
||||
<em style="opacity:.4">Preview will appear here…</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div id="statusWrap" style="display:none;margin-right:auto">
|
||||
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
|
||||
<input type="checkbox" name="is_active" id="formActive" checked> Active
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group mb-0" style="margin-right:auto">
|
||||
<label class="form-label" style="display:inline;margin-right:.5rem">Order</label>
|
||||
<input type="number" name="sort_order" id="formOrder" class="form-input"
|
||||
value="0" min="0" style="width:70px;display:inline-block">
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('sectionModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="formSubmitBtn">Add Section</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#sectionTbody tr { cursor: grab; }
|
||||
#sectionTbody tr.drag-over { background: rgba(255,94,26,.06); }
|
||||
#formBody:focus { border-color: var(--admin-primary); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* ── Modal ───────────────────────────────────────── */
|
||||
function openModal(sec) {
|
||||
var isEdit = !!sec;
|
||||
document.getElementById('modalTitle').textContent = isEdit ? 'Edit Section' : 'Add Section';
|
||||
document.getElementById('formSubmitBtn').textContent = isEdit ? 'Save Changes' : 'Add Section';
|
||||
document.getElementById('formAction').value = isEdit ? 'update' : 'create';
|
||||
document.getElementById('formSectionId').value = isEdit ? sec.section_id : '';
|
||||
document.getElementById('formHeading').value = isEdit ? (sec.heading || '') : '';
|
||||
document.getElementById('formBody').value = isEdit ? sec.body : '';
|
||||
document.getElementById('formOrder').value = isEdit ? sec.sort_order : 0;
|
||||
document.getElementById('formActive').checked = isEdit ? !!parseInt(sec.is_active) : true;
|
||||
document.getElementById('statusWrap').style.display = isEdit ? '' : 'none';
|
||||
updatePreview();
|
||||
Modal.open('sectionModal');
|
||||
// Focus textarea after modal opens
|
||||
setTimeout(function() { document.getElementById('formBody').focus(); }, 150);
|
||||
}
|
||||
|
||||
/* ── Inline text preview ─────────────────────────── */
|
||||
function updatePreview() {
|
||||
var raw = document.getElementById('formBody').value;
|
||||
var box = document.getElementById('inlinePreview');
|
||||
if (!raw.trim()) {
|
||||
box.innerHTML = '<em style="opacity:.4">Preview will appear here…</em>';
|
||||
return;
|
||||
}
|
||||
var paras = raw.split(/\n{2,}/).map(function(p) { return p.trim(); }).filter(Boolean);
|
||||
box.innerHTML = paras.map(function(p) {
|
||||
return '<p style="margin:0 0 .75rem">' + p.replace(/\n/g, '<br>').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/<br>/g,'<br>') + '</p>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ── Drag-to-reorder ─────────────────────────────── */
|
||||
(function() {
|
||||
var tbody = document.getElementById('sectionTbody');
|
||||
if (!tbody) return;
|
||||
var dragging = null;
|
||||
|
||||
tbody.querySelectorAll('tr').forEach(function(row) {
|
||||
row.draggable = true;
|
||||
row.addEventListener('dragstart', function() { dragging = this; this.style.opacity = '.4'; });
|
||||
row.addEventListener('dragend', function() { this.style.opacity = ''; dragging = null; saveOrder(); });
|
||||
row.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('drag-over'); });
|
||||
row.addEventListener('dragleave', function() { this.classList.remove('drag-over'); });
|
||||
row.addEventListener('drop', function(e) {
|
||||
e.preventDefault(); this.classList.remove('drag-over');
|
||||
if (dragging && dragging !== this) {
|
||||
if (Array.from(tbody.querySelectorAll('tr')).indexOf(dragging) <
|
||||
Array.from(tbody.querySelectorAll('tr')).indexOf(this)) this.after(dragging);
|
||||
else this.before(dragging);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function saveOrder() {
|
||||
var ids = Array.from(tbody.querySelectorAll('tr')).map(function(r) { return r.dataset.id; });
|
||||
fetch('/admin/about-us.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'action=reorder&order=' + encodeURIComponent(JSON.stringify(ids))
|
||||
});
|
||||
tbody.querySelectorAll('tr').forEach(function(r, i) {
|
||||
r.querySelector('.sort-cell').textContent = i + 1;
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,714 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Advanced Analytics Dashboard
|
||||
*/
|
||||
|
||||
$pageTitle = 'Advanced Analytics';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Get date range from query params or default to last 30 days
|
||||
$endDate = $_GET['end_date'] ?? date('Y-m-d');
|
||||
$startDate = $_GET['start_date'] ?? date('Y-m-d', strtotime('-30 days'));
|
||||
$period = $_GET['period'] ?? '30';
|
||||
|
||||
if ($period === '7') {
|
||||
$startDate = date('Y-m-d', strtotime('-7 days'));
|
||||
} elseif ($period === '30') {
|
||||
$startDate = date('Y-m-d', strtotime('-30 days'));
|
||||
} elseif ($period === '90') {
|
||||
$startDate = date('Y-m-d', strtotime('-90 days'));
|
||||
} elseif ($period === '365') {
|
||||
$startDate = date('Y-m-d', strtotime('-1 year'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Sales Overview
|
||||
$salesOverview = db()->fetch(
|
||||
"SELECT
|
||||
COUNT(*) as total_orders,
|
||||
COALESCE(SUM(total), 0) as total_revenue,
|
||||
COALESCE(AVG(total), 0) as avg_order_value,
|
||||
COUNT(DISTINCT customer_id) as unique_customers
|
||||
FROM orders
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end AND payment_status = 'paid'",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
|
||||
// Ensure defaults
|
||||
if (!$salesOverview) {
|
||||
$salesOverview = ['total_orders' => 0, 'total_revenue' => 0, 'avg_order_value' => 0, 'unique_customers' => 0];
|
||||
} else {
|
||||
$salesOverview['total_orders'] = (int)($salesOverview['total_orders'] ?? 0);
|
||||
$salesOverview['total_revenue'] = (float)($salesOverview['total_revenue'] ?? 0);
|
||||
$salesOverview['avg_order_value'] = (float)($salesOverview['avg_order_value'] ?? 0);
|
||||
$salesOverview['unique_customers'] = (int)($salesOverview['unique_customers'] ?? 0);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$salesOverview = ['total_orders' => 0, 'total_revenue' => 0, 'avg_order_value' => 0, 'unique_customers' => 0];
|
||||
}
|
||||
|
||||
// Previous period for comparison
|
||||
$daysDiff = (strtotime($endDate) - strtotime($startDate)) / 86400;
|
||||
$prevEndDate = date('Y-m-d', strtotime($startDate . ' -1 day'));
|
||||
$prevStartDate = date('Y-m-d', strtotime($prevEndDate . " -{$daysDiff} days"));
|
||||
|
||||
try {
|
||||
$prevSalesOverview = db()->fetch(
|
||||
"SELECT
|
||||
COUNT(*) as total_orders,
|
||||
COALESCE(SUM(total), 0) as total_revenue
|
||||
FROM orders
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end AND payment_status = 'paid'",
|
||||
['start' => $prevStartDate, 'end' => $prevEndDate]
|
||||
);
|
||||
|
||||
if (!$prevSalesOverview) {
|
||||
$prevSalesOverview = ['total_orders' => 0, 'total_revenue' => 0];
|
||||
} else {
|
||||
$prevSalesOverview['total_orders'] = (int)($prevSalesOverview['total_orders'] ?? 0);
|
||||
$prevSalesOverview['total_revenue'] = (float)($prevSalesOverview['total_revenue'] ?? 0);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$prevSalesOverview = ['total_orders' => 0, 'total_revenue' => 0];
|
||||
}
|
||||
|
||||
try {
|
||||
// Daily Sales Data for chart
|
||||
$dailySales = db()->fetchAll(
|
||||
"SELECT
|
||||
DATE(created_at) as date,
|
||||
COUNT(*) as orders,
|
||||
COALESCE(SUM(total), 0) as revenue
|
||||
FROM orders
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end AND payment_status = 'paid'
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$dailySales = [];
|
||||
}
|
||||
|
||||
// Top Selling Products (from orders JSON items field)
|
||||
$topProducts = [];
|
||||
try {
|
||||
$orders = db()->fetchAll(
|
||||
"SELECT items, total FROM orders
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end AND payment_status = 'paid'",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
|
||||
$productCounts = [];
|
||||
foreach ($orders as $order) {
|
||||
$items = json_decode($order['items'], true) ?? [];
|
||||
foreach ($items as $item) {
|
||||
$name = $item['name'] ?? 'Unknown';
|
||||
if (!isset($productCounts[$name])) {
|
||||
$productCounts[$name] = ['name' => $name, 'total_sold' => 0, 'total_revenue' => 0];
|
||||
}
|
||||
$productCounts[$name]['total_sold'] += $item['quantity'] ?? 1;
|
||||
$productCounts[$name]['total_revenue'] += $item['total'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
usort($productCounts, fn($a, $b) => $b['total_sold'] - $a['total_sold']);
|
||||
$topProducts = array_slice(array_values($productCounts), 0, 10);
|
||||
} catch (Exception $e) {
|
||||
$topProducts = [];
|
||||
}
|
||||
|
||||
// Sales by Category (from orders JSON - more reliable)
|
||||
$categoryStats = [];
|
||||
try {
|
||||
foreach ($orders ?? [] as $order) {
|
||||
$items = json_decode($order['items'], true) ?? [];
|
||||
foreach ($items as $item) {
|
||||
$cat = 'General';
|
||||
if (!isset($categoryStats[$cat])) {
|
||||
$categoryStats[$cat] = ['category' => $cat, 'orders' => 0, 'items_sold' => 0, 'revenue' => 0];
|
||||
}
|
||||
$categoryStats[$cat]['items_sold'] += $item['quantity'] ?? 1;
|
||||
$categoryStats[$cat]['revenue'] += $item['total'] ?? 0;
|
||||
}
|
||||
}
|
||||
$categoryStats = array_values($categoryStats);
|
||||
} catch (Exception $e) {
|
||||
$categoryStats = [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Customer Acquisition
|
||||
$newCustomers = db()->fetch(
|
||||
"SELECT COUNT(*) as count FROM customers WHERE DATE(created_at) BETWEEN :start AND :end",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
)['count'] ?? 0;
|
||||
} catch (Exception $e) {
|
||||
$newCustomers = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
$returningCustomers = db()->fetch(
|
||||
"SELECT COUNT(DISTINCT customer_id) as count
|
||||
FROM orders o
|
||||
WHERE DATE(o.created_at) BETWEEN :start AND :end
|
||||
AND payment_status = 'paid'
|
||||
AND customer_id IN (
|
||||
SELECT customer_id FROM orders
|
||||
WHERE DATE(created_at) < :start2 AND payment_status = 'paid'
|
||||
)",
|
||||
['start' => $startDate, 'end' => $endDate, 'start2' => $startDate]
|
||||
)['count'] ?? 0;
|
||||
} catch (Exception $e) {
|
||||
$returningCustomers = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
// Payment Methods Distribution
|
||||
$paymentMethods = db()->fetchAll(
|
||||
"SELECT
|
||||
COALESCE(payment_method, 'Unknown') as method,
|
||||
COUNT(*) as count,
|
||||
SUM(total) as revenue
|
||||
FROM orders
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end AND payment_status = 'paid'
|
||||
GROUP BY payment_method
|
||||
ORDER BY count DESC",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$paymentMethods = [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Abandoned Carts
|
||||
$abandonedCarts = db()->fetch(
|
||||
"SELECT COUNT(*) as count, COALESCE(SUM(subtotal), 0) as value
|
||||
FROM abandoned_carts
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end AND recovered = 0",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
|
||||
if (!$abandonedCarts) {
|
||||
$abandonedCarts = ['count' => 0, 'value' => 0];
|
||||
} else {
|
||||
$abandonedCarts['count'] = (int)($abandonedCarts['count'] ?? 0);
|
||||
$abandonedCarts['value'] = (float)($abandonedCarts['value'] ?? 0);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$abandonedCarts = ['count' => 0, 'value' => 0];
|
||||
}
|
||||
|
||||
try {
|
||||
// Order Status Distribution
|
||||
$orderStatuses = db()->fetchAll(
|
||||
"SELECT order_status, COUNT(*) as count
|
||||
FROM orders
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end
|
||||
GROUP BY order_status",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$orderStatuses = [];
|
||||
}
|
||||
|
||||
try {
|
||||
// Hourly Sales Pattern
|
||||
$hourlySales = db()->fetchAll(
|
||||
"SELECT
|
||||
HOUR(created_at) as hour,
|
||||
COUNT(*) as orders,
|
||||
COALESCE(SUM(total), 0) as revenue
|
||||
FROM orders
|
||||
WHERE DATE(created_at) BETWEEN :start AND :end AND payment_status = 'paid'
|
||||
GROUP BY HOUR(created_at)
|
||||
ORDER BY hour",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$hourlySales = [];
|
||||
}
|
||||
|
||||
// Top Customers
|
||||
try {
|
||||
$topCustomers = db()->fetchAll(
|
||||
"SELECT
|
||||
c.customer_id,
|
||||
c.name,
|
||||
c.email,
|
||||
COUNT(o.order_id) as order_count,
|
||||
COALESCE(SUM(o.total), 0) as total_spent
|
||||
FROM customers c
|
||||
JOIN orders o ON c.customer_id = o.customer_id
|
||||
WHERE DATE(o.created_at) BETWEEN :start AND :end AND o.payment_status = 'paid'
|
||||
GROUP BY c.customer_id
|
||||
ORDER BY total_spent DESC
|
||||
LIMIT 10",
|
||||
['start' => $startDate, 'end' => $endDate]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$topCustomers = [];
|
||||
}
|
||||
|
||||
// Inventory Stats
|
||||
try {
|
||||
$lowStockCount = db()->count('products', 'stock <= low_stock_threshold AND stock > 0');
|
||||
$outOfStockCount = db()->count('products', 'stock = 0 AND is_active = 1');
|
||||
} catch (Exception $e) {
|
||||
$lowStockCount = 0;
|
||||
$outOfStockCount = 0;
|
||||
}
|
||||
|
||||
// Calculate percentage changes
|
||||
$revenueChange = $prevSalesOverview['total_revenue'] > 0
|
||||
? (($salesOverview['total_revenue'] - $prevSalesOverview['total_revenue']) / $prevSalesOverview['total_revenue']) * 100
|
||||
: 0;
|
||||
$ordersChange = $prevSalesOverview['total_orders'] > 0
|
||||
? (($salesOverview['total_orders'] - $prevSalesOverview['total_orders']) / $prevSalesOverview['total_orders']) * 100
|
||||
: 0;
|
||||
?>
|
||||
|
||||
<style>
|
||||
.analytics-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.date-filter {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-filter .btn {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.date-filter .btn.active {
|
||||
background: var(--admin-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--admin-surface);
|
||||
border-radius: var(--admin-radius);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-card-title {
|
||||
color: var(--admin-text-muted);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.stat-card-icon.primary {
|
||||
background: rgba(255, 94, 26, 0.1);
|
||||
color: var(--admin-primary);
|
||||
}
|
||||
|
||||
.stat-card-icon.success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--admin-success);
|
||||
}
|
||||
|
||||
.stat-card-icon.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--admin-warning);
|
||||
}
|
||||
|
||||
.stat-card-icon.info {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-card-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-card-change {
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-card-change.positive {
|
||||
color: var(--admin-success);
|
||||
}
|
||||
|
||||
.stat-card-change.negative {
|
||||
color: var(--admin-error);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: var(--admin-surface);
|
||||
border-radius: var(--admin-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.analytics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.analytics-grid-equal {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--admin-bg);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--admin-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mini-chart {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.mini-chart-bar {
|
||||
flex: 1;
|
||||
background: rgba(255, 94, 26, 0.3);
|
||||
border-radius: 4px 4px 0 0;
|
||||
min-height: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mini-chart-bar:hover {
|
||||
background: var(--admin-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.analytics-grid,
|
||||
.analytics-grid-equal {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="analytics-header">
|
||||
<div>
|
||||
<h1 class="page-title">Advanced Analytics</h1>
|
||||
<p class="text-muted"><?= date('M d, Y', strtotime($startDate)) ?> - <?= date('M d, Y', strtotime($endDate)) ?></p>
|
||||
</div>
|
||||
|
||||
<div class="date-filter">
|
||||
<a href="?period=7" class="btn <?= $period === '7' ? 'btn-primary active' : 'btn-secondary' ?>">7 Days</a>
|
||||
<a href="?period=30" class="btn <?= $period === '30' ? 'btn-primary active' : 'btn-secondary' ?>">30 Days</a>
|
||||
<a href="?period=90" class="btn <?= $period === '90' ? 'btn-primary active' : 'btn-secondary' ?>">90 Days</a>
|
||||
<a href="?period=365" class="btn <?= $period === '365' ? 'btn-primary active' : 'btn-secondary' ?>">1 Year</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<h3 class="stat-card-title">Total Revenue</h3>
|
||||
<div class="stat-card-icon primary"><i class="fas fa-dollar-sign"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= formatCurrency($salesOverview['total_revenue'] ?? 0) ?></div>
|
||||
<div class="stat-card-change <?= $revenueChange >= 0 ? 'positive' : 'negative' ?>">
|
||||
<i class="fas fa-<?= $revenueChange >= 0 ? 'arrow-up' : 'arrow-down' ?>"></i>
|
||||
<?= abs(round($revenueChange, 1)) ?>% vs previous period
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<h3 class="stat-card-title">Total Orders</h3>
|
||||
<div class="stat-card-icon success"><i class="fas fa-shopping-bag"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= number_format($salesOverview['total_orders'] ?? 0) ?></div>
|
||||
<div class="stat-card-change <?= $ordersChange >= 0 ? 'positive' : 'negative' ?>">
|
||||
<i class="fas fa-<?= $ordersChange >= 0 ? 'arrow-up' : 'arrow-down' ?>"></i>
|
||||
<?= abs(round($ordersChange, 1)) ?>% vs previous period
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<h3 class="stat-card-title">Average Order Value</h3>
|
||||
<div class="stat-card-icon warning"><i class="fas fa-receipt"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= formatCurrency($salesOverview['avg_order_value'] ?? 0) ?></div>
|
||||
<div class="stat-card-change" style="color: var(--admin-text-muted);">
|
||||
<?= $salesOverview['unique_customers'] ?? 0 ?> unique customers
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<h3 class="stat-card-title">Abandoned Carts</h3>
|
||||
<div class="stat-card-icon info"><i class="fas fa-cart-arrow-down"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= number_format($abandonedCarts['count'] ?? 0) ?></div>
|
||||
<div class="stat-card-change" style="color: var(--admin-warning);">
|
||||
<?= formatCurrency($abandonedCarts['value'] ?? 0) ?> potential revenue
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revenue Chart -->
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Revenue Trend</h3>
|
||||
</div>
|
||||
<div class="mini-chart" id="revenueChart">
|
||||
<?php
|
||||
$maxRevenue = max(array_column($dailySales, 'revenue') ?: [1]);
|
||||
foreach ($dailySales as $day):
|
||||
$height = $maxRevenue > 0 ? ($day['revenue'] / $maxRevenue) * 100 : 0;
|
||||
?>
|
||||
<div class="mini-chart-bar"
|
||||
style="height: <?= max(4, $height) ?>%;"
|
||||
title="<?= date('M d', strtotime($day['date'])) ?>: <?= formatCurrency($day['revenue']) ?>"></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (empty($dailySales)): ?>
|
||||
<div style="width: 100%; text-align: center; padding: 2rem; color: var(--admin-text-muted);">
|
||||
No sales data for this period
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analytics-grid">
|
||||
<!-- Top Products -->
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Top Selling Products</h3>
|
||||
</div>
|
||||
<?php if (empty($topProducts)): ?>
|
||||
<p class="text-muted text-center">No product data available</p>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$maxSold = max(array_column($topProducts, 'total_sold') ?: [1]);
|
||||
foreach ($topProducts as $i => $product):
|
||||
?>
|
||||
<div class="list-item">
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.25rem;">
|
||||
<span style="font-weight: 500;"><?= $i + 1 ?>.</span>
|
||||
<span style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<?= htmlspecialchars(truncate($product['name'], 30)) ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress-bar" style="width: 200px;">
|
||||
<div class="progress-bar-fill" style="width: <?= ($product['total_sold'] / $maxSold) * 100 ?>%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-weight: 600;"><?= number_format($product['total_sold']) ?> sold</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;"><?= formatCurrency($product['total_revenue']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Sales by Category -->
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Sales by Category</h3>
|
||||
</div>
|
||||
<?php if (empty($categoryStats)): ?>
|
||||
<p class="text-muted text-center">No category data available</p>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$totalCatRevenue = array_sum(array_column($categoryStats, 'revenue')) ?: 1;
|
||||
$colors = ['#FF5E1A', '#10B981', '#F59E0B', '#3B82F6', '#8B5CF6', '#EC4899'];
|
||||
foreach ($categoryStats as $i => $cat):
|
||||
$percentage = ($cat['revenue'] / $totalCatRevenue) * 100;
|
||||
?>
|
||||
<div class="list-item">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<div style="width: 12px; height: 12px; border-radius: 3px; background: <?= $colors[$i % count($colors)] ?>;"></div>
|
||||
<span style="font-weight: 500;"><?= htmlspecialchars($cat['category']) ?></span>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-weight: 600;"><?= formatCurrency($cat['revenue']) ?></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;"><?= round($percentage, 1) ?>%</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="analytics-grid-equal">
|
||||
<!-- Customer Insights -->
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Customer Insights</h3>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div style="text-align: center; padding: 1.5rem; background: var(--admin-bg); border-radius: var(--admin-radius);">
|
||||
<div style="font-size: 2rem; font-weight: 700; color: var(--admin-success);"><?= $newCustomers ?></div>
|
||||
<div class="text-muted" style="font-size: 0.875rem;">New Customers</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 1.5rem; background: var(--admin-bg); border-radius: var(--admin-radius);">
|
||||
<div style="font-size: 2rem; font-weight: 700; color: var(--admin-primary);"><?= $returningCustomers ?></div>
|
||||
<div class="text-muted" style="font-size: 0.875rem;">Returning Customers</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="margin: 0 0 0.75rem; font-size: 0.875rem;">Top Customers</h4>
|
||||
<?php foreach (array_slice($topCustomers, 0, 5) as $customer): ?>
|
||||
<div class="list-item" style="padding: 0.5rem 0;">
|
||||
<div>
|
||||
<div style="font-weight: 500;"><?= htmlspecialchars($customer['name'] ?? $customer['email']) ?></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;"><?= $customer['order_count'] ?> orders</div>
|
||||
</div>
|
||||
<div style="font-weight: 600; color: var(--admin-success);"><?= formatCurrency($customer['total_spent']) ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Payment & Inventory -->
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Payment Methods</h3>
|
||||
</div>
|
||||
<?php if (empty($paymentMethods)): ?>
|
||||
<p class="text-muted text-center">No payment data available</p>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$totalPayments = array_sum(array_column($paymentMethods, 'count')) ?: 1;
|
||||
foreach ($paymentMethods as $method):
|
||||
?>
|
||||
<div class="list-item" style="padding: 0.5rem 0;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<i class="fas fa-<?= match($method['method']) {
|
||||
'card', 'stripe' => 'credit-card',
|
||||
'cash' => 'money-bill',
|
||||
'wallet' => 'wallet',
|
||||
default => 'money-check'
|
||||
} ?>" style="color: var(--admin-text-muted);"></i>
|
||||
<span style="font-weight: 500;"><?= ucfirst($method['method']) ?></span>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="font-weight: 600;"><?= round(($method['count'] / $totalPayments) * 100, 1) ?>%</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;"><?= formatCurrency($method['revenue']) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<h4 style="margin: 1.5rem 0 0.75rem; font-size: 0.875rem;">Inventory Alerts</h4>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div style="padding: 1rem; background: rgba(245, 158, 11, 0.1); border-radius: var(--admin-radius); text-align: center;">
|
||||
<div style="font-size: 1.5rem; font-weight: 700; color: var(--admin-warning);"><?= $lowStockCount ?></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">Low Stock</div>
|
||||
</div>
|
||||
<div style="padding: 1rem; background: rgba(239, 68, 68, 0.1); border-radius: var(--admin-radius); text-align: center;">
|
||||
<div style="font-size: 1.5rem; font-weight: 700; color: var(--admin-error);"><?= $outOfStockCount ?></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">Out of Stock</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hourly Sales Pattern -->
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">Sales by Hour of Day</h3>
|
||||
</div>
|
||||
<div class="mini-chart" style="height: 120px;">
|
||||
<?php
|
||||
// Fill in missing hours
|
||||
$hourlyData = array_fill(0, 24, 0);
|
||||
foreach ($hourlySales as $h) {
|
||||
$hourlyData[$h['hour']] = $h['orders'];
|
||||
}
|
||||
$maxHourly = max($hourlyData) ?: 1;
|
||||
|
||||
for ($h = 0; $h < 24; $h++):
|
||||
$height = ($hourlyData[$h] / $maxHourly) * 100;
|
||||
$label = $h === 0 ? '12am' : ($h < 12 ? "{$h}am" : ($h === 12 ? '12pm' : ($h - 12) . 'pm'));
|
||||
?>
|
||||
<div class="mini-chart-bar"
|
||||
style="height: <?= max(4, $height) ?>%;"
|
||||
title="<?= $label ?>: <?= $hourlyData[$h] ?> orders"></div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; font-size: 0.7rem; color: var(--admin-text-muted);">
|
||||
<span>12am</span>
|
||||
<span>6am</span>
|
||||
<span>12pm</span>
|
||||
<span>6pm</span>
|
||||
<span>12am</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
header('Content-Type: application/json');
|
||||
if (!AdminAuth::isLoggedIn()) { echo json_encode(['error'=>'Unauthorized']); exit; }
|
||||
$cid = trim($_GET['customer_id'] ?? '');
|
||||
if (!$cid) { echo json_encode(['error'=>'No customer ID','orders'=>[]]); exit; }
|
||||
try {
|
||||
$orders = db()->fetchAll(
|
||||
"SELECT order_id, order_number, total, order_status, payment_status, items, shipping_address, tracking_number, created_at FROM orders WHERE customer_id = :id ORDER BY created_at DESC",
|
||||
['id' => $cid]
|
||||
);
|
||||
echo json_encode(['success'=>true,'orders'=>$orders]);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['error'=>$e->getMessage(),'orders'=>[]]);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/header.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['image'])) {
|
||||
echo json_encode(['error' => 'No file received']); exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['image'];
|
||||
$allowed = ['image/jpeg','image/png','image/gif','image/webp'];
|
||||
if (!in_array($file['type'], $allowed)) {
|
||||
echo json_encode(['error' => 'Invalid type. Use JPG, PNG, WebP or GIF.']); exit;
|
||||
}
|
||||
if ($file['size'] > 5 * 1024 * 1024) {
|
||||
echo json_encode(['error' => 'File too large (max 5 MB).']); exit;
|
||||
}
|
||||
|
||||
$dir = __DIR__ . '/../../uploads/splashes/';
|
||||
if (!is_dir($dir)) mkdir($dir, 0755, true);
|
||||
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
$name = 'splash_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
|
||||
$path = $dir . $name;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $path)) {
|
||||
echo json_encode(['success' => true, 'url' => '/uploads/splashes/' . $name]);
|
||||
} else {
|
||||
echo json_encode(['error' => 'Could not save file.']);
|
||||
}
|
||||
@@ -0,0 +1,642 @@
|
||||
/**
|
||||
* Tom's Java Jive - Admin Stylesheet
|
||||
*/
|
||||
|
||||
:root {
|
||||
--admin-primary: #E86A33;
|
||||
--admin-primary-dark: #C4562A;
|
||||
--admin-secondary: #8B4513;
|
||||
--admin-success: #10B981;
|
||||
--admin-warning: #F59E0B;
|
||||
--admin-error: #EF4444;
|
||||
--admin-info: #3B82F6;
|
||||
|
||||
--admin-bg: #F3F4F6;
|
||||
--admin-surface: #FFFFFF;
|
||||
--admin-border: #E5E7EB;
|
||||
--admin-text: #111827;
|
||||
--admin-text-muted: #6B7280;
|
||||
--admin-text-light: #9CA3AF;
|
||||
|
||||
--admin-sidebar-bg: #1F2937;
|
||||
--admin-sidebar-text: #E5E7EB;
|
||||
--admin-sidebar-active: rgba(232, 106, 51, 0.2);
|
||||
|
||||
--admin-radius: 8px;
|
||||
--admin-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
|
||||
--admin-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.admin-body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-size: 0.9375rem;
|
||||
background: var(--admin-bg);
|
||||
color: var(--admin-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.admin-sidebar {
|
||||
width: 260px;
|
||||
background: var(--admin-sidebar-bg);
|
||||
color: var(--admin-sidebar-text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.admin-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.admin-logo .logo-img {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1rem 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-group-title {
|
||||
display: block;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--admin-text-light);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
color: var(--admin-sidebar-text);
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--admin-sidebar-active);
|
||||
color: var(--admin-primary);
|
||||
border-right: 3px solid var(--admin-primary);
|
||||
}
|
||||
|
||||
.nav-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.nav-item.active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.admin-main {
|
||||
flex: 1;
|
||||
margin-left: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.admin-header {
|
||||
background: var(--admin-surface);
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.header-search {
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-search i {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--admin-text-light);
|
||||
}
|
||||
|
||||
.header-search input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem 0.5rem 2.5rem;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: var(--admin-radius);
|
||||
font-size: 0.875rem;
|
||||
background: var(--admin-bg);
|
||||
}
|
||||
|
||||
.header-search input:focus {
|
||||
outline: none;
|
||||
border-color: var(--admin-primary);
|
||||
background: var(--admin-surface);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.admin-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Content Area */
|
||||
.admin-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--admin-surface);
|
||||
border-radius: var(--admin-radius);
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
box-shadow: var(--admin-shadow);
|
||||
}
|
||||
|
||||
.stat-card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--admin-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.stat-card-icon.primary { background: rgba(232, 106, 51, 0.15); color: var(--admin-primary); }
|
||||
.stat-card-icon.success { background: rgba(16, 185, 129, 0.15); color: var(--admin-success); }
|
||||
.stat-card-icon.warning { background: rgba(245, 158, 11, 0.15); color: var(--admin-warning); }
|
||||
.stat-card-icon.error { background: rgba(239, 68, 68, 0.15); color: var(--admin-error); }
|
||||
|
||||
.stat-card-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.stat-card-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
|
||||
/* Admin Cards */
|
||||
.admin-card {
|
||||
background: var(--admin-surface);
|
||||
border-radius: var(--admin-radius);
|
||||
box-shadow: var(--admin-shadow);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.admin-card-header {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--admin-text-muted);
|
||||
background: var(--admin-bg);
|
||||
}
|
||||
|
||||
.admin-table tr:hover {
|
||||
background: var(--admin-bg);
|
||||
}
|
||||
|
||||
.admin-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--admin-radius);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--admin-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--admin-primary-dark);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--admin-surface);
|
||||
color: var(--admin-text);
|
||||
border: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--admin-bg);
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--admin-error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #DC2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--admin-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.375rem;
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.9375rem;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: var(--admin-radius);
|
||||
background: var(--admin-surface);
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--admin-primary);
|
||||
box-shadow: 0 0 0 3px rgba(232, 106, 51, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--admin-error);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: rgba(232, 106, 51, 0.15);
|
||||
color: var(--admin-primary);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: var(--admin-success);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: var(--admin-warning);
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--admin-error);
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: var(--admin-radius);
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--admin-success);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--admin-error);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--admin-warning);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-muted { color: var(--admin-text-muted); }
|
||||
.text-error { color: var(--admin-error); }
|
||||
.text-success { color: var(--admin-success); }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-1 { margin-bottom: 1rem; }
|
||||
.mb-2 { margin-bottom: 1.5rem; }
|
||||
.mt-1 { margin-top: 1rem; }
|
||||
.mt-2 { margin-top: 1.5rem; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.admin-sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.admin-sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid currentColor;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--admin-surface);
|
||||
border-radius: var(--admin-radius);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active .modal {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
border-top: 1px solid var(--admin-border);
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Tom's Java Jive - Admin JavaScript
|
||||
*/
|
||||
|
||||
// Sidebar Toggle
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sidebarToggle = document.getElementById('sidebarToggle');
|
||||
const sidebar = document.querySelector('.admin-sidebar');
|
||||
|
||||
if (sidebarToggle && sidebar) {
|
||||
sidebarToggle.addEventListener('click', function() {
|
||||
sidebar.classList.toggle('open');
|
||||
});
|
||||
|
||||
// Close sidebar when clicking outside on mobile
|
||||
document.addEventListener('click', function(e) {
|
||||
if (window.innerWidth <= 1024) {
|
||||
if (!sidebar.contains(e.target) && !sidebarToggle.contains(e.target)) {
|
||||
sidebar.classList.remove('open');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm dialogs
|
||||
document.querySelectorAll('[data-confirm]').forEach(el => {
|
||||
el.addEventListener('click', function(e) {
|
||||
if (!confirm(this.dataset.confirm)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-hide alerts
|
||||
document.querySelectorAll('.alert').forEach(alert => {
|
||||
setTimeout(() => {
|
||||
alert.style.opacity = '0';
|
||||
alert.style.transform = 'translateY(-10px)';
|
||||
setTimeout(() => alert.remove(), 300);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
// Toast notifications
|
||||
const AdminToast = {
|
||||
container: null,
|
||||
|
||||
init() {
|
||||
if (!this.container) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:1000;display:flex;flex-direction:column;gap:8px;';
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
},
|
||||
|
||||
show(message, type = 'success', duration = 3000) {
|
||||
this.init();
|
||||
|
||||
const colors = {
|
||||
success: '#10B981',
|
||||
error: '#EF4444',
|
||||
warning: '#F59E0B',
|
||||
info: '#3B82F6'
|
||||
};
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
background: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
border-left: 4px solid ${colors[type] || colors.info};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
toast.innerHTML = `
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}" style="color:${colors[type]}"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
this.container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideIn 0.3s ease reverse';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, duration);
|
||||
},
|
||||
|
||||
success(msg) { this.show(msg, 'success'); },
|
||||
error(msg) { this.show(msg, 'error'); },
|
||||
warning(msg) { this.show(msg, 'warning'); },
|
||||
info(msg) { this.show(msg, 'info'); }
|
||||
};
|
||||
|
||||
// API helper
|
||||
async function adminFetch(url, options = {}) {
|
||||
const defaults = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch(url, { ...defaults, ...options });
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Request failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
function setLoading(button, isLoading) {
|
||||
if (isLoading) {
|
||||
button.dataset.originalHtml = button.innerHTML;
|
||||
button.innerHTML = '<span class="loading"></span> Loading...';
|
||||
button.disabled = true;
|
||||
} else {
|
||||
button.innerHTML = button.dataset.originalHtml || button.innerHTML;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Format currency
|
||||
function formatCurrency(amount) {
|
||||
return '$' + parseFloat(amount).toFixed(2);
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(dateString) {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// Debounce
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Modal
|
||||
const Modal = {
|
||||
open(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
},
|
||||
|
||||
close(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Close modals on overlay click
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('modal-overlay')) {
|
||||
e.target.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Image preview
|
||||
function previewImage(input, previewId) {
|
||||
const preview = document.getElementById(previewId);
|
||||
if (!preview) return;
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
preview.src = e.target.result;
|
||||
preview.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add slideIn animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Email Campaigns
|
||||
*/
|
||||
|
||||
$pageTitle = 'Email Campaigns';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle send campaign
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_POST['action'] === 'send') {
|
||||
$subject = trim($_POST['subject'] ?? '');
|
||||
$content = trim($_POST['content'] ?? '');
|
||||
$testEmail = trim($_POST['test_email'] ?? '');
|
||||
|
||||
if (empty($subject) || empty($content)) {
|
||||
setFlash('error', 'Subject and content are required');
|
||||
} else {
|
||||
if ($testEmail) {
|
||||
// Send test email
|
||||
$sent = sendEmail($testEmail, $subject, $content);
|
||||
if ($sent) {
|
||||
setFlash('success', 'Test email sent to ' . $testEmail);
|
||||
} else {
|
||||
setFlash('error', 'Failed to send test email');
|
||||
}
|
||||
} else {
|
||||
// Send to all subscribers
|
||||
$subscribers = db()->fetchAll("SELECT email, name FROM email_subscribers WHERE is_active = 1");
|
||||
$sentCount = 0;
|
||||
|
||||
foreach ($subscribers as $sub) {
|
||||
$personalizedContent = str_replace(
|
||||
['{{name}}', '{{email}}'],
|
||||
[$sub['name'] ?? 'Valued Customer', $sub['email']],
|
||||
$content
|
||||
);
|
||||
|
||||
if (sendEmail($sub['email'], $subject, $personalizedContent)) {
|
||||
$sentCount++;
|
||||
}
|
||||
}
|
||||
|
||||
setFlash('success', "Campaign sent to $sentCount subscribers");
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: /admin/campaigns.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get subscriber stats
|
||||
$totalSubscribers = db()->count('email_subscribers', 'is_active = 1');
|
||||
$recentSubscribers = db()->fetchAll(
|
||||
"SELECT * FROM email_subscribers ORDER BY created_at DESC LIMIT 10"
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Email Campaigns</h1>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
|
||||
<!-- Campaign Form -->
|
||||
<div>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="send">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Create Campaign</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Subject Line *</label>
|
||||
<input type="text" name="subject" class="form-input" required placeholder="e.g., New Coffee Arrivals!">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email Content (HTML) *</label>
|
||||
<textarea name="content" class="form-textarea" rows="15" required placeholder="Enter HTML email content..."></textarea>
|
||||
<small class="text-muted">Use {{name}} for subscriber name, {{email}} for email</small>
|
||||
</div>
|
||||
|
||||
<div style="border-top: 1px solid var(--admin-border); padding-top: 1rem; margin-top: 1rem;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Send Test Email First (optional)</label>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<input type="email" name="test_email" class="form-input" placeholder="your@email.com">
|
||||
<button type="submit" class="btn btn-secondary">Send Test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg" onclick="return confirm('Send this campaign to <?= $totalSubscribers ?> subscribers?')">
|
||||
<i class="fas fa-paper-plane"></i> Send to All Subscribers (<?= $totalSubscribers ?>)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Email Templates -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Quick Templates</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
|
||||
<button class="btn btn-secondary" onclick="loadTemplate('welcome')">
|
||||
<i class="fas fa-hand-wave"></i> Welcome
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="loadTemplate('promo')">
|
||||
<i class="fas fa-percent"></i> Promo
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="loadTemplate('new_product')">
|
||||
<i class="fas fa-box-open"></i> New Product
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div>
|
||||
<!-- Stats -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Subscriber Stats</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div style="text-align: center; margin-bottom: 1rem;">
|
||||
<div style="font-size: 2.5rem; font-weight: 700; color: var(--admin-primary);"><?= $totalSubscribers ?></div>
|
||||
<div class="text-muted">Active Subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Subscribers -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Recent Subscribers</h3>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<?php if (empty($recentSubscribers)): ?>
|
||||
<p class="text-muted" style="padding: 1rem; text-align: center;">No subscribers yet</p>
|
||||
<?php else: ?>
|
||||
<ul style="list-style: none; padding: 0; margin: 0;">
|
||||
<?php foreach ($recentSubscribers as $sub): ?>
|
||||
<li style="padding: 0.75rem 1rem; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<div style="font-size: 0.875rem;"><?= htmlspecialchars($sub['email']) ?></div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;"><?= formatDate($sub['created_at']) ?></div>
|
||||
</div>
|
||||
<?php if (!$sub['is_active']): ?>
|
||||
<span class="badge badge-error">Unsubscribed</span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const templates = {
|
||||
welcome: {
|
||||
subject: 'Welcome to Tom\'s Java Jive! ☕',
|
||||
content: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #8B4513; color: white; padding: 30px; text-align: center;">
|
||||
<h1 style="margin: 0;">Welcome to the Family!</h1>
|
||||
</div>
|
||||
<div style="padding: 30px; background: #FDFBF7;">
|
||||
<p>Hi {{name}},</p>
|
||||
<p>Thank you for subscribing to Tom's Java Jive newsletter! We're thrilled to have you as part of our coffee-loving community.</p>
|
||||
<p>As a welcome gift, enjoy <strong>10% off</strong> your first order with code: <strong>WELCOME10</strong></p>
|
||||
<p style="text-align: center; margin-top: 30px;">
|
||||
<a href="https://tomsjavajive.com/shop.php" style="background: #E86A33; color: white; padding: 15px 30px; text-decoration: none; border-radius: 6px; font-weight: bold;">Shop Now</a>
|
||||
</p>
|
||||
<p style="margin-top: 30px;">Happy brewing!</p>
|
||||
<p>- The Tom's Java Jive Team</p>
|
||||
</div>
|
||||
</div>`
|
||||
},
|
||||
promo: {
|
||||
subject: '🎉 Special Offer Inside!',
|
||||
content: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: linear-gradient(135deg, #E86A33, #8B4513); color: white; padding: 40px; text-align: center;">
|
||||
<h1 style="margin: 0; font-size: 36px;">FLASH SALE</h1>
|
||||
<p style="font-size: 24px; margin-top: 10px;">20% OFF Everything</p>
|
||||
</div>
|
||||
<div style="padding: 30px; background: #FDFBF7; text-align: center;">
|
||||
<p>Hi {{name}},</p>
|
||||
<p>For a limited time, enjoy 20% off your entire order!</p>
|
||||
<p style="font-size: 24px; font-weight: bold; color: #E86A33; margin: 20px 0;">Use code: FLASH20</p>
|
||||
<p>Hurry - offer ends soon!</p>
|
||||
<p style="margin-top: 30px;">
|
||||
<a href="https://tomsjavajive.com/shop.php" style="background: #E86A33; color: white; padding: 15px 30px; text-decoration: none; border-radius: 6px; font-weight: bold;">Shop the Sale</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>`
|
||||
},
|
||||
new_product: {
|
||||
subject: '☕ New Arrival: You\'re Going to Love This!',
|
||||
content: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1 style="margin: 0;">New Coffee Alert!</h1>
|
||||
</div>
|
||||
<div style="padding: 30px; background: #FDFBF7;">
|
||||
<p>Hi {{name}},</p>
|
||||
<p>We're excited to introduce our latest addition to the Tom's Java Jive family!</p>
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0; text-align: center;">
|
||||
<h2 style="color: #8B4513;">[Product Name]</h2>
|
||||
<p>[Product Description]</p>
|
||||
<p style="font-size: 20px; font-weight: bold; color: #E86A33;">$XX.XX</p>
|
||||
</div>
|
||||
<p>Be among the first to try it!</p>
|
||||
<p style="text-align: center; margin-top: 30px;">
|
||||
<a href="https://tomsjavajive.com/shop.php" style="background: #E86A33; color: white; padding: 15px 30px; text-decoration: none; border-radius: 6px; font-weight: bold;">Try It Now</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
};
|
||||
|
||||
function loadTemplate(name) {
|
||||
const template = templates[name];
|
||||
if (template) {
|
||||
document.querySelector('input[name="subject"]').value = template.subject;
|
||||
document.querySelector('textarea[name="content"]').value = template.content;
|
||||
AdminToast.success('Template loaded');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Categories
|
||||
*/
|
||||
|
||||
$pageTitle = 'Categories';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create' || $action === 'update') {
|
||||
$categoryId = $_POST['category_id'] ?? '';
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$slug = trim($_POST['slug'] ?? '') ?: slugify($name);
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if (empty($name)) {
|
||||
setFlash('error', 'Category name is required');
|
||||
} else {
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'description' => $description,
|
||||
'is_active' => $isActive
|
||||
];
|
||||
|
||||
if ($action === 'update' && $categoryId) {
|
||||
db()->update('categories', $data, 'category_id = :id', ['id' => $categoryId]);
|
||||
setFlash('success', 'Category updated');
|
||||
} else {
|
||||
$data['category_id'] = generateId('cat_');
|
||||
db()->insert('categories', $data);
|
||||
setFlash('success', 'Category created');
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: /admin/categories.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete' && !empty($_POST['category_id'])) {
|
||||
db()->delete('categories', 'category_id = :id', ['id' => $_POST['category_id']]);
|
||||
setFlash('success', 'Category deleted');
|
||||
header('Location: /admin/categories.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Get categories with product counts
|
||||
$categories = db()->fetchAll(
|
||||
"SELECT c.*,
|
||||
(SELECT COUNT(*) FROM products p WHERE p.category = c.slug OR p.category = c.name) as product_count
|
||||
FROM categories c
|
||||
ORDER BY c.name ASC"
|
||||
);
|
||||
|
||||
// Also get uncategorized products
|
||||
$uncategorizedCount = db()->count('products', "category IS NULL OR category = ''");
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Categories</h1>
|
||||
<button class="btn btn-primary" onclick="openCategoryModal()">
|
||||
<i class="fas fa-plus"></i> Add Category
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Products</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($categories)): ?>
|
||||
<tr><td colspan="5" class="text-muted" style="text-align: center; padding: 2rem;">No categories yet. Create one above.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($cat['name']) ?></strong></td>
|
||||
<td class="text-muted"><?= htmlspecialchars($cat['slug']) ?></td>
|
||||
<td>
|
||||
<?php if ($cat['product_count'] > 0): ?>
|
||||
<a href="/admin/products.php?category=<?= urlencode($cat['slug']) ?>">
|
||||
<?= $cat['product_count'] ?> products
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">0 products</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($cat['is_active']): ?>
|
||||
<span class="badge badge-success">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-error">Hidden</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openCategoryModal(<?= json_encode($cat) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="category_id" value="<?= $cat['category_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this category? Products won't be deleted.">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($uncategorizedCount > 0): ?>
|
||||
<tr style="background: var(--admin-bg);">
|
||||
<td><em>Uncategorized</em></td>
|
||||
<td class="text-muted">-</td>
|
||||
<td>
|
||||
<a href="/admin/products.php?category=uncategorized"><?= $uncategorizedCount ?> products</a>
|
||||
</td>
|
||||
<td><span class="badge badge-warning">Default</span></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Modal -->
|
||||
<div class="modal-overlay" id="categoryModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="categoryModalTitle">Add Category</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('categoryModal')">×</button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="categoryAction" value="create">
|
||||
<input type="hidden" name="category_id" id="categoryId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Category Name *</label>
|
||||
<input type="text" name="name" id="categoryName" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Slug</label>
|
||||
<input type="text" name="slug" id="categorySlug" class="form-input" placeholder="auto-generated if empty">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" id="categoryDesc" class="form-textarea" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="is_active" id="categoryActive" checked>
|
||||
Active (visible on store)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('categoryModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="categorySubmitBtn">Add Category</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openCategoryModal(category = null) {
|
||||
const isEdit = !!category;
|
||||
|
||||
document.getElementById('categoryModalTitle').textContent = isEdit ? 'Edit Category' : 'Add Category';
|
||||
document.getElementById('categorySubmitBtn').textContent = isEdit ? 'Update Category' : 'Add Category';
|
||||
document.getElementById('categoryAction').value = isEdit ? 'update' : 'create';
|
||||
document.getElementById('categoryId').value = isEdit ? category.category_id : '';
|
||||
document.getElementById('categoryName').value = isEdit ? category.name : '';
|
||||
document.getElementById('categorySlug').value = isEdit ? category.slug : '';
|
||||
document.getElementById('categoryDesc').value = isEdit ? (category.description || '') : '';
|
||||
document.getElementById('categoryActive').checked = isEdit ? category.is_active : true;
|
||||
|
||||
Modal.open('categoryModal');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Coupons
|
||||
*/
|
||||
|
||||
$pageTitle = 'Coupons';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create' || $action === 'update') {
|
||||
$couponId = $_POST['coupon_id'] ?? '';
|
||||
$code = strtoupper(trim($_POST['code'] ?? ''));
|
||||
$discountType = $_POST['discount_type'] ?? 'percentage';
|
||||
$discountValue = floatval($_POST['discount_value'] ?? 0);
|
||||
$minOrderAmount = floatval($_POST['min_order_amount'] ?? 0);
|
||||
$maxUses = intval($_POST['max_uses'] ?? 0);
|
||||
$expiresAt = $_POST['expires_at'] ?: null;
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if (empty($code) || $discountValue <= 0) {
|
||||
setFlash('error', 'Code and discount value are required');
|
||||
} else {
|
||||
$data = [
|
||||
'code' => $code,
|
||||
'discount_type' => $discountType,
|
||||
'discount_value' => $discountValue,
|
||||
'min_order_amount' => $minOrderAmount ?: null,
|
||||
'max_uses' => $maxUses ?: null,
|
||||
'expires_at' => $expiresAt,
|
||||
'is_active' => $isActive
|
||||
];
|
||||
|
||||
if ($action === 'update' && $couponId) {
|
||||
db()->update('coupons', $data, 'coupon_id = :id', ['id' => $couponId]);
|
||||
setFlash('success', 'Coupon updated');
|
||||
} else {
|
||||
// Check for duplicate code
|
||||
$existing = db()->fetch("SELECT id FROM coupons WHERE code = :code", ['code' => $code]);
|
||||
if ($existing) {
|
||||
setFlash('error', 'Coupon code already exists');
|
||||
} else {
|
||||
$data['coupon_id'] = generateId('coup_');
|
||||
$data['times_used'] = 0;
|
||||
db()->insert('coupons', $data);
|
||||
setFlash('success', 'Coupon created');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: /admin/coupons.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete' && !empty($_POST['coupon_id'])) {
|
||||
db()->delete('coupons', 'coupon_id = :id', ['id' => $_POST['coupon_id']]);
|
||||
setFlash('success', 'Coupon deleted');
|
||||
header('Location: /admin/coupons.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'toggle' && !empty($_POST['coupon_id'])) {
|
||||
$coupon = db()->fetch("SELECT is_active FROM coupons WHERE coupon_id = :id", ['id' => $_POST['coupon_id']]);
|
||||
if ($coupon) {
|
||||
db()->update('coupons', ['is_active' => !$coupon['is_active']], 'coupon_id = :id', ['id' => $_POST['coupon_id']]);
|
||||
setFlash('success', 'Coupon status updated');
|
||||
}
|
||||
header('Location: /admin/coupons.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Get coupons
|
||||
$coupons = db()->fetchAll("SELECT * FROM coupons ORDER BY created_at DESC");
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Coupons & Discounts</h1>
|
||||
<button class="btn btn-primary" onclick="openCouponModal()">
|
||||
<i class="fas fa-plus"></i> Create Coupon
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Discount</th>
|
||||
<th>Min Order</th>
|
||||
<th>Usage</th>
|
||||
<th>Expires</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($coupons)): ?>
|
||||
<tr><td colspan="7" class="text-muted" style="text-align: center; padding: 2rem;">No coupons created yet</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($coupons as $coupon):
|
||||
$isExpired = $coupon['expires_at'] && strtotime($coupon['expires_at']) < time();
|
||||
$isMaxed = $coupon['max_uses'] && $coupon['times_used'] >= $coupon['max_uses'];
|
||||
?>
|
||||
<tr>
|
||||
<td><strong style="font-family: monospace;"><?= htmlspecialchars($coupon['code']) ?></strong></td>
|
||||
<td>
|
||||
<?php if ($coupon['discount_type'] === 'percentage'): ?>
|
||||
<?= $coupon['discount_value'] ?>% off
|
||||
<?php else: ?>
|
||||
<?= formatCurrency($coupon['discount_value']) ?> off
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $coupon['min_order_amount'] ? formatCurrency($coupon['min_order_amount']) : '-' ?></td>
|
||||
<td>
|
||||
<?= $coupon['times_used'] ?><?= $coupon['max_uses'] ? '/' . $coupon['max_uses'] : '' ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($coupon['expires_at']): ?>
|
||||
<?php if ($isExpired): ?>
|
||||
<span class="text-error"><?= formatDate($coupon['expires_at']) ?></span>
|
||||
<?php else: ?>
|
||||
<?= formatDate($coupon['expires_at']) ?>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">Never</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!$coupon['is_active']): ?>
|
||||
<span class="badge badge-error">Disabled</span>
|
||||
<?php elseif ($isExpired): ?>
|
||||
<span class="badge badge-warning">Expired</span>
|
||||
<?php elseif ($isMaxed): ?>
|
||||
<span class="badge badge-warning">Maxed Out</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-success">Active</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openCouponModal(<?= json_encode($coupon) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="toggle">
|
||||
<input type="hidden" name="coupon_id" value="<?= $coupon['coupon_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">
|
||||
<i class="fas fa-<?= $coupon['is_active'] ? 'ban' : 'check' ?>"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="coupon_id" value="<?= $coupon['coupon_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this coupon?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coupon Modal -->
|
||||
<div class="modal-overlay" id="couponModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="couponModalTitle">Create Coupon</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('couponModal')">×</button>
|
||||
</div>
|
||||
<form method="POST" id="couponForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="couponAction" value="create">
|
||||
<input type="hidden" name="coupon_id" id="couponId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Coupon Code *</label>
|
||||
<input type="text" name="code" id="couponCode" class="form-input" required style="text-transform: uppercase;">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Discount Type</label>
|
||||
<select name="discount_type" id="couponType" class="form-select">
|
||||
<option value="percentage">Percentage (%)</option>
|
||||
<option value="fixed">Fixed Amount ($)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Discount Value *</label>
|
||||
<input type="number" name="discount_value" id="couponValue" class="form-input" step="0.01" min="0" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Min Order Amount</label>
|
||||
<input type="number" name="min_order_amount" id="couponMinOrder" class="form-input" step="0.01" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Max Uses (0 = unlimited)</label>
|
||||
<input type="number" name="max_uses" id="couponMaxUses" class="form-input" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Expiration Date</label>
|
||||
<input type="date" name="expires_at" id="couponExpires" class="form-input">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="is_active" id="couponActive" value="1" checked>
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('couponModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="couponSubmitBtn">Create Coupon</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openCouponModal(coupon = null) {
|
||||
const isEdit = !!coupon;
|
||||
|
||||
document.getElementById('couponModalTitle').textContent = isEdit ? 'Edit Coupon' : 'Create Coupon';
|
||||
document.getElementById('couponSubmitBtn').textContent = isEdit ? 'Update Coupon' : 'Create Coupon';
|
||||
document.getElementById('couponAction').value = isEdit ? 'update' : 'create';
|
||||
document.getElementById('couponId').value = isEdit ? coupon.coupon_id : '';
|
||||
document.getElementById('couponCode').value = isEdit ? coupon.code : '';
|
||||
document.getElementById('couponType').value = isEdit ? coupon.discount_type : 'percentage';
|
||||
document.getElementById('couponValue').value = isEdit ? coupon.discount_value : '';
|
||||
document.getElementById('couponMinOrder').value = isEdit && coupon.min_order_amount ? coupon.min_order_amount : '';
|
||||
document.getElementById('couponMaxUses').value = isEdit && coupon.max_uses ? coupon.max_uses : '';
|
||||
document.getElementById('couponExpires').value = isEdit && coupon.expires_at ? coupon.expires_at.split(' ')[0] : '';
|
||||
document.getElementById('couponActive').checked = isEdit ? coupon.is_active : true;
|
||||
|
||||
Modal.open('couponModal');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Customers Management
|
||||
*/
|
||||
|
||||
$pageTitle = 'Customers';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$email = strtolower(trim($_POST['email'] ?? ''));
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$walletBalance = floatval($_POST['wallet_balance'] ?? 0);
|
||||
$rewardPoints = intval($_POST['reward_points'] ?? 0);
|
||||
|
||||
if (empty($email)) {
|
||||
setFlash('error', 'Email is required');
|
||||
} else {
|
||||
$existing = db()->fetch("SELECT id FROM customers WHERE email = :email", ['email' => $email]);
|
||||
if ($existing) {
|
||||
setFlash('error', 'Customer with this email already exists');
|
||||
} else {
|
||||
db()->insert('customers', [
|
||||
'customer_id' => generateId('cust_'),
|
||||
'email' => $email,
|
||||
'name' => $name ?: null,
|
||||
'phone' => $phone ?: null,
|
||||
'wallet_balance' => $walletBalance,
|
||||
'reward_points' => $rewardPoints,
|
||||
'is_active' => 1
|
||||
]);
|
||||
setFlash('success', 'Customer created successfully');
|
||||
}
|
||||
}
|
||||
header('Location: /admin/customers.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'update' && !empty($_POST['customer_id'])) {
|
||||
$customerId = $_POST['customer_id'];
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
$walletBalance = floatval($_POST['wallet_balance'] ?? 0);
|
||||
$rewardPoints = intval($_POST['reward_points'] ?? 0);
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
db()->update('customers', [
|
||||
'name' => $name ?: null,
|
||||
'phone' => $phone ?: null,
|
||||
'wallet_balance' => $walletBalance,
|
||||
'reward_points' => $rewardPoints,
|
||||
'is_active' => $isActive
|
||||
], 'customer_id = :id', ['id' => $customerId]);
|
||||
|
||||
setFlash('success', 'Customer updated successfully');
|
||||
header('Location: /admin/customers.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete' && !empty($_POST['customer_id'])) {
|
||||
db()->delete('customers', 'customer_id = :id', ['id' => $_POST['customer_id']]);
|
||||
setFlash('success', 'Customer deleted');
|
||||
header('Location: /admin/customers.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'adjust_wallet' && !empty($_POST['customer_id'])) {
|
||||
$amount = floatval($_POST['amount'] ?? 0);
|
||||
$reason = trim($_POST['reason'] ?? '');
|
||||
|
||||
if ($amount != 0) {
|
||||
db()->query(
|
||||
"UPDATE customers SET wallet_balance = wallet_balance + :amt WHERE customer_id = :id",
|
||||
['amt' => $amount, 'id' => $_POST['customer_id']]
|
||||
);
|
||||
|
||||
// Log transaction
|
||||
db()->insert('wallet_transactions', [
|
||||
'transaction_id' => generateId('wt_'),
|
||||
'customer_id' => $_POST['customer_id'],
|
||||
'amount' => $amount,
|
||||
'type' => $amount > 0 ? 'deposit' : 'withdrawal',
|
||||
'description' => $reason ?: 'Admin adjustment',
|
||||
'balance_after' => db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $_POST['customer_id']])['wallet_balance'] ?? 0
|
||||
]);
|
||||
|
||||
setFlash('success', 'Wallet adjusted by $' . number_format($amount, 2));
|
||||
}
|
||||
header('Location: /admin/customers.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
$search = $_GET['search'] ?? '';
|
||||
$status = $_GET['status'] ?? '';
|
||||
$page = max(1, intval($_GET['page'] ?? 1));
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$where[] = '(email LIKE :search OR name LIKE :search OR phone LIKE :search)';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ($status === 'active') {
|
||||
$where[] = 'is_active = 1';
|
||||
} elseif ($status === 'inactive') {
|
||||
$where[] = 'is_active = 0';
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $where);
|
||||
$total = db()->count('customers', $whereClause, $params);
|
||||
$pagination = paginate($total, $page, ADMIN_ITEMS_PER_PAGE);
|
||||
|
||||
$customers = db()->fetchAll(
|
||||
"SELECT c.customer_id, c.email, c.name, c.phone, c.wallet_balance, c.reward_points, c.is_active, c.created_at,
|
||||
COALESCE((SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.customer_id), 0) as order_count,
|
||||
COALESCE((SELECT SUM(total) FROM orders o WHERE o.customer_id = c.customer_id AND o.payment_status = 'paid'), 0) as total_spent
|
||||
FROM customers c
|
||||
WHERE {$whereClause}
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT " . (int)$pagination['per_page'] . " OFFSET " . (int)$pagination['offset'],
|
||||
$params
|
||||
);
|
||||
|
||||
// Stats
|
||||
$totalCustomers = db()->count('customers');
|
||||
$activeCustomers = db()->count('customers', 'is_active = 1');
|
||||
$totalWalletBalance = (float)(db()->fetch("SELECT COALESCE(SUM(wallet_balance),0) as total FROM customers")['total'] ?? 0);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Customers</h1>
|
||||
<button class="btn btn-primary" onclick="openCustomerModal()">
|
||||
<i class="fas fa-plus"></i> Add Customer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary"><i class="fas fa-users"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= $totalCustomers ?></div>
|
||||
<div class="stat-card-label">Total Customers</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success"><i class="fas fa-user-check"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= $activeCustomers ?></div>
|
||||
<div class="stat-card-label">Active</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon warning"><i class="fas fa-wallet"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= formatCurrency($totalWalletBalance) ?></div>
|
||||
<div class="stat-card-label">Total Wallet Balance</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: end;">
|
||||
<div class="form-group mb-0" style="flex: 1; min-width: 200px;">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" name="search" class="form-input" placeholder="Email, name, phone..." value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All</option>
|
||||
<option value="active" <?= $status === 'active' ? 'selected' : '' ?>>Active</option>
|
||||
<option value="inactive" <?= $status === 'inactive' ? 'selected' : '' ?>>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> Filter</button>
|
||||
<?php if ($search || $status): ?>
|
||||
<a href="/admin/customers.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<span><?= $total ?> customers found</span>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>Phone</th>
|
||||
<th>Orders</th>
|
||||
<th>Total Spent</th>
|
||||
<th>Wallet</th>
|
||||
<th>Points</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($customers)): ?>
|
||||
<tr><td colspan="8" class="text-muted" style="text-align: center; padding: 2rem;">No customers found</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($customers as $customer): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($customer['name'] ?? 'No Name') ?></strong><br>
|
||||
<small class="text-muted"><?= htmlspecialchars($customer['email']) ?></small>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($customer['phone'] ?? '-') ?></td>
|
||||
<td><?= $customer['order_count'] ?? 0 ?></td>
|
||||
<td><?= formatCurrency($customer['total_spent'] ?? 0) ?></td>
|
||||
<td>
|
||||
<strong class="<?= ($customer['wallet_balance'] ?? 0) > 0 ? 'text-success' : '' ?>">
|
||||
<?= formatCurrency($customer['wallet_balance'] ?? 0) ?>
|
||||
</strong>
|
||||
</td>
|
||||
<td><?= $customer['reward_points'] ?? 0 ?></td>
|
||||
<td>
|
||||
<?php if ($customer['is_active']): ?>
|
||||
<span class="badge badge-success">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-error">Inactive</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openCustomerModal(<?= json_encode($customer) ?>)' title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openWalletModal("<?= $customer['customer_id'] ?>", "<?= htmlspecialchars($customer['name'] ?? $customer['email']) ?>", <?= $customer['wallet_balance'] ?? 0 ?>)' title="Adjust Wallet">
|
||||
<i class="fas fa-wallet"></i>
|
||||
</button>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="customer_id" value="<?= $customer['customer_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this customer?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($pagination['total_pages'] > 1): ?>
|
||||
<div style="display: flex; justify-content: center; margin-top: 1rem;">
|
||||
<?= renderPagination($pagination, '/admin/customers.php?search=' . urlencode($search) . '&status=' . $status) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Customer Modal -->
|
||||
<div class="modal-overlay" id="customerModal">
|
||||
<div class="modal" style="max-width:680px;width:95vw">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="customerModalTitle">Add Customer</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('customerModal')">×</button>
|
||||
</div>
|
||||
<div id="customerTabs" style="display:none;border-bottom:1px solid var(--color-border);padding:0 1.5rem">
|
||||
<button type="button" onclick="switchCTab('details')" id="ctab-details"
|
||||
style="padding:.6rem 1rem;border:none;border-bottom:2px solid var(--color-primary);background:none;cursor:pointer;font-weight:600;color:var(--color-primary);margin-bottom:-1px">
|
||||
<i class="fas fa-user"></i> Details
|
||||
</button>
|
||||
<button type="button" onclick="switchCTab('orders')" id="ctab-orders"
|
||||
style="padding:.6rem 1rem;border:none;border-bottom:2px solid transparent;background:none;cursor:pointer;font-weight:600;color:var(--color-text-muted);margin-bottom:-1px">
|
||||
<i class="fas fa-shopping-bag"></i> Orders
|
||||
<span id="cOrderBadge" style="background:var(--color-border);border-radius:10px;padding:1px 7px;font-size:.75rem;margin-left:3px"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="cpanel-details">
|
||||
<form method="POST" id="customerForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="customerAction" value="create">
|
||||
<input type="hidden" name="customer_id" id="customerId">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email *</label>
|
||||
<input type="email" name="email" id="customerEmail" class="form-input" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" name="name" id="customerName" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="tel" name="phone" id="customerPhone" class="form-input">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Wallet Balance ($)</label>
|
||||
<input type="number" name="wallet_balance" id="customerWallet" class="form-input" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Reward Points</label>
|
||||
<input type="number" name="reward_points" id="customerPoints" class="form-input" min="0" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-0" id="statusGroup" style="display:none">
|
||||
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
|
||||
<input type="checkbox" name="is_active" id="customerActive" checked>
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('customerModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="customerSubmitBtn">Add Customer</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="cpanel-orders" style="display:none">
|
||||
<div class="modal-body" style="padding:0;max-height:440px;overflow-y:auto">
|
||||
<div id="cOrdLoading" style="text-align:center;padding:2rem;color:var(--color-text-muted)">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading orders...
|
||||
</div>
|
||||
<div id="cOrdContent" style="display:none">
|
||||
<div id="cOrdEmpty" style="display:none;text-align:center;padding:2rem;color:var(--color-text-muted)">
|
||||
<i class="fas fa-shopping-bag" style="font-size:2rem;opacity:.3;display:block;margin-bottom:.5rem"></i>
|
||||
No orders yet
|
||||
</div>
|
||||
<table id="cOrdTable" style="width:100%;border-collapse:collapse;display:none">
|
||||
<thead>
|
||||
<tr style="background:var(--color-surface)">
|
||||
<th style="padding:.6rem 1rem;font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;text-align:left">Order</th>
|
||||
<th style="padding:.6rem 1rem;font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;text-align:left">Date</th>
|
||||
<th style="padding:.6rem 1rem;font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;text-align:left">Total</th>
|
||||
<th style="padding:.6rem 1rem;font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;text-align:left">Status</th>
|
||||
<th style="padding:.6rem 1rem"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cOrdBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('customerModal')">Close</button>
|
||||
<a id="cOrdViewAll" href="/admin/orders.php" class="btn btn-primary" target="_blank">View All Orders</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.cord-row td{padding:.65rem 1rem;border-bottom:1px solid var(--color-border);font-size:.875rem;vertical-align:middle}
|
||||
.cord-row:hover td{background:var(--color-surface)}
|
||||
.cord-detail{display:none}
|
||||
.cord-detail.open{display:table-row}
|
||||
.cord-detail td{padding:.75rem 1rem 1rem 1.5rem;border-bottom:1px solid var(--color-border);background:var(--color-surface)}
|
||||
</style>
|
||||
<script>
|
||||
// switchCTab defined above
|
||||
function _switchCTabDummy(tab){
|
||||
['details','orders'].forEach(function(t){
|
||||
document.getElementById('ctab-'+t).style.borderBottomColor=t===tab?'var(--color-primary)':'transparent';
|
||||
document.getElementById('ctab-'+t).style.color=t===tab?'var(--color-primary)':'var(--color-text-muted)';
|
||||
document.getElementById('cpanel-'+t).style.display=t===tab?'':'none';
|
||||
});
|
||||
if(tab==='orders'&&_cCustId&&!_cLoaded)loadCOrders(_cCustId);
|
||||
}
|
||||
function loadCOrders(cid){
|
||||
_cLoaded=true;
|
||||
document.getElementById('cOrdLoading').style.display='block';
|
||||
document.getElementById('cOrdContent').style.display='none';
|
||||
fetch('/admin/api/customer-orders.php?customer_id='+encodeURIComponent(cid))
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(data){
|
||||
document.getElementById('cOrdLoading').style.display='none';
|
||||
document.getElementById('cOrdContent').style.display='block';
|
||||
var orders=data.orders||[];
|
||||
document.getElementById('cOrderBadge').textContent=orders.length;
|
||||
if(!orders.length){
|
||||
document.getElementById('cOrdEmpty').style.display='block';
|
||||
document.getElementById('cOrdTable').style.display='none';
|
||||
return;
|
||||
}
|
||||
document.getElementById('cOrdEmpty').style.display='none';
|
||||
document.getElementById('cOrdTable').style.display='table';
|
||||
var tb=document.getElementById('cOrdBody');
|
||||
tb.innerHTML='';
|
||||
var sc={pending:'warning',processing:'primary',confirmed:'primary',shipped:'primary',delivered:'success',cancelled:'error',refunded:'error'};
|
||||
orders.forEach(function(o){
|
||||
var tr=document.createElement('tr');
|
||||
tr.className='cord-row';
|
||||
tr.innerHTML='<td><strong>#'+o.order_number+'</strong></td>'+
|
||||
'<td>'+new Date(o.created_at).toLocaleDateString()+'</td>'+
|
||||
'<td><strong>$'+parseFloat(o.total).toFixed(2)+'</strong></td>'+
|
||||
'<td><span class="badge badge-'+(sc[o.order_status]||'primary')+'">'+o.order_status+'</span></td>'+
|
||||
'<td><button type="button" class="btn btn-sm btn-secondary" onclick="toggleCOrd(''+o.order_id+'')">'+
|
||||
'<i class="fas fa-chevron-down" id="cico-'+o.order_id+'"></i></button></td>';
|
||||
tb.appendChild(tr);
|
||||
var items=[];
|
||||
try{items=JSON.parse(o.items||'[]');}catch(e){}
|
||||
var html='<div style="font-weight:600;margin-bottom:.4rem">Items</div>';
|
||||
if(items.length){
|
||||
items.forEach(function(it){
|
||||
html+='<div style="display:flex;justify-content:space-between;padding:.2rem 0;font-size:.8rem;border-bottom:1px solid var(--color-border)">'+
|
||||
'<span>'+(it.name||it.product_name||'Item')+' × '+(it.quantity||1)+'</span>'+
|
||||
'<span>$'+parseFloat(it.total||it.price||0).toFixed(2)+'</span></div>';
|
||||
});
|
||||
}else{html+='<div style="color:var(--color-text-muted);font-size:.8rem">No item details available</div>';}
|
||||
html+='<div style="display:flex;justify-content:space-between;font-weight:700;margin-top:.4rem;padding-top:.4rem;border-top:2px solid var(--color-border)">'+
|
||||
'<span>Total</span><span>$'+parseFloat(o.total).toFixed(2)+'</span></div>';
|
||||
if(o.tracking_number)html+='<div style="margin-top:.4rem;font-size:.8rem"><strong>Tracking:</strong> '+o.tracking_number+'</div>';
|
||||
html+='<div style="margin-top:.6rem"><a href="/admin/order.php?id='+o.order_id+'" class="btn btn-sm btn-primary" target="_blank">'+
|
||||
'<i class="fas fa-external-link-alt"></i> Open Order</a></div>';
|
||||
var dr=document.createElement('tr');
|
||||
dr.className='cord-detail';
|
||||
dr.id='cdet-'+o.order_id;
|
||||
dr.innerHTML='<td colspan="5">'+html+'</td>';
|
||||
tb.appendChild(dr);
|
||||
});
|
||||
})
|
||||
.catch(function(){
|
||||
document.getElementById('cOrdLoading').innerHTML='<div style="color:var(--color-error);text-align:center;padding:1rem"><i class="fas fa-exclamation-circle"></i> Failed to load orders</div>';
|
||||
});
|
||||
}
|
||||
function toggleCOrd(id){
|
||||
var d=document.getElementById('cdet-'+id);
|
||||
var i=document.getElementById('cico-'+id);
|
||||
d.classList.toggle('open');
|
||||
i.className=d.classList.contains('open')?'fas fa-chevron-up':'fas fa-chevron-down';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="modal-overlay" id="walletModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Adjust Wallet Balance</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('walletModal')">×</button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="adjust_wallet">
|
||||
<input type="hidden" name="customer_id" id="walletCustomerId">
|
||||
|
||||
<p>Customer: <strong id="walletCustomerName"></strong></p>
|
||||
<p>Current Balance: <strong id="walletCurrentBalance"></strong></p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Adjustment Amount</label>
|
||||
<input type="number" name="amount" class="form-input" step="0.01" required placeholder="Use negative to deduct">
|
||||
<small class="text-muted">Positive to add, negative to subtract</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Reason</label>
|
||||
<input type="text" name="reason" class="form-input" placeholder="e.g., Refund, Bonus, Correction">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('walletModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Apply Adjustment</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var _cCustId = null;
|
||||
var _cLoaded = false;
|
||||
|
||||
function switchCTab(tab) {
|
||||
['details','orders'].forEach(function(t) {
|
||||
var tabBtn = document.getElementById('ctab-' + t);
|
||||
var panel = document.getElementById('cpanel-' + t);
|
||||
if (tabBtn) {
|
||||
tabBtn.style.borderBottomColor = t === tab ? 'var(--color-primary)' : 'transparent';
|
||||
tabBtn.style.color = t === tab ? 'var(--color-primary)' : 'var(--color-text-muted)';
|
||||
}
|
||||
if (panel) panel.style.display = t === tab ? '' : 'none';
|
||||
});
|
||||
if (tab === 'orders' && _cCustId && !_cLoaded) loadCOrders(_cCustId);
|
||||
}
|
||||
|
||||
|
||||
function openCustomerModal(customer = null) {
|
||||
const isEdit = !!customer;
|
||||
_cCustId = isEdit ? customer.customer_id : null;
|
||||
_cLoaded = false;
|
||||
document.getElementById('customerModalTitle').textContent = isEdit ? 'Edit Customer' : 'Add Customer';
|
||||
document.getElementById('customerSubmitBtn').textContent = isEdit ? 'Update Customer' : 'Add Customer';
|
||||
document.getElementById('customerAction').value = isEdit ? 'update' : 'create';
|
||||
document.getElementById('customerId').value = isEdit ? customer.customer_id : '';
|
||||
document.getElementById('customerEmail').value = isEdit ? customer.email : '';
|
||||
document.getElementById('customerEmail').readOnly = isEdit;
|
||||
document.getElementById('customerName').value = isEdit ? (customer.name || '') : '';
|
||||
document.getElementById('customerPhone').value = isEdit ? (customer.phone || '') : '';
|
||||
document.getElementById('customerWallet').value = isEdit ? (customer.wallet_balance || 0) : 0;
|
||||
document.getElementById('customerPoints').value = isEdit ? (customer.reward_points || 0) : 0;
|
||||
document.getElementById('customerActive').checked = isEdit ? customer.is_active : true;
|
||||
document.getElementById('statusGroup').style.display = isEdit ? 'block' : 'none';
|
||||
document.getElementById('customerTabs').style.display = isEdit ? 'flex' : 'none';
|
||||
document.getElementById('cOrderBadge').textContent = '';
|
||||
switchCTab('details');
|
||||
Modal.open('customerModal');
|
||||
}
|
||||
|
||||
function openWalletModal(customerId, name, balance) {
|
||||
document.getElementById('walletCustomerId').value = customerId;
|
||||
document.getElementById('walletCustomerName').textContent = name;
|
||||
document.getElementById('walletCurrentBalance').textContent = '$' + parseFloat(balance).toFixed(2);
|
||||
Modal.open('walletModal');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Email Settings
|
||||
*/
|
||||
|
||||
$pageTitle = 'Email Settings';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$section = $_POST['section'] ?? '';
|
||||
|
||||
if ($section === 'sendgrid') {
|
||||
setSetting('email_sendgrid', [
|
||||
'api_key' => trim($_POST['sendgrid_api_key'] ?? ''),
|
||||
'from_email' => trim($_POST['from_email'] ?? ''),
|
||||
'from_name' => trim($_POST['from_name'] ?? '')
|
||||
]);
|
||||
setFlash('success', 'SendGrid settings updated');
|
||||
}
|
||||
|
||||
if ($section === 'notifications') {
|
||||
setSetting('email_notifications', [
|
||||
'order_confirmation' => isset($_POST['notif_order_confirmation']),
|
||||
'order_shipped' => isset($_POST['notif_order_shipped']),
|
||||
'order_delivered' => isset($_POST['notif_order_delivered']),
|
||||
'abandoned_cart' => isset($_POST['notif_abandoned_cart']),
|
||||
'low_stock' => isset($_POST['notif_low_stock']),
|
||||
'admin_new_order' => isset($_POST['notif_admin_new_order']),
|
||||
'admin_email' => trim($_POST['admin_email'] ?? '')
|
||||
]);
|
||||
setFlash('success', 'Notification settings updated');
|
||||
}
|
||||
|
||||
if ($section === 'test') {
|
||||
$testEmail = trim($_POST['test_email'] ?? '');
|
||||
if ($testEmail && filter_var($testEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$sent = sendEmail($testEmail, 'Test Email from Tom\'s Java Jive',
|
||||
'<div style="font-family: Arial; padding: 20px;"><h2>Test Email</h2><p>If you received this, your email settings are working correctly!</p></div>'
|
||||
);
|
||||
if ($sent) {
|
||||
setFlash('success', 'Test email sent to ' . $testEmail);
|
||||
} else {
|
||||
setFlash('error', 'Failed to send test email. Check your SendGrid settings.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: /admin/emails.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$sendgrid = getSetting('email_sendgrid', [
|
||||
'api_key' => '',
|
||||
'from_email' => '',
|
||||
'from_name' => "Tom's Java Jive"
|
||||
]);
|
||||
|
||||
$notifications = getSetting('email_notifications', [
|
||||
'order_confirmation' => true,
|
||||
'order_shipped' => true,
|
||||
'order_delivered' => true,
|
||||
'abandoned_cart' => false,
|
||||
'low_stock' => true,
|
||||
'admin_new_order' => true,
|
||||
'admin_email' => ''
|
||||
]);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Email Settings</h1>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 200px 1fr; gap: 1.5rem;">
|
||||
<div>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0.5rem;">
|
||||
<a href="/admin/settings.php" class="nav-item"><i class="fas fa-store"></i> General</a>
|
||||
<a href="/admin/shipping.php" class="nav-item"><i class="fas fa-truck"></i> Shipping</a>
|
||||
<a href="/admin/payments.php" class="nav-item"><i class="fas fa-credit-card"></i> Payments</a>
|
||||
<a href="/admin/emails.php" class="nav-item active"><i class="fas fa-envelope"></i> Emails</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- SendGrid Settings -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="sendgrid">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title"><i class="fas fa-paper-plane"></i> SendGrid Configuration</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">SendGrid API Key</label>
|
||||
<input type="password" name="sendgrid_api_key" class="form-input"
|
||||
value="<?= htmlspecialchars($sendgrid['api_key']) ?>"
|
||||
placeholder="SG.xxxx...">
|
||||
<small class="text-muted">Get this from <a href="https://app.sendgrid.com/settings/api_keys" target="_blank">SendGrid Dashboard</a></small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">From Email</label>
|
||||
<input type="email" name="from_email" class="form-input"
|
||||
value="<?= htmlspecialchars($sendgrid['from_email']) ?>"
|
||||
placeholder="noreply@yourdomain.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">From Name</label>
|
||||
<input type="text" name="from_name" class="form-input"
|
||||
value="<?= htmlspecialchars($sendgrid['from_name']) ?>"
|
||||
placeholder="Tom's Java Jive">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save SendGrid Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Notification Settings -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="notifications">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Email Notifications</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<h4 style="margin-bottom: 1rem; font-size: 0.9rem; color: var(--admin-text-muted);">Customer Notifications</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="notif_order_confirmation" <?= $notifications['order_confirmation'] ? 'checked' : '' ?>>
|
||||
Order confirmation email
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="notif_order_shipped" <?= $notifications['order_shipped'] ? 'checked' : '' ?>>
|
||||
Order shipped notification
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="notif_order_delivered" <?= $notifications['order_delivered'] ? 'checked' : '' ?>>
|
||||
Order delivered notification
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="notif_abandoned_cart" <?= $notifications['abandoned_cart'] ? 'checked' : '' ?>>
|
||||
Abandoned cart reminders
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr style="margin: 1.5rem 0;">
|
||||
<h4 style="margin-bottom: 1rem; font-size: 0.9rem; color: var(--admin-text-muted);">Admin Notifications</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="notif_admin_new_order" <?= $notifications['admin_new_order'] ? 'checked' : '' ?>>
|
||||
New order notification
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="notif_low_stock" <?= $notifications['low_stock'] ? 'checked' : '' ?>>
|
||||
Low stock alerts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Admin Email Address</label>
|
||||
<input type="email" name="admin_email" class="form-input"
|
||||
value="<?= htmlspecialchars($notifications['admin_email']) ?>"
|
||||
placeholder="admin@yourdomain.com">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2">Save Notification Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Test Email -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="test">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Test Email</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Send test email to:</label>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<input type="email" name="test_email" class="form-input" placeholder="your@email.com" required>
|
||||
<button type="submit" class="btn btn-secondary">Send Test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Gift Cards
|
||||
*/
|
||||
|
||||
$pageTitle = 'Gift Cards';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$initialBalance = floatval($_POST['initial_balance'] ?? 0);
|
||||
$recipientEmail = trim($_POST['recipient_email'] ?? '');
|
||||
$recipientName = trim($_POST['recipient_name'] ?? '');
|
||||
$message = trim($_POST['message'] ?? '');
|
||||
$purchaserEmail = trim($_POST['purchaser_email'] ?? '');
|
||||
|
||||
if ($initialBalance > 0) {
|
||||
$code = strtoupper('GC' . bin2hex(random_bytes(4)));
|
||||
|
||||
db()->insert('gift_cards', [
|
||||
'gift_card_id' => generateId('gc_'),
|
||||
'code' => $code,
|
||||
'initial_balance' => $initialBalance,
|
||||
'current_balance' => $initialBalance,
|
||||
'purchaser_email' => $purchaserEmail ?: null,
|
||||
'recipient_email' => $recipientEmail ?: null,
|
||||
'recipient_name' => $recipientName ?: null,
|
||||
'message' => $message ?: null,
|
||||
'is_active' => 1
|
||||
]);
|
||||
|
||||
setFlash('success', "Gift card created! Code: $code");
|
||||
|
||||
// Send email if recipient provided
|
||||
if ($recipientEmail) {
|
||||
$html = <<<HTML
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
|
||||
<h1 style="margin: 0;">You've Received a Gift!</h1>
|
||||
</div>
|
||||
<div style="padding: 30px; background: #FDFBF7; text-align: center;">
|
||||
<p>Hi{$recipientName},</p>
|
||||
<p>You've received a Tom's Java Jive gift card!</p>
|
||||
<div style="background: white; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="font-size: 24px; font-weight: bold; color: #E86A33;">$initialBalance</p>
|
||||
<p style="font-size: 18px; letter-spacing: 3px;">$code</p>
|
||||
</div>
|
||||
<?php if ($message): ?><p style='font-style: italic;'>\<?php endif; ?>
|
||||
<a href="https://tomsjavajive.com/shop.php" style="display: inline-block; background: #E86A33; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; margin-top: 20px;">Shop Now</a>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
sendEmail($recipientEmail, "You've Received a Gift Card!", $html);
|
||||
}
|
||||
} else {
|
||||
setFlash('error', 'Invalid balance amount');
|
||||
}
|
||||
|
||||
header('Location: /admin/gift-cards.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'toggle' && !empty($_POST['gift_card_id'])) {
|
||||
$gc = db()->fetch("SELECT is_active FROM gift_cards WHERE gift_card_id = :id", ['id' => $_POST['gift_card_id']]);
|
||||
if ($gc) {
|
||||
db()->update('gift_cards', ['is_active' => !$gc['is_active']], 'gift_card_id = :id', ['id' => $_POST['gift_card_id']]);
|
||||
setFlash('success', 'Gift card status updated');
|
||||
}
|
||||
header('Location: /admin/gift-cards.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'adjust' && !empty($_POST['gift_card_id'])) {
|
||||
$amount = floatval($_POST['amount'] ?? 0);
|
||||
if ($amount != 0) {
|
||||
db()->query(
|
||||
"UPDATE gift_cards SET current_balance = current_balance + :amt WHERE gift_card_id = :id",
|
||||
['amt' => $amount, 'id' => $_POST['gift_card_id']]
|
||||
);
|
||||
setFlash('success', 'Balance adjusted');
|
||||
}
|
||||
header('Location: /admin/gift-cards.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
$search = $_GET['search'] ?? '';
|
||||
$status = $_GET['status'] ?? '';
|
||||
$page = max(1, intval($_GET['page'] ?? 1));
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$where[] = '(code LIKE :search OR recipient_email LIKE :search)';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ($status === 'active') {
|
||||
$where[] = 'is_active = 1 AND current_balance > 0';
|
||||
} elseif ($status === 'depleted') {
|
||||
$where[] = 'current_balance <= 0';
|
||||
} elseif ($status === 'disabled') {
|
||||
$where[] = 'is_active = 0';
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $where);
|
||||
$total = db()->count('gift_cards', $whereClause, $params);
|
||||
$pagination = paginate($total, $page, ADMIN_ITEMS_PER_PAGE);
|
||||
|
||||
$giftCards = db()->fetchAll(
|
||||
"SELECT * FROM gift_cards WHERE {$whereClause} ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
array_merge($params, ['limit' => $pagination['per_page'], 'offset' => $pagination['offset']])
|
||||
);
|
||||
|
||||
// Stats
|
||||
$totalValue = db()->fetch("SELECT SUM(current_balance) as total FROM gift_cards WHERE is_active = 1")['total'] ?? 0;
|
||||
$activeCount = db()->count('gift_cards', 'is_active = 1 AND current_balance > 0');
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Gift Cards</h1>
|
||||
<button class="btn btn-primary" onclick="Modal.open('createModal')">
|
||||
<i class="fas fa-plus"></i> Create Gift Card
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid" style="margin-bottom: 1.5rem;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary"><i class="fas fa-gift"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= $activeCount ?></div>
|
||||
<div class="stat-card-label">Active Cards</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success"><i class="fas fa-dollar-sign"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= formatCurrency($totalValue) ?></div>
|
||||
<div class="stat-card-label">Outstanding Value</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: end;">
|
||||
<div class="form-group mb-0" style="flex: 1; min-width: 200px;">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" name="search" class="form-input" placeholder="Code or email..." value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All</option>
|
||||
<option value="active" <?= $status === 'active' ? 'selected' : '' ?>>Active</option>
|
||||
<option value="depleted" <?= $status === 'depleted' ? 'selected' : '' ?>>Depleted</option>
|
||||
<option value="disabled" <?= $status === 'disabled' ? 'selected' : '' ?>>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> Filter</button>
|
||||
<?php if ($search || $status): ?>
|
||||
<a href="/admin/gift-cards.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Recipient</th>
|
||||
<th>Initial</th>
|
||||
<th>Balance</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($giftCards)): ?>
|
||||
<tr><td colspan="7" class="text-muted" style="text-align: center; padding: 2rem;">No gift cards found</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($giftCards as $gc): ?>
|
||||
<tr>
|
||||
<td><strong style="font-family: monospace;"><?= htmlspecialchars($gc['code']) ?></strong></td>
|
||||
<td><?= htmlspecialchars($gc['recipient_email'] ?? $gc['recipient_name'] ?? '-') ?></td>
|
||||
<td><?= formatCurrency($gc['initial_balance']) ?></td>
|
||||
<td>
|
||||
<?php if ($gc['current_balance'] <= 0): ?>
|
||||
<span class="badge badge-error">$0.00</span>
|
||||
<?php else: ?>
|
||||
<strong class="text-success"><?= formatCurrency($gc['current_balance']) ?></strong>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (!$gc['is_active']): ?>
|
||||
<span class="badge badge-error">Disabled</span>
|
||||
<?php elseif ($gc['current_balance'] <= 0): ?>
|
||||
<span class="badge badge-warning">Depleted</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-success">Active</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-muted"><?= formatDate($gc['created_at']) ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick="openAdjustModal('<?= $gc['gift_card_id'] ?>', '<?= $gc['code'] ?>', <?= $gc['current_balance'] ?>)">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="toggle">
|
||||
<input type="hidden" name="gift_card_id" value="<?= $gc['gift_card_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-secondary" title="<?= $gc['is_active'] ? 'Disable' : 'Enable' ?>">
|
||||
<i class="fas fa-<?= $gc['is_active'] ? 'ban' : 'check' ?>"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($pagination['total_pages'] > 1): ?>
|
||||
<div style="display: flex; justify-content: center; margin-top: 1rem;">
|
||||
<?= renderPagination($pagination, '/admin/gift-cards.php?search=' . urlencode($search) . '&status=' . $status) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div class="modal-overlay" id="createModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Create Gift Card</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('createModal')">×</button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Amount *</label>
|
||||
<input type="number" name="initial_balance" class="form-input" step="0.01" min="1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Recipient Email (optional)</label>
|
||||
<input type="email" name="recipient_email" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Recipient Name (optional)</label>
|
||||
<input type="text" name="recipient_name" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Personal Message (optional)</label>
|
||||
<textarea name="message" class="form-textarea" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Purchaser Email (optional)</label>
|
||||
<input type="email" name="purchaser_email" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('createModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Gift Card</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adjust Modal -->
|
||||
<div class="modal-overlay" id="adjustModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Adjust Balance</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('adjustModal')">×</button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="adjust">
|
||||
<input type="hidden" name="gift_card_id" id="adjust-gc-id">
|
||||
<p>Card: <strong id="adjust-gc-code"></strong></p>
|
||||
<p>Current Balance: <strong id="adjust-gc-balance"></strong></p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Adjustment Amount</label>
|
||||
<input type="number" name="amount" class="form-input" step="0.01" placeholder="Use negative to deduct">
|
||||
<small class="text-muted">Enter positive to add, negative to subtract</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('adjustModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Apply Adjustment</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openAdjustModal(id, code, balance) {
|
||||
document.getElementById('adjust-gc-id').value = id;
|
||||
document.getElementById('adjust-gc-code').textContent = code;
|
||||
document.getElementById('adjust-gc-balance').textContent = '$' + parseFloat(balance).toFixed(2);
|
||||
Modal.open('adjustModal');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$pageTitle = 'Import / Export Inventory';
|
||||
$currentPage = 'import-export';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
/* ────────────────────────────────────────────────────
|
||||
EXPORT
|
||||
──────────────────────────────────────────────────── */
|
||||
if (isset($_GET['export'])) {
|
||||
$products = db()->fetchAll(
|
||||
"SELECT p.*, c.name as category_name FROM products p
|
||||
LEFT JOIN categories c ON p.category = c.slug
|
||||
ORDER BY p.name ASC"
|
||||
);
|
||||
|
||||
$cols = ['product_id','name','description','price','sale_price','cost_price',
|
||||
'sku','barcode','category','tags','stock','low_stock_threshold',
|
||||
'weight','is_active','is_featured','images'];
|
||||
|
||||
ob_end_clean();
|
||||
header('Content-Type: text/csv; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="inventory_' . date('Y-m-d') . '.csv"');
|
||||
header('Cache-Control: no-cache');
|
||||
echo "\xEF\xBB\xBF"; // UTF-8 BOM — makes Excel open correctly
|
||||
$out = fopen('php://output', 'w');
|
||||
fputcsv($out, $cols);
|
||||
foreach ($products as $p) {
|
||||
fputcsv($out, array_map(fn($c) => $p[$c] ?? '', $cols));
|
||||
}
|
||||
fclose($out);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────
|
||||
INLINE FIELD EDIT (AJAX)
|
||||
──────────────────────────────────────────────────── */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'inline_edit') {
|
||||
ob_end_clean();
|
||||
header('Content-Type: application/json');
|
||||
$pid = $_POST['product_id'] ?? '';
|
||||
$field = $_POST['field'] ?? '';
|
||||
$val = $_POST['value'] ?? '';
|
||||
|
||||
$allowed = ['stock','price','sale_price','cost_price','sku','is_active'];
|
||||
if (!$pid || !in_array($field, $allowed)) {
|
||||
echo json_encode(['error' => 'Invalid']); exit;
|
||||
}
|
||||
if ($field === 'sale_price' && trim($val) === '') $val = null;
|
||||
|
||||
db()->update('products', [$field => $val === '' ? null : $val],
|
||||
'product_id = :id', ['id' => $pid]);
|
||||
echo json_encode(['ok' => true, 'val' => $val]);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────
|
||||
IMPORT (POST with file)
|
||||
──────────────────────────────────────────────────── */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'import') {
|
||||
$mode = $_POST['mode'] ?? 'smart'; // smart | replace
|
||||
|
||||
if (empty($_FILES['csv_file']['tmp_name'])) {
|
||||
setFlash('error', 'No file uploaded'); header('Location: /admin/import-export.php'); exit;
|
||||
}
|
||||
|
||||
$fh = fopen($_FILES['csv_file']['tmp_name'], 'r');
|
||||
// Strip UTF-8 BOM if present
|
||||
$bom = fread($fh, 3);
|
||||
if ($bom !== "\xEF\xBB\xBF") rewind($fh);
|
||||
|
||||
$headers = fgetcsv($fh);
|
||||
if (!$headers) {
|
||||
setFlash('error', 'Could not read CSV headers'); header('Location: /admin/import-export.php'); exit;
|
||||
}
|
||||
$headers = array_map('trim', $headers);
|
||||
|
||||
// Required: name + price
|
||||
if (!in_array('name', $headers) || !in_array('price', $headers)) {
|
||||
setFlash('error', 'CSV must have at least "name" and "price" columns');
|
||||
header('Location: /admin/import-export.php'); exit;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
while (($row = fgetcsv($fh)) !== false) {
|
||||
if (count($row) < 2) continue;
|
||||
$rows[] = array_combine($headers, array_pad($row, count($headers), ''));
|
||||
}
|
||||
fclose($fh);
|
||||
|
||||
if (empty($rows)) {
|
||||
setFlash('error', 'No data rows found in CSV');
|
||||
header('Location: /admin/import-export.php'); exit;
|
||||
}
|
||||
|
||||
$inserted = $updated = $deleted = 0;
|
||||
|
||||
try {
|
||||
// Get current product IDs
|
||||
$existing = array_column(db()->fetchAll("SELECT product_id FROM products"), 'product_id');
|
||||
$importedIds = array_filter(array_column($rows, 'product_id'));
|
||||
|
||||
if ($mode === 'replace') {
|
||||
// Wipe and reimport — warn user before this in the UI
|
||||
db()->query("DELETE FROM products WHERE product_id IS NOT NULL", []);
|
||||
foreach ($rows as $r) {
|
||||
$pid = (!empty($r['product_id'])) ? $r['product_id'] : generateId('prod_');
|
||||
db()->insert('products', [
|
||||
'product_id' => $pid,
|
||||
'name' => $r['name'],
|
||||
'description' => $r['description'] ?? null,
|
||||
'price' => floatval($r['price'] ?? 0),
|
||||
'sale_price' => ($r['sale_price'] ?? '') !== '' ? floatval($r['sale_price']) : null,
|
||||
'cost_price' => ($r['cost_price'] ?? '') !== '' ? floatval($r['cost_price']) : null,
|
||||
'sku' => $r['sku'] ?? null,
|
||||
'barcode' => $r['barcode'] ?? null,
|
||||
'category' => $r['category'] ?? null,
|
||||
'tags' => $r['tags'] ?? null,
|
||||
'stock' => intval($r['stock'] ?? 0),
|
||||
'low_stock_threshold'=> intval($r['low_stock_threshold'] ?? 10),
|
||||
'weight' => ($r['weight'] ?? '') !== '' ? floatval($r['weight']) : null,
|
||||
'is_active' => intval($r['is_active'] ?? 1),
|
||||
'is_featured' => intval($r['is_featured'] ?? 0),
|
||||
'images' => $r['images'] ?? null,
|
||||
]);
|
||||
$inserted++;
|
||||
}
|
||||
} else {
|
||||
// Smart mode: update existing, insert new, delete removed
|
||||
foreach ($rows as $r) {
|
||||
$pid = $r['product_id'] ?? '';
|
||||
$data = [
|
||||
'name' => $r['name'],
|
||||
'description' => $r['description'] ?? null,
|
||||
'price' => floatval($r['price'] ?? 0),
|
||||
'sale_price' => ($r['sale_price'] ?? '') !== '' ? floatval($r['sale_price']) : null,
|
||||
'cost_price' => ($r['cost_price'] ?? '') !== '' ? floatval($r['cost_price']) : null,
|
||||
'sku' => $r['sku'] ?? null,
|
||||
'barcode' => $r['barcode'] ?? null,
|
||||
'category' => $r['category'] ?? null,
|
||||
'tags' => $r['tags'] ?? null,
|
||||
'stock' => intval($r['stock'] ?? 0),
|
||||
'low_stock_threshold'=> intval($r['low_stock_threshold'] ?? 10),
|
||||
'weight' => ($r['weight'] ?? '') !== '' ? floatval($r['weight']) : null,
|
||||
'is_active' => intval($r['is_active'] ?? 1),
|
||||
'is_featured' => intval($r['is_featured'] ?? 0),
|
||||
'images' => $r['images'] ?? null,
|
||||
];
|
||||
|
||||
if ($pid && in_array($pid, $existing)) {
|
||||
db()->update('products', $data, 'product_id = :id', ['id' => $pid]);
|
||||
$updated++;
|
||||
} else {
|
||||
$data['product_id'] = $pid ?: generateId('prod_');
|
||||
db()->insert('products', $data);
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
// Delete products not in import
|
||||
$toDelete = array_diff($existing, $importedIds);
|
||||
foreach ($toDelete as $pid) {
|
||||
db()->delete('products', 'product_id = :id', ['id' => $pid]);
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
$msg = "Import complete — {$inserted} added, {$updated} updated";
|
||||
if ($deleted) $msg .= ", {$deleted} removed";
|
||||
setFlash('success', $msg);
|
||||
|
||||
} catch (Exception $e) {
|
||||
setFlash('error', 'Import failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
header('Location: /admin/import-export.php'); exit;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────
|
||||
DELETE single product
|
||||
──────────────────────────────────────────────────── */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
|
||||
db()->delete('products', 'product_id = :id', ['id' => $_POST['product_id'] ?? '']);
|
||||
setFlash('success', 'Product deleted');
|
||||
header('Location: /admin/import-export.php'); exit;
|
||||
}
|
||||
|
||||
/* ────────────────────────────────────────────────────
|
||||
LIST
|
||||
──────────────────────────────────────────────────── */
|
||||
$search = trim($_GET['search'] ?? '');
|
||||
$catFilter = $_GET['category'] ?? '';
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
if ($search) {
|
||||
$where[] = '(p.name LIKE :q OR p.sku LIKE :q2 OR p.category LIKE :q3)';
|
||||
$params['q'] = $params['q2'] = $params['q3'] = "%$search%";
|
||||
}
|
||||
if ($catFilter) {
|
||||
$where[] = 'p.category = :cat';
|
||||
$params['cat'] = $catFilter;
|
||||
}
|
||||
$whereSQL = implode(' AND ', $where);
|
||||
|
||||
$products = db()->fetchAll(
|
||||
"SELECT p.* FROM products p WHERE $whereSQL ORDER BY p.name ASC",
|
||||
$params
|
||||
);
|
||||
$categories = db()->fetchAll("SELECT name, slug FROM categories WHERE is_active=1 ORDER BY name");
|
||||
$totalProducts = db()->count('products');
|
||||
$totalStock = db()->fetch("SELECT SUM(stock) as s FROM products")['s'] ?? 0;
|
||||
$lowStock = db()->count('products', 'stock <= low_stock_threshold AND is_active = 1');
|
||||
?>
|
||||
|
||||
<div class="page-header" style="flex-wrap:wrap;gap:.75rem">
|
||||
<h1 class="page-title">Import / Export Inventory</h1>
|
||||
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
|
||||
<a href="/admin/import-export.php?export=1" class="btn btn-success">
|
||||
<i class="fas fa-file-download"></i> Export CSV
|
||||
</a>
|
||||
<button class="btn btn-primary" onclick="document.getElementById('importPanel').classList.toggle('hidden')">
|
||||
<i class="fas fa-file-upload"></i> Import CSV
|
||||
</button>
|
||||
<a href="/admin/product-edit.php" class="btn btn-secondary">
|
||||
<i class="fas fa-plus"></i> Add Product
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid" style="margin-bottom:1.5rem">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary"><i class="fas fa-box"></i></div>
|
||||
<div><div class="stat-card-value"><?= $totalProducts ?></div><div class="stat-card-label">Total Products</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success"><i class="fas fa-cubes"></i></div>
|
||||
<div><div class="stat-card-value"><?= number_format($totalStock) ?></div><div class="stat-card-label">Total Units in Stock</div></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon <?= $lowStock > 0 ? 'warning' : 'success' ?>"><i class="fas fa-exclamation-triangle"></i></div>
|
||||
<div><div class="stat-card-value"><?= $lowStock ?></div><div class="stat-card-label">Low Stock Alerts</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Panel (hidden by default) -->
|
||||
<div id="importPanel" class="hidden admin-card" style="margin-bottom:1.5rem;border:2px solid var(--admin-primary)">
|
||||
<div class="admin-card-header" style="background:rgba(255,94,26,.06)">
|
||||
<h3><i class="fas fa-file-upload"></i> Import Inventory from CSV</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:2rem">
|
||||
|
||||
<!-- Mode selector -->
|
||||
<div>
|
||||
<h4 style="margin:0 0 .75rem">Import Mode</h4>
|
||||
<label id="modeSmart" style="display:flex;align-items:flex-start;gap:.75rem;padding:1rem;border:2px solid var(--admin-primary);border-radius:var(--radius-md);cursor:pointer;margin-bottom:.75rem;background:rgba(255,94,26,.04)">
|
||||
<input type="radio" name="importMode" value="smart" checked onchange="setMode(this)" style="margin-top:.15rem;flex-shrink:0">
|
||||
<div>
|
||||
<strong>Smart Update</strong> <span class="badge badge-success" style="margin-left:.25rem">Recommended</span><br>
|
||||
<span class="text-muted" style="font-size:.85rem">Updates existing products, adds new ones, removes products not in the file. <strong>Preserves order history.</strong></span>
|
||||
</div>
|
||||
</label>
|
||||
<label id="modeReplace" style="display:flex;align-items:flex-start;gap:.75rem;padding:1rem;border:2px solid var(--color-border);border-radius:var(--radius-md);cursor:pointer">
|
||||
<input type="radio" name="importMode" value="replace" onchange="setMode(this)" style="margin-top:.15rem;flex-shrink:0">
|
||||
<div>
|
||||
<strong style="color:var(--color-error)"><i class="fas fa-exclamation-triangle"></i> Full Replace</strong><br>
|
||||
<span class="text-muted" style="font-size:.85rem">Deletes ALL existing products first, then imports. Use only if you want a clean slate.</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Upload form -->
|
||||
<div>
|
||||
<h4 style="margin:0 0 .75rem">Upload File</h4>
|
||||
<form method="POST" enctype="multipart/form-data" id="importForm" onsubmit="return confirmImport()">
|
||||
<input type="hidden" name="action" value="import">
|
||||
<input type="hidden" name="mode" id="importModeField" value="smart">
|
||||
|
||||
<div id="csvDrop" style="border:2px dashed var(--color-border);border-radius:var(--radius-md);padding:2rem;text-align:center;cursor:pointer;transition:all .2s;position:relative;margin-bottom:1rem">
|
||||
<i class="fas fa-file-csv" style="font-size:2.5rem;color:var(--admin-text-muted);display:block;margin-bottom:.5rem"></i>
|
||||
<div style="font-size:.9rem;color:var(--admin-text-muted)">Drop CSV here or <span style="color:var(--admin-primary);font-weight:600">browse</span></div>
|
||||
<div id="csvFileName" style="margin-top:.5rem;font-weight:600;color:var(--admin-primary)"></div>
|
||||
<input type="file" name="csv_file" id="csvFile" accept=".csv,text/csv" required
|
||||
style="position:absolute;inset:0;opacity:0;cursor:pointer" onchange="showFileName(this)">
|
||||
</div>
|
||||
|
||||
<div id="replaceWarning" style="display:none;background:#FEF2F2;border:1px solid #FCA5A5;border-radius:var(--radius-md);padding:.75rem 1rem;margin-bottom:1rem;font-size:.875rem;color:#991B1B">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>Warning:</strong> Full Replace will permanently delete all existing products before importing. This cannot be undone.
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:.75rem;align-items:center">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1">
|
||||
<i class="fas fa-upload"></i> Import Now
|
||||
</button>
|
||||
<a href="/admin/import-export.php?export=1" class="btn btn-secondary" title="Download a template first">
|
||||
<i class="fas fa-download"></i> Get Template
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-muted" style="font-size:.8rem;margin:.5rem 0 0">Tip: Export first to get the correct column format, edit in Excel, then import.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + Filter -->
|
||||
<div class="admin-card" style="margin-bottom:1rem">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:flex-end">
|
||||
<div class="form-group mb-0" style="flex:1;min-width:200px">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" name="search" class="form-input" placeholder="Name, SKU, category…"
|
||||
value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<option value="">All</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?= htmlspecialchars($cat['slug']) ?>"
|
||||
<?= $catFilter === $cat['slug'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($cat['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> Filter</button>
|
||||
<?php if ($search || $catFilter): ?>
|
||||
<a href="/admin/import-export.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
<span class="text-muted" style="line-height:2.5rem;font-size:.875rem"><?= count($products) ?> product<?= count($products) !== 1 ? 's' : '' ?></span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inventory Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<span style="font-size:.85rem;color:var(--admin-text-muted)">
|
||||
<i class="fas fa-info-circle"></i> Click any price or stock cell to edit inline. Changes save instantly.
|
||||
</span>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding:0;overflow-x:auto">
|
||||
<?php if (empty($products)): ?>
|
||||
<div class="text-center text-muted" style="padding:3rem">No products found.</div>
|
||||
<?php else: ?>
|
||||
<table class="admin-table" style="min-width:900px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Category</th>
|
||||
<th style="text-align:right">Price</th>
|
||||
<th style="text-align:right">Sale</th>
|
||||
<th style="text-align:right">Cost</th>
|
||||
<th style="text-align:center">Stock</th>
|
||||
<th style="text-align:center">Active</th>
|
||||
<th style="text-align:center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($products as $p):
|
||||
$isLow = $p['stock'] <= $p['low_stock_threshold'] && $p['is_active'];
|
||||
?>
|
||||
<tr class="<?= !$p['is_active'] ? 'row-inactive' : '' ?>">
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($p['name']) ?></strong>
|
||||
<?php if ($isLow): ?>
|
||||
<span class="badge badge-warning" style="margin-left:.4rem;font-size:.7rem">Low Stock</span>
|
||||
<?php endif; ?>
|
||||
<div style="font-size:.75rem;color:var(--admin-text-muted)"><?= htmlspecialchars($p['product_id']) ?></div>
|
||||
</td>
|
||||
<td class="editable-cell" data-field="sku" data-id="<?= $p['product_id'] ?>">
|
||||
<span class="cell-display"><?= htmlspecialchars($p['sku'] ?? '—') ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:90px;padding:.2rem .4rem;font-size:.85rem" value="<?= htmlspecialchars($p['sku'] ?? '') ?>">
|
||||
</td>
|
||||
<td><?= htmlspecialchars($p['category'] ?? '—') ?></td>
|
||||
<td class="editable-cell" data-field="price" data-id="<?= $p['product_id'] ?>" style="text-align:right">
|
||||
<span class="cell-display">$<?= number_format($p['price'], 2) ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:80px;padding:.2rem .4rem;font-size:.85rem;text-align:right" value="<?= $p['price'] ?>">
|
||||
</td>
|
||||
<td class="editable-cell" data-field="sale_price" data-id="<?= $p['product_id'] ?>" style="text-align:right">
|
||||
<span class="cell-display"><?= $p['sale_price'] ? '$'.number_format($p['sale_price'],2) : '<span style="color:var(--admin-text-muted)">—</span>' ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:80px;padding:.2rem .4rem;font-size:.85rem;text-align:right" value="<?= $p['sale_price'] ?? '' ?>" placeholder="none">
|
||||
</td>
|
||||
<td class="editable-cell" data-field="cost_price" data-id="<?= $p['product_id'] ?>" style="text-align:right">
|
||||
<span class="cell-display"><?= $p['cost_price'] ? '$'.number_format($p['cost_price'],2) : '<span style="color:var(--admin-text-muted)">—</span>' ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:80px;padding:.2rem .4rem;font-size:.85rem;text-align:right" value="<?= $p['cost_price'] ?? '' ?>" placeholder="none">
|
||||
</td>
|
||||
<td class="editable-cell" data-field="stock" data-id="<?= $p['product_id'] ?>" style="text-align:center">
|
||||
<span class="cell-display" style="font-weight:600;color:<?= $isLow ? 'var(--color-warning)' : 'inherit' ?>"><?= $p['stock'] ?></span>
|
||||
<input class="cell-input form-input" style="display:none;width:70px;padding:.2rem .4rem;font-size:.85rem;text-align:center" value="<?= $p['stock'] ?>">
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<label class="toggle-switch" title="Toggle active">
|
||||
<input type="checkbox" class="active-toggle"
|
||||
data-id="<?= $p['product_id'] ?>"
|
||||
<?= $p['is_active'] ? 'checked' : '' ?>>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td style="text-align:center;white-space:nowrap">
|
||||
<a href="/admin/product-edit.php?id=<?= $p['product_id'] ?>" class="btn btn-sm btn-secondary" title="Full Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form method="POST" style="display:inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="product_id" value="<?= $p['product_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete <?= htmlspecialchars(addslashes($p['name'])) ?>?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hidden { display: none !important; }
|
||||
|
||||
.row-inactive td { opacity: .5; }
|
||||
|
||||
.editable-cell {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.editable-cell:hover .cell-display {
|
||||
background: rgba(255,94,26,.08);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
outline: 1px dashed var(--admin-primary);
|
||||
}
|
||||
.editable-cell.editing .cell-display { display: none; }
|
||||
.editable-cell.editing .cell-input { display: inline-block !important; }
|
||||
.editable-cell .saving-dot {
|
||||
display: none;
|
||||
color: var(--admin-primary);
|
||||
font-size: .75rem;
|
||||
margin-left: .3rem;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
.toggle-switch {
|
||||
position: relative; display: inline-block;
|
||||
width: 36px; height: 20px; cursor: pointer;
|
||||
}
|
||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.toggle-slider {
|
||||
position: absolute; inset: 0;
|
||||
background: var(--color-border); border-radius: 20px;
|
||||
transition: .2s;
|
||||
}
|
||||
.toggle-slider::before {
|
||||
content: ''; position: absolute;
|
||||
width: 14px; height: 14px; left: 3px; bottom: 3px;
|
||||
background: white; border-radius: 50%; transition: .2s;
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider { background: var(--admin-primary); }
|
||||
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); }
|
||||
|
||||
#csvDrop.drag-active { border-color: var(--admin-primary); background: rgba(255,94,26,.04); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* ── Import panel mode ───────────────────────────── */
|
||||
function setMode(radio) {
|
||||
document.getElementById('importModeField').value = radio.value;
|
||||
document.getElementById('replaceWarning').style.display = radio.value === 'replace' ? '' : 'none';
|
||||
document.getElementById('modeSmart').style.borderColor = radio.value === 'smart' ? 'var(--admin-primary)' : 'var(--color-border)';
|
||||
document.getElementById('modeReplace').style.borderColor = radio.value === 'replace' ? 'var(--color-error)' : 'var(--color-border)';
|
||||
}
|
||||
|
||||
function showFileName(input) {
|
||||
if (input.files[0]) {
|
||||
document.getElementById('csvFileName').textContent = input.files[0].name;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmImport() {
|
||||
var mode = document.getElementById('importModeField').value;
|
||||
if (mode === 'replace') {
|
||||
return confirm('WARNING: Full Replace will DELETE all existing products and reimport from the CSV.\n\nThis cannot be undone. Are you sure?');
|
||||
}
|
||||
var file = document.getElementById('csvFile').files[0];
|
||||
if (!file) { alert('Please select a CSV file.'); return false; }
|
||||
return confirm('Import ' + file.name + '?\n\nSmart Update will update existing products and add/remove to match the file.');
|
||||
}
|
||||
|
||||
/* ── CSV drag & drop ─────────────────────────────── */
|
||||
var drop = document.getElementById('csvDrop');
|
||||
['dragenter','dragover'].forEach(function(e) {
|
||||
drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.add('drag-active'); });
|
||||
});
|
||||
['dragleave','drop'].forEach(function(e) {
|
||||
drop.addEventListener(e, function(ev) { ev.preventDefault(); drop.classList.remove('drag-active'); });
|
||||
});
|
||||
drop.addEventListener('drop', function(ev) {
|
||||
var f = ev.dataTransfer.files[0];
|
||||
if (f) {
|
||||
var dt = new DataTransfer();
|
||||
dt.items.add(f);
|
||||
document.getElementById('csvFile').files = dt.files;
|
||||
document.getElementById('csvFileName').textContent = f.name;
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Inline cell editing ─────────────────────────── */
|
||||
document.querySelectorAll('.editable-cell').forEach(function(cell) {
|
||||
var display = cell.querySelector('.cell-display');
|
||||
var input = cell.querySelector('.cell-input');
|
||||
var pid = cell.dataset.id;
|
||||
var field = cell.dataset.field;
|
||||
|
||||
cell.addEventListener('click', function(e) {
|
||||
if (cell.classList.contains('editing')) return;
|
||||
cell.classList.add('editing');
|
||||
input.style.display = 'inline-block';
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
|
||||
function save() {
|
||||
cell.classList.remove('editing');
|
||||
var val = input.value;
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'inline_edit');
|
||||
fd.append('product_id', pid);
|
||||
fd.append('field', field);
|
||||
fd.append('value', val);
|
||||
|
||||
fetch('/admin/import-export.php', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.ok) {
|
||||
// Refresh display text
|
||||
if (field === 'sku') {
|
||||
display.textContent = val || '—';
|
||||
} else if (field === 'stock') {
|
||||
display.textContent = val;
|
||||
} else {
|
||||
display.innerHTML = val !== '' ? '$' + parseFloat(val).toFixed(2) : '<span style="color:var(--admin-text-muted)">—</span>';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
input.addEventListener('blur', save);
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); save(); }
|
||||
if (e.key === 'Escape') { cell.classList.remove('editing'); input.value = input.defaultValue; }
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Active toggle ───────────────────────────────── */
|
||||
document.querySelectorAll('.active-toggle').forEach(function(cb) {
|
||||
cb.addEventListener('change', function() {
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'inline_edit');
|
||||
fd.append('product_id', this.dataset.id);
|
||||
fd.append('field', 'is_active');
|
||||
fd.append('value', this.checked ? 1 : 0);
|
||||
fetch('/admin/import-export.php', { method: 'POST', body: fd });
|
||||
var row = this.closest('tr');
|
||||
if (row) row.classList.toggle('row-inactive', !this.checked);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,8 @@
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/admin/assets/admin.js"></script>
|
||||
<?php if (isset($extraScripts)) echo $extraScripts; ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Admin Header
|
||||
* Authentication temporarily disabled
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
|
||||
AdminAuth::require();
|
||||
|
||||
$adminUser = AdminAuth::getUser();
|
||||
|
||||
$currentPage = basename($_SERVER['PHP_SELF'], '.php');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $pageTitle ?? 'Admin' ?> - Tom's Java Jive Admin</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="stylesheet" href="/admin/assets/admin.css">
|
||||
|
||||
<?php if (isset($extraHead)) echo $extraHead; ?>
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<div class="admin-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="admin-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a href="/admin/" class="admin-logo">
|
||||
<img src="/assets/images/logo.svg" alt="Logo" class="logo-img" style="height: 32px;">
|
||||
<span>Admin</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/admin/" class="nav-item <?= $currentPage === 'index' ? 'active' : '' ?>">
|
||||
<i class="fas fa-chart-line"></i> Dashboard
|
||||
</a>
|
||||
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-title">Sales</span>
|
||||
<a href="/admin/orders.php" class="nav-item <?= $currentPage === 'orders' ? 'active' : '' ?>">
|
||||
<i class="fas fa-shopping-cart"></i> Orders
|
||||
</a>
|
||||
<a href="/admin/pos.php" class="nav-item <?= $currentPage === 'pos' ? 'active' : '' ?>">
|
||||
<i class="fas fa-cash-register"></i> POS
|
||||
</a>
|
||||
<a href="/admin/analytics.php" class="nav-item <?= $currentPage === 'analytics' ? 'active' : '' ?>">
|
||||
<i class="fas fa-chart-bar"></i> Analytics
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-title">Catalog</span>
|
||||
<a href="/admin/products.php" class="nav-item <?= $currentPage === 'products' ? 'active' : '' ?>">
|
||||
<i class="fas fa-box"></i> Products
|
||||
</a>
|
||||
<a href="/admin/categories.php" class="nav-item <?= $currentPage === 'categories' ? 'active' : '' ?>">
|
||||
<i class="fas fa-tags"></i> Categories
|
||||
</a>
|
||||
<a href="/admin/product-types.php" class="nav-item <?= $currentPage === 'product-types' ? 'active' : '' ?>">
|
||||
<i class="fas fa-layer-group"></i> Product Types
|
||||
</a>
|
||||
<a href="/admin/inventory.php" class="nav-item <?= $currentPage === 'inventory' ? 'active' : '' ?>">
|
||||
<i class="fas fa-warehouse"></i> Inventory
|
||||
</a>
|
||||
<a href="/admin/import-export.php" class="nav-item <?= $currentPage === 'import-export' ? 'active' : '' ?>">
|
||||
<i class="fas fa-file-csv"></i> Import / Export
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-title">Customers</span>
|
||||
<a href="/admin/customers.php" class="nav-item <?= $currentPage === 'customers' ? 'active' : '' ?>">
|
||||
<i class="fas fa-users"></i> Customers
|
||||
</a>
|
||||
<a href="/admin/reviews.php" class="nav-item <?= $currentPage === 'reviews' ? 'active' : '' ?>">
|
||||
<i class="fas fa-star"></i> Reviews
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-title">Content</span>
|
||||
<a href="/admin/splashes.php" class="nav-item <?= $currentPage === 'splashes' ? 'active' : '' ?>">
|
||||
<i class="fas fa-th-large"></i> Splash Box
|
||||
</a>
|
||||
<a href="/admin/about-us.php" class="nav-item <?= $currentPage === 'about-us' ? 'active' : '' ?>">
|
||||
<i class="fas fa-align-left"></i> About Us Text
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-title">Marketing</span>
|
||||
<a href="/admin/gift-cards.php" class="nav-item <?= $currentPage === 'gift-cards' ? 'active' : '' ?>">
|
||||
<i class="fas fa-gift"></i> Gift Cards
|
||||
</a>
|
||||
<a href="/admin/coupons.php" class="nav-item <?= $currentPage === 'coupons' ? 'active' : '' ?>">
|
||||
<i class="fas fa-ticket-alt"></i> Coupons
|
||||
</a>
|
||||
<a href="/admin/campaigns.php" class="nav-item <?= $currentPage === 'campaigns' ? 'active' : '' ?>">
|
||||
<i class="fas fa-envelope"></i> Email Campaigns
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-group">
|
||||
<span class="nav-group-title">Settings</span>
|
||||
<a href="/admin/settings.php" class="nav-item <?= $currentPage === 'settings' ? 'active' : '' ?>">
|
||||
<i class="fas fa-cog"></i> Store Settings
|
||||
</a>
|
||||
<a href="/admin/integrations.php" class="nav-item <?= $currentPage === 'integrations' ? 'active' : '' ?>">
|
||||
<i class="fas fa-plug"></i> Integrations
|
||||
</a>
|
||||
<a href="/admin/shipping.php" class="nav-item <?= $currentPage === 'shipping' ? 'active' : '' ?>">
|
||||
<i class="fas fa-truck"></i> Shipping
|
||||
</a>
|
||||
<a href="/admin/payments.php" class="nav-item <?= $currentPage === 'payments' ? 'active' : '' ?>">
|
||||
<i class="fas fa-credit-card"></i> Payments
|
||||
</a>
|
||||
<a href="/admin/users.php" class="nav-item <?= $currentPage === 'users' ? 'active' : '' ?>">
|
||||
<i class="fas fa-user-shield"></i> Admin Users
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="/" target="_blank" class="nav-item">
|
||||
<i class="fas fa-external-link-alt"></i> View Store
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="admin-main">
|
||||
<header class="admin-header">
|
||||
<button class="sidebar-toggle" id="sidebarToggle">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<div class="header-search">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" placeholder="Search...">
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="admin-user">
|
||||
<span><?= htmlspecialchars($adminUser['name']) ?></span>
|
||||
<a href="/admin/logout.php" class="btn btn-sm">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="admin-content">
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div style="background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.35);color:#10b981;padding:13px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;font-weight:500;display:flex;align-items:center;gap:10px">
|
||||
<i class="fas fa-check-circle" style="font-size:16px"></i> <?= getFlash('success') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div style="background:rgba(220,38,38,.1);border:1px solid rgba(220,38,38,.3);color:#f87171;padding:13px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;font-weight:500;display:flex;align-items:center;gap:10px">
|
||||
<i class="fas fa-exclamation-circle" style="font-size:16px"></i> <?= getFlash('error') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('info')): ?>
|
||||
<div style="background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3);color:#60a5fa;padding:13px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;font-weight:500;display:flex;align-items:center;gap:10px">
|
||||
<i class="fas fa-info-circle" style="font-size:16px"></i> <?= getFlash('info') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('warning')): ?>
|
||||
<div style="background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.3);color:#f59e0b;padding:13px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;font-weight:500;display:flex;align-items:center;gap:10px">
|
||||
<i class="fas fa-exclamation-triangle" style="font-size:16px"></i> <?= getFlash('warning') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash("success")): ?><div style="background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.35);color:#10b981;padding:13px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;font-weight:500;display:flex;align-items:center;gap:8px"><i class="fas fa-check-circle"></i><?= getFlash("success") ?></div><?php endif; ?>
|
||||
<?php if (hasFlash("error")): ?><div style="background:rgba(220,38,38,.1);border:1px solid rgba(220,38,38,.3);color:#f87171;padding:13px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;font-weight:500;display:flex;align-items:center;gap:8px"><i class="fas fa-exclamation-circle"></i><?= getFlash("error") ?></div><?php endif; ?>
|
||||
<?php if (hasFlash("info")): ?><div style="background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3);color:#60a5fa;padding:13px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;font-weight:500;display:flex;align-items:center;gap:8px"><i class="fas fa-info-circle"></i><?= getFlash("info") ?></div><?php endif; ?>
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Admin Dashboard
|
||||
*/
|
||||
|
||||
$pageTitle = 'Dashboard';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Get stats
|
||||
$totalOrders = db()->count('orders');
|
||||
$todayOrders = db()->count('orders', 'DATE(created_at) = CURDATE()');
|
||||
$totalRevenue = db()->fetch("SELECT COALESCE(SUM(total), 0) as total FROM orders WHERE payment_status = 'paid'")['total'] ?? 0;
|
||||
$todayRevenue = db()->fetch("SELECT COALESCE(SUM(total), 0) as total FROM orders WHERE payment_status = 'paid' AND DATE(created_at) = CURDATE()")['total'] ?? 0;
|
||||
$totalCustomers = db()->count('customers');
|
||||
$totalProducts = db()->count('products', 'is_active = 1');
|
||||
$lowStockProducts = db()->count('products', 'stock <= low_stock_threshold AND is_active = 1');
|
||||
$pendingOrders = db()->count('orders', "order_status = 'pending'");
|
||||
|
||||
// Recent orders
|
||||
$recentOrders = db()->fetchAll(
|
||||
"SELECT * FROM orders ORDER BY created_at DESC LIMIT 10"
|
||||
);
|
||||
|
||||
// Low stock products
|
||||
$lowStockItems = db()->fetchAll(
|
||||
"SELECT * FROM products WHERE stock <= low_stock_threshold AND is_active = 1 ORDER BY stock ASC LIMIT 5"
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
<span class="text-muted"><?= date('l, F j, Y') ?></span>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary">
|
||||
<i class="fas fa-dollar-sign"></i>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= formatCurrency($todayRevenue) ?></div>
|
||||
<div class="stat-card-label">Today's Revenue</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success">
|
||||
<i class="fas fa-shopping-cart"></i>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= $todayOrders ?></div>
|
||||
<div class="stat-card-label">Today's Orders</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon warning">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= $totalCustomers ?></div>
|
||||
<div class="stat-card-label">Total Customers</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon <?= $pendingOrders > 0 ? 'error' : 'primary' ?>">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= $pendingOrders ?></div>
|
||||
<div class="stat-card-label">Pending Orders</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
|
||||
<!-- Recent Orders -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Recent Orders</h3>
|
||||
<a href="/admin/orders.php" class="btn btn-sm btn-secondary">View All</a>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
<th>Customer</th>
|
||||
<th>Total</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recentOrders)): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-muted" style="text-align: center; padding: 2rem;">
|
||||
No orders yet
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recentOrders as $order): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/admin/order.php?id=<?= $order['order_id'] ?>">
|
||||
<?= htmlspecialchars($order['order_number']) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($order['customer_name'] ?? $order['customer_email']) ?></td>
|
||||
<td><?= formatCurrency($order['total']) ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$statusClass = match($order['order_status']) {
|
||||
'pending' => 'warning',
|
||||
'confirmed', 'processing' => 'primary',
|
||||
'shipped', 'delivered' => 'success',
|
||||
'cancelled', 'refunded' => 'error',
|
||||
default => 'primary'
|
||||
};
|
||||
?>
|
||||
<span class="badge badge-<?= $statusClass ?>">
|
||||
<?= ucfirst($order['order_status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted"><?= formatDate($order['created_at']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Stats -->
|
||||
<div>
|
||||
<!-- Quick Stats -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Overview</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<span>Total Revenue</span>
|
||||
<strong><?= formatCurrency($totalRevenue) ?></strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<span>Total Orders</span>
|
||||
<strong><?= $totalOrders ?></strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 1rem;">
|
||||
<span>Active Products</span>
|
||||
<strong><?= $totalProducts ?></strong>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>Low Stock Items</span>
|
||||
<strong class="<?= $lowStockProducts > 0 ? 'text-error' : '' ?>"><?= $lowStockProducts ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Low Stock Alert -->
|
||||
<?php if (!empty($lowStockItems)): ?>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">
|
||||
<i class="fas fa-exclamation-triangle text-warning"></i> Low Stock
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<?php foreach ($lowStockItems as $item): ?>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.75rem;">
|
||||
<span><?= htmlspecialchars(truncate($item['name'], 25)) ?></span>
|
||||
<span class="badge badge-error"><?= $item['stock'] ?> left</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<a href="/admin/inventory.php" class="btn btn-sm btn-secondary btn-block mt-1">
|
||||
Manage Inventory
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Quick Actions</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<a href="/admin/product-edit.php" class="btn btn-primary btn-block mb-1">
|
||||
<i class="fas fa-plus"></i> Add Product
|
||||
</a>
|
||||
<a href="/admin/pos.php" class="btn btn-secondary btn-block mb-1">
|
||||
<i class="fas fa-cash-register"></i> Open POS
|
||||
</a>
|
||||
<a href="/admin/orders.php?status=pending" class="btn btn-secondary btn-block">
|
||||
<i class="fas fa-clock"></i> Pending Orders
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Integrations Settings
|
||||
*/
|
||||
|
||||
$pageTitle = 'Integrations';
|
||||
$currentPage = 'integrations';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$section = $_POST['section'] ?? '';
|
||||
|
||||
$settingsMap = [
|
||||
'sendgrid' => ['sendgrid_api_key', 'sendgrid_from_email', 'sendgrid_from_name', 'email_notifications_enabled'],
|
||||
'twilio' => ['twilio_account_sid', 'twilio_auth_token', 'twilio_phone_number', 'sms_notifications_enabled'],
|
||||
'push' => ['vapid_public_key', 'vapid_private_key', 'push_notifications_enabled'],
|
||||
'loyalty' => ['loyalty_enabled']
|
||||
];
|
||||
|
||||
if (isset($settingsMap[$section])) {
|
||||
foreach ($settingsMap[$section] as $key) {
|
||||
$value = $_POST[$key] ?? '';
|
||||
|
||||
// Check if setting exists
|
||||
$existing = db()->fetch("SELECT id FROM settings WHERE setting_key = :key", ['key' => $key]);
|
||||
|
||||
if ($existing) {
|
||||
db()->query(
|
||||
"UPDATE settings SET setting_value = :value, updated_at = NOW() WHERE setting_key = :key",
|
||||
['value' => $value, 'key' => $key]
|
||||
);
|
||||
} else {
|
||||
db()->insert('settings', [
|
||||
'setting_key' => $key,
|
||||
'setting_value' => $value
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
setFlash('success', ucfirst($section) . ' settings saved successfully!');
|
||||
}
|
||||
|
||||
redirect('/admin/integrations.php');
|
||||
}
|
||||
|
||||
// Load current settings
|
||||
$settings = [];
|
||||
$allSettings = db()->fetchAll("SELECT setting_key, setting_value FROM settings");
|
||||
foreach ($allSettings as $s) {
|
||||
$settings[$s['setting_key']] = $s['setting_value'];
|
||||
}
|
||||
?>
|
||||
|
||||
<style>
|
||||
.integration-card {
|
||||
background: var(--admin-surface);
|
||||
border-radius: var(--admin-radius);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.integration-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
}
|
||||
|
||||
.integration-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.integration-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.integration-icon.sendgrid { background: #1A82E2; color: white; }
|
||||
.integration-icon.twilio { background: #F22F46; color: white; }
|
||||
.integration-icon.push { background: #8B5CF6; color: white; }
|
||||
.integration-icon.loyalty { background: #F59E0B; color: white; }
|
||||
|
||||
.integration-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.configured {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--admin-success);
|
||||
}
|
||||
|
||||
.status-badge.not-configured {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--admin-warning);
|
||||
}
|
||||
|
||||
.status-badge.enabled {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--admin-success);
|
||||
}
|
||||
|
||||
.status-badge.disabled {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--admin-error);
|
||||
}
|
||||
|
||||
.key-input {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--admin-text-muted);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.help-link {
|
||||
color: var(--admin-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Integrations</h1>
|
||||
<p class="text-muted">Configure third-party service integrations</p>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success mb-2">
|
||||
<i class="fas fa-check-circle"></i> <?= getFlash('success') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- SendGrid Email -->
|
||||
<div class="integration-card">
|
||||
<div class="integration-header">
|
||||
<div class="integration-title">
|
||||
<div class="integration-icon sendgrid">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin: 0;">SendGrid Email</h3>
|
||||
<p style="margin: 0.25rem 0 0; color: var(--admin-text-muted); font-size: 0.875rem;">
|
||||
Send transactional emails (order confirmations, shipping updates, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$sgConfigured = !empty($settings['sendgrid_api_key']) && $settings['sendgrid_api_key'] !== 'YOUR_SENDGRID_API_KEY_HERE';
|
||||
$sgEnabled = ($settings['email_notifications_enabled'] ?? '0') === '1';
|
||||
?>
|
||||
<span class="status-badge <?= $sgConfigured && $sgEnabled ? 'enabled' : ($sgConfigured ? 'configured' : 'not-configured') ?>">
|
||||
<?= $sgConfigured && $sgEnabled ? 'Enabled' : ($sgConfigured ? 'Configured' : 'Not Configured') ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="integration-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="sendgrid">
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex: 2;">
|
||||
<label class="form-label">SendGrid API Key</label>
|
||||
<input type="password" name="sendgrid_api_key" class="form-input key-input"
|
||||
value="<?= htmlspecialchars($settings['sendgrid_api_key'] ?? '') ?>"
|
||||
placeholder="SG.xxxxxxxxxxxxxxxxxxxxxxxx">
|
||||
<p class="help-text">
|
||||
Get your API key from
|
||||
<a href="https://app.sendgrid.com/settings/api_keys" target="_blank" class="help-link">SendGrid Dashboard</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">From Email</label>
|
||||
<input type="email" name="sendgrid_from_email" class="form-input"
|
||||
value="<?= htmlspecialchars($settings['sendgrid_from_email'] ?? 'noreply@tomsjavajive.com') ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">From Name</label>
|
||||
<input type="text" name="sendgrid_from_name" class="form-input"
|
||||
value="<?= htmlspecialchars($settings['sendgrid_from_name'] ?? "Tom's Java Jive") ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" name="email_notifications_enabled" value="1"
|
||||
<?= ($settings['email_notifications_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
||||
Enable email notifications
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Settings
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="testSendGrid()">
|
||||
<i class="fas fa-paper-plane"></i> Send Test Email
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Twilio SMS -->
|
||||
<div class="integration-card">
|
||||
<div class="integration-header">
|
||||
<div class="integration-title">
|
||||
<div class="integration-icon twilio">
|
||||
<i class="fas fa-sms"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin: 0;">Twilio SMS</h3>
|
||||
<p style="margin: 0.25rem 0 0; color: var(--admin-text-muted); font-size: 0.875rem;">
|
||||
Send SMS notifications for orders, shipping updates, and promotions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$twConfigured = !empty($settings['twilio_account_sid']) && !empty($settings['twilio_auth_token']);
|
||||
$twEnabled = ($settings['sms_notifications_enabled'] ?? '0') === '1';
|
||||
?>
|
||||
<span class="status-badge <?= $twConfigured && $twEnabled ? 'enabled' : ($twConfigured ? 'configured' : 'not-configured') ?>">
|
||||
<?= $twConfigured && $twEnabled ? 'Enabled' : ($twConfigured ? 'Configured' : 'Not Configured') ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="integration-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="twilio">
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Account SID</label>
|
||||
<input type="text" name="twilio_account_sid" class="form-input key-input"
|
||||
value="<?= htmlspecialchars($settings['twilio_account_sid'] ?? '') ?>"
|
||||
placeholder="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Auth Token</label>
|
||||
<input type="password" name="twilio_auth_token" class="form-input key-input"
|
||||
value="<?= htmlspecialchars($settings['twilio_auth_token'] ?? '') ?>"
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Twilio Phone Number</label>
|
||||
<input type="text" name="twilio_phone_number" class="form-input"
|
||||
value="<?= htmlspecialchars($settings['twilio_phone_number'] ?? '') ?>"
|
||||
placeholder="+1234567890">
|
||||
<p class="help-text">
|
||||
Get your credentials from
|
||||
<a href="https://console.twilio.com/" target="_blank" class="help-link">Twilio Console</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" name="sms_notifications_enabled" value="1"
|
||||
<?= ($settings['sms_notifications_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
||||
Enable SMS notifications
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Settings
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="testTwilio()">
|
||||
<i class="fas fa-sms"></i> Send Test SMS
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Push Notifications -->
|
||||
<div class="integration-card">
|
||||
<div class="integration-header">
|
||||
<div class="integration-title">
|
||||
<div class="integration-icon push">
|
||||
<i class="fas fa-bell"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin: 0;">Push Notifications</h3>
|
||||
<p style="margin: 0.25rem 0 0; color: var(--admin-text-muted); font-size: 0.875rem;">
|
||||
Web push notifications for order updates and promotions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$pushConfigured = !empty($settings['vapid_public_key']) && !empty($settings['vapid_private_key']);
|
||||
$pushEnabled = ($settings['push_notifications_enabled'] ?? '0') === '1';
|
||||
?>
|
||||
<span class="status-badge <?= $pushConfigured && $pushEnabled ? 'enabled' : ($pushConfigured ? 'configured' : 'not-configured') ?>">
|
||||
<?= $pushConfigured && $pushEnabled ? 'Enabled' : ($pushConfigured ? 'Configured' : 'Not Configured') ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="integration-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="push">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">VAPID Public Key</label>
|
||||
<input type="text" name="vapid_public_key" class="form-input key-input"
|
||||
value="<?= htmlspecialchars($settings['vapid_public_key'] ?? '') ?>"
|
||||
placeholder="BNxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">VAPID Private Key</label>
|
||||
<input type="password" name="vapid_private_key" class="form-input key-input"
|
||||
value="<?= htmlspecialchars($settings['vapid_private_key'] ?? '') ?>"
|
||||
placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
|
||||
<p class="help-text">
|
||||
Generate VAPID keys at
|
||||
<a href="https://web-push-codelab.glitch.me/" target="_blank" class="help-link">Web Push Codelab</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" name="push_notifications_enabled" value="1"
|
||||
<?= ($settings['push_notifications_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
||||
Enable push notifications
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loyalty Program -->
|
||||
<div class="integration-card">
|
||||
<div class="integration-header">
|
||||
<div class="integration-title">
|
||||
<div class="integration-icon loyalty">
|
||||
<i class="fas fa-crown"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin: 0;">Loyalty Program</h3>
|
||||
<p style="margin: 0.25rem 0 0; color: var(--admin-text-muted); font-size: 0.875rem;">
|
||||
Reward customers with points and tiers (Bronze, Silver, Gold, Platinum)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php $loyaltyEnabled = ($settings['loyalty_enabled'] ?? '1') === '1'; ?>
|
||||
<span class="status-badge <?= $loyaltyEnabled ? 'enabled' : 'disabled' ?>">
|
||||
<?= $loyaltyEnabled ? 'Enabled' : 'Disabled' ?>
|
||||
</span>
|
||||
</div>
|
||||
<div class="integration-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="loyalty">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" name="loyalty_enabled" value="1"
|
||||
<?= $loyaltyEnabled ? 'checked' : '' ?>>
|
||||
Enable loyalty program
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="background: var(--admin-bg); padding: 1.5rem; border-radius: var(--admin-radius); margin: 1rem 0;">
|
||||
<h4 style="margin: 0 0 1rem;">Tier Structure</h4>
|
||||
<table class="table" style="margin: 0;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tier</th>
|
||||
<th>Min Points</th>
|
||||
<th>Multiplier</th>
|
||||
<th>Key Benefits</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span style="color: #CD7F32;"><i class="fas fa-coffee"></i></span> Bronze Bean</td>
|
||||
<td>0</td>
|
||||
<td>1x</td>
|
||||
<td>1 point/$1, Birthday reward</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span style="color: #C0C0C0;"><i class="fas fa-mug-hot"></i></span> Silver Roast</td>
|
||||
<td>500</td>
|
||||
<td>1.25x</td>
|
||||
<td>Free shipping $25+, Double points weekends</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span style="color: #FFD700;"><i class="fas fa-crown"></i></span> Gold Blend</td>
|
||||
<td>1,500</td>
|
||||
<td>1.5x</td>
|
||||
<td>Free shipping all orders, Priority support</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span style="color: #E5E4E2;"><i class="fas fa-gem"></i></span> Platinum Reserve</td>
|
||||
<td>5,000</td>
|
||||
<td>2x</td>
|
||||
<td>Express shipping, VIP events, Account manager</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-muted" style="margin: 1rem 0 0; font-size: 0.875rem;">
|
||||
100 points = $1 credit • Points earned on every purchase
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Save Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Modal -->
|
||||
<div class="modal-overlay" id="testModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="testModalTitle">Send Test</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('testModal')">×</button>
|
||||
</div>
|
||||
<form id="testForm">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" id="testInputLabel">Recipient</label>
|
||||
<input type="text" id="testRecipient" class="form-input" required>
|
||||
</div>
|
||||
<div id="testResult" style="display: none; padding: 1rem; border-radius: var(--admin-radius); margin-top: 1rem;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('testModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="testSubmitBtn">
|
||||
<i class="fas fa-paper-plane"></i> Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let testType = '';
|
||||
|
||||
function testSendGrid() {
|
||||
testType = 'email';
|
||||
document.getElementById('testModalTitle').textContent = 'Send Test Email';
|
||||
document.getElementById('testInputLabel').textContent = 'Recipient Email';
|
||||
document.getElementById('testRecipient').type = 'email';
|
||||
document.getElementById('testRecipient').placeholder = 'test@example.com';
|
||||
document.getElementById('testResult').style.display = 'none';
|
||||
Modal.open('testModal');
|
||||
}
|
||||
|
||||
function testTwilio() {
|
||||
testType = 'sms';
|
||||
document.getElementById('testModalTitle').textContent = 'Send Test SMS';
|
||||
document.getElementById('testInputLabel').textContent = 'Phone Number';
|
||||
document.getElementById('testRecipient').type = 'tel';
|
||||
document.getElementById('testRecipient').placeholder = '+1234567890';
|
||||
document.getElementById('testResult').style.display = 'none';
|
||||
Modal.open('testModal');
|
||||
}
|
||||
|
||||
document.getElementById('testForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const recipient = document.getElementById('testRecipient').value;
|
||||
const resultDiv = document.getElementById('testResult');
|
||||
const submitBtn = document.getElementById('testSubmitBtn');
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/test-notification.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: testType, recipient })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
if (data.success) {
|
||||
resultDiv.style.background = 'rgba(16, 185, 129, 0.1)';
|
||||
resultDiv.innerHTML = '<i class="fas fa-check-circle" style="color: var(--admin-success);"></i> ' + (data.message || 'Test sent successfully!');
|
||||
} else {
|
||||
resultDiv.style.background = 'rgba(239, 68, 68, 0.1)';
|
||||
resultDiv.innerHTML = '<i class="fas fa-times-circle" style="color: var(--admin-error);"></i> ' + (data.error || 'Failed to send test');
|
||||
}
|
||||
} catch (err) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.style.background = 'rgba(239, 68, 68, 0.1)';
|
||||
resultDiv.innerHTML = '<i class="fas fa-times-circle" style="color: var(--admin-error);"></i> Error: ' + err.message;
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-paper-plane"></i> Send';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,337 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Inventory Management
|
||||
*/
|
||||
|
||||
$pageTitle = 'Inventory';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'adjust' && !empty($_POST['product_id'])) {
|
||||
$adjustment = intval($_POST['adjustment'] ?? 0);
|
||||
$reason = trim($_POST['reason'] ?? '');
|
||||
|
||||
if ($adjustment != 0) {
|
||||
$product = db()->fetch("SELECT name, stock FROM products WHERE product_id = :id", ['id' => $_POST['product_id']]);
|
||||
$newStock = max(0, ($product['stock'] ?? 0) + $adjustment);
|
||||
|
||||
db()->update('products', ['stock' => $newStock], 'product_id = :id', ['id' => $_POST['product_id']]);
|
||||
setFlash('success', $product['name'] . ' stock adjusted by ' . ($adjustment > 0 ? '+' : '') . $adjustment . '. New stock: ' . $newStock);
|
||||
}
|
||||
header('Location: /admin/inventory.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'update_threshold' && !empty($_POST['product_id'])) {
|
||||
$threshold = intval($_POST['low_stock_threshold'] ?? 10);
|
||||
db()->update('products', ['low_stock_threshold' => $threshold], 'product_id = :id', ['id' => $_POST['product_id']]);
|
||||
setFlash('success', 'Low stock threshold updated');
|
||||
header('Location: /admin/inventory.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'bulk_adjust') {
|
||||
$adjustments = $_POST['adjustments'] ?? [];
|
||||
$count = 0;
|
||||
foreach ($adjustments as $productId => $adj) {
|
||||
$adj = intval($adj);
|
||||
if ($adj != 0) {
|
||||
db()->query(
|
||||
"UPDATE products SET stock = GREATEST(0, stock + :adj) WHERE product_id = :id",
|
||||
['adj' => $adj, 'id' => $productId]
|
||||
);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
if ($count > 0) {
|
||||
setFlash('success', "Adjusted stock for $count products");
|
||||
}
|
||||
header('Location: /admin/inventory.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
$filter = $_GET['filter'] ?? '';
|
||||
$search = $_GET['search'] ?? '';
|
||||
$category = $_GET['category'] ?? '';
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$where[] = '(name LIKE :search OR sku LIKE :search OR barcode LIKE :search)';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ($category) {
|
||||
$where[] = 'category = :category';
|
||||
$params['category'] = $category;
|
||||
}
|
||||
|
||||
if ($filter === 'low') {
|
||||
$where[] = 'stock <= low_stock_threshold AND stock > 0';
|
||||
} elseif ($filter === 'out') {
|
||||
$where[] = 'stock <= 0';
|
||||
} elseif ($filter === 'in') {
|
||||
$where[] = 'stock > low_stock_threshold';
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $where);
|
||||
|
||||
$products = db()->fetchAll(
|
||||
"SELECT product_id, name, sku, barcode, category, stock, low_stock_threshold, price, is_active
|
||||
FROM products
|
||||
WHERE {$whereClause}
|
||||
ORDER BY stock ASC, name ASC",
|
||||
$params
|
||||
);
|
||||
|
||||
// Get categories for filter
|
||||
$categories = db()->fetchAll(
|
||||
"SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != '' ORDER BY category"
|
||||
);
|
||||
|
||||
// Stats
|
||||
$totalProducts = db()->count('products', 'is_active = 1');
|
||||
$lowStockCount = db()->count('products', 'stock <= low_stock_threshold AND stock > 0 AND is_active = 1');
|
||||
$outOfStockCount = db()->count('products', 'stock <= 0 AND is_active = 1');
|
||||
$totalStock = db()->fetch("SELECT SUM(stock) as total FROM products WHERE is_active = 1")['total'] ?? 0;
|
||||
$inventoryValue = db()->fetch("SELECT SUM(stock * price) as total FROM products WHERE is_active = 1")['total'] ?? 0;
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Inventory Management</h1>
|
||||
<button class="btn btn-primary" onclick="document.getElementById('bulkForm').style.display = document.getElementById('bulkForm').style.display === 'none' ? 'block' : 'none'">
|
||||
<i class="fas fa-boxes"></i> Bulk Adjust
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary"><i class="fas fa-boxes"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= number_format($totalStock) ?></div>
|
||||
<div class="stat-card-label">Total Units</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success"><i class="fas fa-dollar-sign"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= formatCurrency($inventoryValue) ?></div>
|
||||
<div class="stat-card-label">Inventory Value</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon warning"><i class="fas fa-exclamation-triangle"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= $lowStockCount ?></div>
|
||||
<div class="stat-card-label">Low Stock</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon error"><i class="fas fa-times-circle"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= $outOfStockCount ?></div>
|
||||
<div class="stat-card-label">Out of Stock</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: end;">
|
||||
<div class="form-group mb-0" style="flex: 1; min-width: 200px;">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" name="search" class="form-input" placeholder="Name, SKU, barcode..." value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?= htmlspecialchars($cat['category']) ?>" <?= $category === $cat['category'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars(ucfirst($cat['category'])) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Stock Status</label>
|
||||
<select name="filter" class="form-select">
|
||||
<option value="">All</option>
|
||||
<option value="in" <?= $filter === 'in' ? 'selected' : '' ?>>In Stock</option>
|
||||
<option value="low" <?= $filter === 'low' ? 'selected' : '' ?>>Low Stock</option>
|
||||
<option value="out" <?= $filter === 'out' ? 'selected' : '' ?>>Out of Stock</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> Filter</button>
|
||||
<?php if ($filter || $search || $category): ?>
|
||||
<a href="/admin/inventory.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Adjustment Form (hidden by default) -->
|
||||
<form method="POST" id="bulkForm" style="display: none;">
|
||||
<input type="hidden" name="action" value="bulk_adjust">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Bulk Stock Adjustment</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<p class="text-muted mb-1">Enter adjustment values for each product (positive to add, negative to subtract). Leave blank or 0 to skip.</p>
|
||||
<button type="submit" class="btn btn-primary mb-1">Apply All Adjustments</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Inventory Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<span><?= count($products) ?> products</span>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU / Barcode</th>
|
||||
<th>Category</th>
|
||||
<th>Stock</th>
|
||||
<th>Threshold</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($products)): ?>
|
||||
<tr><td colspan="7" class="text-muted" style="text-align: center; padding: 2rem;">No products found</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($product['name']) ?></strong>
|
||||
<?php if (!$product['is_active']): ?>
|
||||
<span class="badge badge-error">Inactive</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($product['sku']): ?>
|
||||
<code><?= htmlspecialchars($product['sku']) ?></code>
|
||||
<?php endif; ?>
|
||||
<?php if ($product['barcode']): ?>
|
||||
<br><small class="text-muted"><?= htmlspecialchars($product['barcode']) ?></small>
|
||||
<?php endif; ?>
|
||||
<?php if (!$product['sku'] && !$product['barcode']): ?>
|
||||
<span class="text-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($product['category'] ?? '-') ?></td>
|
||||
<td>
|
||||
<span style="font-size: 1.5rem; font-weight: 700; color: <?= $product['stock'] <= 0 ? 'var(--admin-error)' : ($product['stock'] <= $product['low_stock_threshold'] ? 'var(--admin-warning)' : 'var(--admin-success)') ?>;">
|
||||
<?= $product['stock'] ?>
|
||||
</span>
|
||||
<input type="number" name="adjustments[<?= $product['product_id'] ?>]" form="bulkForm"
|
||||
class="form-input" style="width: 80px; margin-left: 0.5rem; display: none;" placeholder="+/-">
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted"><?= $product['low_stock_threshold'] ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($product['stock'] <= 0): ?>
|
||||
<span class="badge badge-error">Out of Stock</span>
|
||||
<?php elseif ($product['stock'] <= $product['low_stock_threshold']): ?>
|
||||
<span class="badge badge-warning">Low Stock</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-success">In Stock</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick="openAdjustModal('<?= $product['product_id'] ?>', '<?= htmlspecialchars(addslashes($product['name'])) ?>', <?= $product['stock'] ?>, <?= $product['low_stock_threshold'] ?>)" title="Adjust Stock">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<a href="/admin/product-edit.php?id=<?= $product['product_id'] ?>" class="btn btn-sm btn-secondary" title="Edit Product">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Adjust Modal -->
|
||||
<div class="modal-overlay" id="adjustModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Adjust Stock</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('adjustModal')">×</button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="adjust">
|
||||
<input type="hidden" name="product_id" id="adjustProductId">
|
||||
|
||||
<p><strong id="adjustProductName"></strong></p>
|
||||
<p>Current Stock: <strong id="adjustCurrentStock"></strong></p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Stock Adjustment</label>
|
||||
<input type="number" name="adjustment" id="adjustmentInput" class="form-input" required placeholder="e.g., 10 or -5">
|
||||
<small class="text-muted">Positive to add, negative to subtract</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Reason (optional)</label>
|
||||
<input type="text" name="reason" class="form-input" placeholder="e.g., Received shipment, Damaged goods">
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Low Stock Threshold</label>
|
||||
<input type="number" name="low_stock_threshold" id="adjustThreshold" class="form-input" min="0">
|
||||
<small class="text-muted">Alert when stock falls to this level</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('adjustModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openAdjustModal(productId, name, stock, threshold) {
|
||||
document.getElementById('adjustProductId').value = productId;
|
||||
document.getElementById('adjustProductName').textContent = name;
|
||||
document.getElementById('adjustCurrentStock').textContent = stock;
|
||||
document.getElementById('adjustThreshold').value = threshold;
|
||||
document.getElementById('adjustmentInput').value = '';
|
||||
Modal.open('adjustModal');
|
||||
}
|
||||
|
||||
// Toggle bulk adjustment inputs
|
||||
document.getElementById('bulkForm').addEventListener('toggle', function() {
|
||||
document.querySelectorAll('input[name^="adjustments"]').forEach(input => {
|
||||
input.style.display = this.style.display === 'none' ? 'none' : 'inline-block';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Admin Login
|
||||
*/
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
|
||||
if (AdminAuth::isLoggedIn()) {
|
||||
header('Location: /admin/');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$password = trim($_POST['password'] ?? '');
|
||||
if (AdminAuth::login($email, $password)) {
|
||||
$redirect = $_SESSION['admin_redirect'] ?? '/admin/';
|
||||
unset($_SESSION['admin_redirect']);
|
||||
header('Location: ' . $redirect);
|
||||
exit;
|
||||
}
|
||||
$error = 'Invalid email or password.';
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login — Tom's Java Jive</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#0f0f0f;font-family:Inter,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background-image:radial-gradient(circle at 30% 50%,rgba(255,94,26,.06),transparent 50%)}
|
||||
.box{background:#1a1a1a;border:1px solid #2a2a2a;border-radius:16px;padding:44px;width:100%;max-width:420px;box-shadow:0 24px 60px rgba(0,0,0,.5)}
|
||||
.logo{text-align:center;margin-bottom:32px}
|
||||
.logo h1{font-size:22px;font-weight:700;color:#FF5E1A;margin-bottom:4px}
|
||||
.logo p{font-size:13px;color:#555;letter-spacing:.5px;text-transform:uppercase}
|
||||
label{display:block;font-size:12px;font-weight:600;color:#777;text-transform:uppercase;letter-spacing:.5px;margin-bottom:7px}
|
||||
input{width:100%;background:#111;border:1.5px solid #2a2a2a;border-radius:8px;padding:13px 15px;color:#fff;font-family:Inter,sans-serif;font-size:15px;outline:none;margin-bottom:18px;transition:border-color .2s}
|
||||
input:focus{border-color:#FF5E1A}
|
||||
.btn{width:100%;padding:14px;border:none;border-radius:8px;background:#FF5E1A;color:#fff;font-family:Inter,sans-serif;font-weight:600;font-size:15px;cursor:pointer;transition:background .2s;margin-top:4px}
|
||||
.btn:hover{background:#e54d0f}
|
||||
.error{background:rgba(220,38,38,.1);border:1px solid rgba(220,38,38,.3);color:#f87171;padding:12px 15px;border-radius:8px;font-size:14px;margin-bottom:20px;display:flex;align-items:center;gap:8px}
|
||||
.back{display:block;text-align:center;margin-top:20px;color:#555;font-size:13px;text-decoration:none;transition:color .2s}
|
||||
.back:hover{color:#FF5E1A}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<div class="logo">
|
||||
<h1>☕ Tom's Java Jive</h1>
|
||||
<p>Admin Panel</p>
|
||||
</div>
|
||||
<?php if ($error): ?>
|
||||
<div class="error">⚠ <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST">
|
||||
<label>Email Address</label>
|
||||
<input type="email" name="email" placeholder="admin@tomsjavajive.com" required autocomplete="email">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" placeholder="••••••••" required autocomplete="current-password">
|
||||
<button type="submit" class="btn">Sign In to Admin</button>
|
||||
</form>
|
||||
<a href="/" class="back">← Back to Store</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Admin Logout
|
||||
*/
|
||||
require_once __DIR__ . '/../includes/auth.php';
|
||||
AdminAuth::logout();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: /admin/login.php');
|
||||
exit;
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Admin Order Detail
|
||||
*/
|
||||
|
||||
$pageTitle = 'Order Details';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
$orderId = $_GET['id'] ?? '';
|
||||
|
||||
if (empty($orderId)) {
|
||||
header('Location: /admin/orders.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
|
||||
|
||||
if (!$order) {
|
||||
setFlash('error', 'Order not found');
|
||||
header('Location: /admin/orders.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle status update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'update_status') {
|
||||
$status = $_POST['status'] ?? '';
|
||||
$trackingNumber = $_POST['tracking_number'] ?? '';
|
||||
|
||||
$updateData = ['order_status' => $status];
|
||||
if ($trackingNumber) {
|
||||
$updateData['tracking_number'] = $trackingNumber;
|
||||
}
|
||||
|
||||
db()->update('orders', $updateData, 'order_id = :id', ['id' => $orderId]);
|
||||
setFlash('success', 'Order status updated');
|
||||
header('Location: /admin/order.php?id=' . $orderId);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'add_note') {
|
||||
$note = trim($_POST['note'] ?? '');
|
||||
if ($note) {
|
||||
$existingNotes = $order['notes'] ?? '';
|
||||
$newNote = '[' . date('M j, Y g:i A') . '] ' . $note;
|
||||
$allNotes = $existingNotes ? $existingNotes . "\n" . $newNote : $newNote;
|
||||
|
||||
db()->update('orders', ['notes' => $allNotes], 'order_id = :id', ['id' => $orderId]);
|
||||
setFlash('success', 'Note added');
|
||||
header('Location: /admin/order.php?id=' . $orderId);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$items = json_decode($order['items'], true) ?? [];
|
||||
$shippingAddress = json_decode($order['shipping_address'], true) ?? [];
|
||||
|
||||
$statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<a href="/admin/orders.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</a>
|
||||
<h1 class="page-title">Order #<?= htmlspecialchars($order['order_number']) ?></h1>
|
||||
<?php if ($order['is_pos_order']): ?>
|
||||
<span class="badge badge-primary">POS Order</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button class="btn btn-secondary" onclick="window.print()">
|
||||
<i class="fas fa-print"></i> Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
|
||||
<!-- Main Column -->
|
||||
<div>
|
||||
<!-- Order Items -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Order Items</h3>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Price</th>
|
||||
<th>Qty</th>
|
||||
<th style="text-align: right;">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?php if (isset($item['product_id'])): ?>
|
||||
<a href="/admin/product-edit.php?id=<?= $item['product_id'] ?>">
|
||||
<?= htmlspecialchars($item['name']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($item['name']) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= formatCurrency($item['price']) ?></td>
|
||||
<td><?= $item['quantity'] ?></td>
|
||||
<td style="text-align: right;"><?= formatCurrency($item['total']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: right;">Subtotal</td>
|
||||
<td style="text-align: right;"><?= formatCurrency($order['subtotal']) ?></td>
|
||||
</tr>
|
||||
<?php if ($order['shipping_cost'] > 0): ?>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: right;">Shipping</td>
|
||||
<td style="text-align: right;"><?= formatCurrency($order['shipping_cost']) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if (($order['tax'] ?? 0) > 0): ?>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: right;">Tax</td>
|
||||
<td style="text-align: right;"><?= formatCurrency($order['tax']) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php if (($order['discount'] ?? 0) > 0): ?>
|
||||
<tr>
|
||||
<td colspan="3" style="text-align: right;">Discount</td>
|
||||
<td style="text-align: right; color: var(--admin-success);">-<?= formatCurrency($order['discount']) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr style="font-size: 1.125rem; font-weight: 600;">
|
||||
<td colspan="3" style="text-align: right;">Total</td>
|
||||
<td style="text-align: right;"><?= formatCurrency($order['total']) ?></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Order Notes</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<?php if ($order['notes']): ?>
|
||||
<pre style="white-space: pre-wrap; font-family: inherit; margin-bottom: 1rem; padding: 1rem; background: var(--admin-bg); border-radius: var(--admin-radius);"><?= htmlspecialchars($order['notes']) ?></pre>
|
||||
<?php else: ?>
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">No notes yet.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add_note">
|
||||
<div class="form-group mb-0">
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<input type="text" name="note" class="form-input" placeholder="Add a note...">
|
||||
<button type="submit" class="btn btn-secondary">Add Note</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div>
|
||||
<!-- Status -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Order Status</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="update_status">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<?php foreach ($statuses as $s): ?>
|
||||
<option value="<?= $s ?>" <?= $order['order_status'] === $s ? 'selected' : '' ?>>
|
||||
<?= ucfirst($s) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tracking Number</label>
|
||||
<input type="text" name="tracking_number" class="form-input" value="<?= htmlspecialchars($order['tracking_number'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">Update Status</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Customer</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<p><strong><?= htmlspecialchars($order['customer_name']) ?></strong></p>
|
||||
<p><?= htmlspecialchars($order['customer_email']) ?></p>
|
||||
<?php if ($order['customer_phone']): ?>
|
||||
<p><?= htmlspecialchars($order['customer_phone']) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($order['customer_id']): ?>
|
||||
<a href="/admin/customers.php?id=<?= $order['customer_id'] ?>" class="btn btn-sm btn-secondary mt-1">
|
||||
View Customer
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Address -->
|
||||
<?php if (!empty($shippingAddress) && ($shippingAddress['type'] ?? '') !== 'pickup'): ?>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Shipping Address</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<p>
|
||||
<?= htmlspecialchars($shippingAddress['address'] ?? '') ?><br>
|
||||
<?= htmlspecialchars($shippingAddress['city'] ?? '') ?>,
|
||||
<?= htmlspecialchars($shippingAddress['state'] ?? '') ?>
|
||||
<?= htmlspecialchars($shippingAddress['zip'] ?? '') ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Payment -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Payment</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span>Method</span>
|
||||
<span><?= ucfirst($order['payment_method'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span>Status</span>
|
||||
<?php
|
||||
$paymentClass = match($order['payment_status']) {
|
||||
'paid' => 'success',
|
||||
'failed' => 'error',
|
||||
'refunded' => 'warning',
|
||||
default => 'primary'
|
||||
};
|
||||
?>
|
||||
<span class="badge badge-<?= $paymentClass ?>"><?= ucfirst($order['payment_status']) ?></span>
|
||||
</div>
|
||||
<?php if ($order['stripe_payment_intent']): ?>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>Stripe ID</span>
|
||||
<span class="text-muted" style="font-size: 0.8rem;"><?= htmlspecialchars(substr($order['stripe_payment_intent'], 0, 20)) ?>...</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Timeline</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
|
||||
<span class="text-muted">Created</span>
|
||||
<span><?= formatDateTime($order['created_at']) ?></span>
|
||||
</div>
|
||||
<?php if ($order['updated_at'] && $order['updated_at'] !== $order['created_at']): ?>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span class="text-muted">Updated</span>
|
||||
<span><?= formatDateTime($order['updated_at']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Orders Page
|
||||
*/
|
||||
|
||||
$pageTitle = 'Orders';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle status update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$action = $_POST['action'];
|
||||
$orderId = $_POST['order_id'] ?? '';
|
||||
|
||||
if ($action === 'update_status' && $orderId) {
|
||||
$status = $_POST['status'] ?? '';
|
||||
$trackingNumber = $_POST['tracking_number'] ?? null;
|
||||
|
||||
$updateData = ['order_status' => $status];
|
||||
if ($trackingNumber) {
|
||||
$updateData['tracking_number'] = $trackingNumber;
|
||||
}
|
||||
|
||||
db()->update('orders', $updateData, 'order_id = :id', ['id' => $orderId]);
|
||||
setFlash('success', 'Order status updated');
|
||||
header('Location: /admin/orders.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
$status = $_GET['status'] ?? '';
|
||||
$search = $_GET['search'] ?? '';
|
||||
$dateFrom = $_GET['date_from'] ?? '';
|
||||
$dateTo = $_GET['date_to'] ?? '';
|
||||
$page = max(1, intval($_GET['page'] ?? 1));
|
||||
|
||||
// Build query
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if ($status) {
|
||||
$where[] = 'order_status = :status';
|
||||
$params['status'] = $status;
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$where[] = '(order_number LIKE :search OR customer_name LIKE :search OR customer_email LIKE :search)';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ($dateFrom) {
|
||||
$where[] = 'DATE(created_at) >= :date_from';
|
||||
$params['date_from'] = $dateFrom;
|
||||
}
|
||||
|
||||
if ($dateTo) {
|
||||
$where[] = 'DATE(created_at) <= :date_to';
|
||||
$params['date_to'] = $dateTo;
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $where);
|
||||
|
||||
// Get total and paginate
|
||||
$totalOrders = db()->count('orders', $whereClause, $params);
|
||||
$pagination = paginate($totalOrders, $page, ADMIN_ITEMS_PER_PAGE);
|
||||
|
||||
// Get orders
|
||||
$orders = db()->fetchAll(
|
||||
"SELECT * FROM orders WHERE {$whereClause} ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
array_merge($params, ['limit' => $pagination['per_page'], 'offset' => $pagination['offset']])
|
||||
);
|
||||
|
||||
$statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Orders</h1>
|
||||
<a href="/admin/pos.php" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> New Order (POS)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<?= getFlash('success') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: end;">
|
||||
<div class="form-group mb-0" style="flex: 1; min-width: 200px;">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" name="search" class="form-input" placeholder="Order #, name, email..."
|
||||
value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All Statuses</option>
|
||||
<?php foreach ($statuses as $s): ?>
|
||||
<option value="<?= $s ?>" <?= $status === $s ? 'selected' : '' ?>>
|
||||
<?= ucfirst($s) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">From Date</label>
|
||||
<input type="date" name="date_from" class="form-input" value="<?= $dateFrom ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">To Date</label>
|
||||
<input type="date" name="date_to" class="form-input" value="<?= $dateTo ?>">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
<i class="fas fa-filter"></i> Filter
|
||||
</button>
|
||||
|
||||
<?php if ($status || $search || $dateFrom || $dateTo): ?>
|
||||
<a href="/admin/orders.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<span><?= $totalOrders ?> orders found</span>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
<th>Customer</th>
|
||||
<th>Items</th>
|
||||
<th>Total</th>
|
||||
<th>Payment</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
<th style="width: 150px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($orders)): ?>
|
||||
<tr>
|
||||
<td colspan="8" class="text-muted" style="text-align: center; padding: 2rem;">
|
||||
No orders found
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($orders as $order):
|
||||
$items = json_decode($order['items'], true) ?? [];
|
||||
$itemCount = array_sum(array_column($items, 'quantity'));
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($order['order_number']) ?></strong>
|
||||
<?php if ($order['is_pos_order']): ?>
|
||||
<span class="badge badge-primary">POS</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div><?= htmlspecialchars($order['customer_name'] ?? 'Guest') ?></div>
|
||||
<small class="text-muted"><?= htmlspecialchars($order['customer_email']) ?></small>
|
||||
</td>
|
||||
<td><?= $itemCount ?> item<?= $itemCount !== 1 ? 's' : '' ?></td>
|
||||
<td><strong><?= formatCurrency($order['total']) ?></strong></td>
|
||||
<td>
|
||||
<?php
|
||||
$paymentClass = match($order['payment_status']) {
|
||||
'paid' => 'success',
|
||||
'failed' => 'error',
|
||||
'refunded' => 'warning',
|
||||
default => 'primary'
|
||||
};
|
||||
?>
|
||||
<span class="badge badge-<?= $paymentClass ?>">
|
||||
<?= ucfirst($order['payment_status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$statusClass = match($order['order_status']) {
|
||||
'pending' => 'warning',
|
||||
'confirmed', 'processing' => 'primary',
|
||||
'shipped', 'delivered' => 'success',
|
||||
'cancelled', 'refunded' => 'error',
|
||||
default => 'primary'
|
||||
};
|
||||
?>
|
||||
<span class="badge badge-<?= $statusClass ?>">
|
||||
<?= ucfirst($order['order_status']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted"><?= formatDate($order['created_at']) ?></td>
|
||||
<td>
|
||||
<a href="/admin/order.php?id=<?= $order['order_id'] ?>"
|
||||
class="btn btn-sm btn-secondary" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-secondary"
|
||||
onclick="openStatusModal('<?= $order['order_id'] ?>', '<?= $order['order_status'] ?>')"
|
||||
title="Update Status">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($pagination['total_pages'] > 1): ?>
|
||||
<div style="display: flex; justify-content: center; margin-top: 1rem;">
|
||||
<?= renderPagination($pagination, '/admin/orders.php?status=' . urlencode($status) . '&search=' . urlencode($search)) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Status Update Modal -->
|
||||
<div class="modal-overlay" id="statusModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Update Order Status</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('statusModal')">×</button>
|
||||
</div>
|
||||
<form method="POST" action="">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="update_status">
|
||||
<input type="hidden" name="order_id" id="modalOrderId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" id="modalStatus" class="form-select" required>
|
||||
<?php foreach ($statuses as $s): ?>
|
||||
<option value="<?= $s ?>"><?= ucfirst($s) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0" id="trackingGroup" style="display: none;">
|
||||
<label class="form-label">Tracking Number</label>
|
||||
<input type="text" name="tracking_number" class="form-input" placeholder="Enter tracking number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('statusModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Update Status</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openStatusModal(orderId, currentStatus) {
|
||||
document.getElementById('modalOrderId').value = orderId;
|
||||
document.getElementById('modalStatus').value = currentStatus;
|
||||
Modal.open('statusModal');
|
||||
}
|
||||
|
||||
document.getElementById('modalStatus').addEventListener('change', function() {
|
||||
document.getElementById('trackingGroup').style.display =
|
||||
this.value === 'shipped' ? 'block' : 'none';
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Payment Settings
|
||||
*/
|
||||
|
||||
$pageTitle = 'Payment Settings';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$section = $_POST['section'] ?? '';
|
||||
|
||||
if ($section === 'stripe') {
|
||||
setSetting('payment_stripe', [
|
||||
'enabled' => isset($_POST['stripe_enabled']),
|
||||
'test_mode' => isset($_POST['stripe_test_mode']),
|
||||
'publishable_key' => trim($_POST['stripe_publishable_key'] ?? ''),
|
||||
'secret_key' => trim($_POST['stripe_secret_key'] ?? ''),
|
||||
'webhook_secret' => trim($_POST['stripe_webhook_secret'] ?? '')
|
||||
]);
|
||||
setFlash('success', 'Stripe settings updated');
|
||||
}
|
||||
|
||||
if ($section === 'methods') {
|
||||
setSetting('payment_methods', [
|
||||
'card' => isset($_POST['method_card']),
|
||||
'cash' => isset($_POST['method_cash']),
|
||||
'wallet' => isset($_POST['method_wallet']),
|
||||
'gift_card' => isset($_POST['method_gift_card'])
|
||||
]);
|
||||
setFlash('success', 'Payment methods updated');
|
||||
}
|
||||
|
||||
header('Location: /admin/payments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$stripe = getSetting('payment_stripe', [
|
||||
'enabled' => true,
|
||||
'test_mode' => true,
|
||||
'publishable_key' => '',
|
||||
'secret_key' => '',
|
||||
'webhook_secret' => ''
|
||||
]);
|
||||
|
||||
$methods = getSetting('payment_methods', [
|
||||
'card' => true,
|
||||
'cash' => true,
|
||||
'wallet' => true,
|
||||
'gift_card' => true
|
||||
]);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Payment Settings</h1>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 200px 1fr; gap: 1.5rem;">
|
||||
<div>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0.5rem;">
|
||||
<a href="/admin/settings.php" class="nav-item"><i class="fas fa-store"></i> General</a>
|
||||
<a href="/admin/shipping.php" class="nav-item"><i class="fas fa-truck"></i> Shipping</a>
|
||||
<a href="/admin/payments.php" class="nav-item active"><i class="fas fa-credit-card"></i> Payments</a>
|
||||
<a href="/admin/emails.php" class="nav-item"><i class="fas fa-envelope"></i> Emails</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- Stripe Settings -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="stripe">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title"><i class="fab fa-stripe" style="color: #635BFF;"></i> Stripe</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="stripe_enabled" <?= $stripe['enabled'] ? 'checked' : '' ?>>
|
||||
Enable Stripe payments
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="stripe_test_mode" <?= $stripe['test_mode'] ? 'checked' : '' ?>>
|
||||
Test mode (use test keys)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Publishable Key</label>
|
||||
<input type="text" name="stripe_publishable_key" class="form-input"
|
||||
value="<?= htmlspecialchars($stripe['publishable_key']) ?>"
|
||||
placeholder="pk_test_... or pk_live_...">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Secret Key</label>
|
||||
<input type="password" name="stripe_secret_key" class="form-input"
|
||||
value="<?= htmlspecialchars($stripe['secret_key']) ?>"
|
||||
placeholder="sk_test_... or sk_live_...">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Webhook Secret</label>
|
||||
<input type="password" name="stripe_webhook_secret" class="form-input"
|
||||
value="<?= htmlspecialchars($stripe['webhook_secret']) ?>"
|
||||
placeholder="whsec_...">
|
||||
<small class="text-muted">Get this from your Stripe webhook settings</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2">Save Stripe Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- POS Payment Methods -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="methods">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">POS Payment Methods</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<p class="text-muted" style="margin-bottom: 1rem;">Select which payment methods are available in the Point of Sale system.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="method_card" <?= $methods['card'] ? 'checked' : '' ?>>
|
||||
<i class="fas fa-credit-card"></i> Card (Terminal)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="method_cash" <?= $methods['cash'] ? 'checked' : '' ?>>
|
||||
<i class="fas fa-money-bill"></i> Cash
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="method_wallet" <?= $methods['wallet'] ? 'checked' : '' ?>>
|
||||
<i class="fas fa-wallet"></i> Customer Wallet
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="method_gift_card" <?= $methods['gift_card'] ? 'checked' : '' ?>>
|
||||
<i class="fas fa-gift"></i> Gift Card
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2">Save Payment Methods</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
+1386
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Product Edit/Add
|
||||
*/
|
||||
|
||||
$pageTitle = 'Product';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Load categories and product types for dropdowns
|
||||
$categoriesList = db()->fetchAll("SELECT category_id, name FROM categories WHERE is_active = 1 ORDER BY name ASC");
|
||||
$productTypesList = db()->fetchAll("SELECT type_id, name FROM product_types WHERE is_active = 1 ORDER BY sort_order ASC, name ASC");
|
||||
|
||||
|
||||
$productId = $_GET['id'] ?? '';
|
||||
$product = null;
|
||||
$isEdit = false;
|
||||
$errors = [];
|
||||
|
||||
if ($productId) {
|
||||
$product = db()->fetch("SELECT * FROM products WHERE product_id = :id", ['id' => $productId]);
|
||||
if ($product) {
|
||||
$isEdit = true;
|
||||
$pageTitle = 'Edit Product';
|
||||
$product['images'] = json_decode($product['images'] ?? '[]', true);
|
||||
$product['tags'] = json_decode($product['tags'] ?? '[]', true);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
$product = [
|
||||
'product_id' => '',
|
||||
'name' => '',
|
||||
'description' => '',
|
||||
'price' => '',
|
||||
'sale_price' => '',
|
||||
'cost_price' => '',
|
||||
'sku' => '',
|
||||
'barcode' => '',
|
||||
'category' => '',
|
||||
'tags' => [],
|
||||
'images' => [],
|
||||
'stock' => 100,
|
||||
'low_stock_threshold' => 10,
|
||||
'weight' => '',
|
||||
'is_active' => 1,
|
||||
'is_featured' => 0
|
||||
];
|
||||
$pageTitle = 'Add Product';
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$price = floatval($_POST['price'] ?? 0);
|
||||
$salePrice = !empty($_POST['sale_price']) ? floatval($_POST['sale_price']) : null;
|
||||
$costPrice = !empty($_POST['cost_price']) ? floatval($_POST['cost_price']) : null;
|
||||
$sku = trim($_POST['sku'] ?? '');
|
||||
$barcode = trim($_POST['barcode'] ?? '');
|
||||
$category = trim($_POST['category'] ?? '');
|
||||
$stock = intval($_POST['stock'] ?? 0);
|
||||
$lowStockThreshold = intval($_POST['low_stock_threshold'] ?? 10);
|
||||
$weight = !empty($_POST['weight']) ? floatval($_POST['weight']) : null;
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
$isFeatured = isset($_POST['is_featured']) ? 1 : 0;
|
||||
$imageUrls = array_filter(array_map('trim', explode("\n", $_POST['image_urls'] ?? '')));
|
||||
|
||||
// Validate
|
||||
if (empty($name)) $errors['name'] = 'Product name is required';
|
||||
if ($price <= 0) $errors['price'] = 'Price must be greater than 0';
|
||||
|
||||
if (empty($errors)) {
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'price' => $price,
|
||||
'sale_price' => $salePrice,
|
||||
'cost_price' => $costPrice,
|
||||
'sku' => $sku ?: null,
|
||||
'barcode' => $barcode ?: null,
|
||||
'category' => $category ?: null,
|
||||
'product_type_id' => trim($_POST['product_type_id'] ?? '') ?: null,
|
||||
'images' => json_encode($imageUrls),
|
||||
'stock' => $stock,
|
||||
'low_stock_threshold' => $lowStockThreshold,
|
||||
'weight' => $weight,
|
||||
'is_active' => $isActive,
|
||||
'is_featured' => $isFeatured
|
||||
];
|
||||
|
||||
if ($isEdit) {
|
||||
db()->update('products', $data, 'product_id = :id', ['id' => $productId]);
|
||||
setFlash('success', 'Product updated successfully');
|
||||
} else {
|
||||
$data['product_id'] = generateId('prod_');
|
||||
db()->insert('products', $data);
|
||||
setFlash('success', 'Product created successfully');
|
||||
}
|
||||
|
||||
header('Location: /admin/products.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Keep form values on error
|
||||
$product = array_merge($product, $_POST);
|
||||
$product['images'] = $imageUrls;
|
||||
}
|
||||
|
||||
// Get categories for dropdown
|
||||
$categories = db()->fetchAll(
|
||||
"SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != '' ORDER BY category"
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title"><?= $isEdit ? 'Edit' : 'Add' ?> Product</h1>
|
||||
<a href="/admin/products.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Products
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="alert alert-error">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
Please fix the errors below
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="">
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; align-items: start;">
|
||||
<!-- Main Info -->
|
||||
<div>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Basic Information</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Product Name *</label>
|
||||
<input type="text" name="name" class="form-input"
|
||||
value="<?= htmlspecialchars($product['name']) ?>" required>
|
||||
<?php if (isset($errors['name'])): ?>
|
||||
<span class="form-error"><?= $errors['name'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" class="form-textarea" rows="4"><?= htmlspecialchars($product['description']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-input">
|
||||
<option value="">-- Select Category --</option>
|
||||
<?php foreach ($categoriesList as $cat): ?>
|
||||
<option value="<?= htmlspecialchars($cat['name']) ?>"
|
||||
<?= ($product['category'] ?? '') === $cat['name'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($cat['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Product Type</label>
|
||||
<select name="product_type_id" class="form-input">
|
||||
<option value="">-- Select Type --</option>
|
||||
<?php foreach ($productTypesList as $pt): ?>
|
||||
<option value="<?= htmlspecialchars($pt['type_id']) ?>"
|
||||
<?= ($product['product_type_id'] ?? '') === $pt['type_id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($pt['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Weight (oz)</label>
|
||||
<input type="number" name="weight" class="form-input" step="0.01"
|
||||
value="<?= htmlspecialchars($product['weight']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Pricing</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Price *</label>
|
||||
<input type="number" name="price" class="form-input" step="0.01" min="0"
|
||||
value="<?= htmlspecialchars($product['price']) ?>" required>
|
||||
<?php if (isset($errors['price'])): ?>
|
||||
<span class="form-error"><?= $errors['price'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Sale Price</label>
|
||||
<input type="number" name="sale_price" class="form-input" step="0.01" min="0"
|
||||
value="<?= htmlspecialchars($product['sale_price'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Cost Price (for profit tracking)</label>
|
||||
<input type="number" name="cost_price" class="form-input" step="0.01" min="0"
|
||||
value="<?= htmlspecialchars($product['cost_price'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Images</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><div class="form-group">
|
||||
<label class="form-label">Product Images</label>
|
||||
|
||||
<!-- Drag & Drop Upload Zone -->
|
||||
<div id="dropZone" style="border:2px dashed var(--color-border);border-radius:var(--radius-md);padding:2rem;text-align:center;cursor:pointer;transition:all .2s;margin-bottom:1rem;background:var(--color-background)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)" onclick="document.getElementById('imageFileInput').click()">
|
||||
<i class="fas fa-cloud-upload-alt" style="font-size:2rem;color:var(--color-text-muted);margin-bottom:.5rem;display:block"></i>
|
||||
<div style="font-weight:600;margin-bottom:.25rem">Drag & drop images here</div>
|
||||
<div style="font-size:.875rem;color:var(--color-text-muted)">or click to browse — JPG, PNG, WebP, GIF up to 5MB each</div>
|
||||
<input type="file" id="imageFileInput" multiple accept="image/*" style="display:none" onchange="handleFileSelect(this.files)">
|
||||
</div>
|
||||
|
||||
<!-- Upload Preview -->
|
||||
<div id="uploadPreviews" style="display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem"></div>
|
||||
|
||||
<!-- URL Input -->
|
||||
<div style="position:relative;margin-bottom:.5rem">
|
||||
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem;color:var(--color-text-muted);font-size:.875rem">
|
||||
<hr style="flex:1;border:none;border-top:1px solid var(--color-border)">
|
||||
<span>or add image URLs</span>
|
||||
<hr style="flex:1;border:none;border-top:1px solid var(--color-border)">
|
||||
</div>
|
||||
<textarea name="images" id="imageUrls" class="form-textarea" rows="3" placeholder="https://example.com/image1.jpg https://example.com/image2.jpg"><?= htmlspecialchars(is_array($product['images']) ? implode("
|
||||
", $product['images']) : ($product['images'] ?? '')) ?></textarea>
|
||||
<small class="text-muted">Enter one URL per line</small>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('dropZone').style.borderColor = 'var(--color-primary)';
|
||||
document.getElementById('dropZone').style.background = 'rgba(255,94,26,.05)';
|
||||
}
|
||||
function handleDragLeave(e) {
|
||||
document.getElementById('dropZone').style.borderColor = 'var(--color-border)';
|
||||
document.getElementById('dropZone').style.background = 'var(--color-background)';
|
||||
}
|
||||
function handleDrop(e) {
|
||||
e.preventDefault();
|
||||
handleDragLeave(e);
|
||||
handleFileSelect(e.dataTransfer.files);
|
||||
}
|
||||
function handleFileSelect(files) {
|
||||
Array.from(files).forEach(file => {
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert(file.name + ' is too large (max 5MB)');
|
||||
return;
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
uploadImage(file, e.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
function uploadImage(file, dataUrl) {
|
||||
var preview = document.createElement('div');
|
||||
preview.style.cssText = 'position:relative;width:100px;height:100px;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--color-border)';
|
||||
preview.innerHTML = '<img src="' + dataUrl + '" style="width:100%;height:100%;object-fit:cover">' +
|
||||
'<div style="position:absolute;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center">' +
|
||||
'<i class="fas fa-spinner fa-spin" style="color:#fff;font-size:1.25rem"></i></div>';
|
||||
document.getElementById('uploadPreviews').appendChild(preview);
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('action', 'upload_image');
|
||||
|
||||
fetch('/admin/upload-image.php', {method:'POST', body:formData})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.url) {
|
||||
preview.innerHTML = '<img src="' + data.url + '" style="width:100%;height:100%;object-fit:cover">' +
|
||||
'<button type="button" onclick="removePreview(this, '' + data.url + '')" style="position:absolute;top:2px;right:2px;background:rgba(0,0,0,.6);border:none;color:#fff;border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:.7rem;display:flex;align-items:center;justify-content:center">×</button>';
|
||||
var urls = document.getElementById('imageUrls');
|
||||
urls.value = (urls.value ? urls.value + '
|
||||
' : '') + data.url;
|
||||
} else {
|
||||
preview.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:.75rem;color:var(--color-error);padding:.5rem;text-align:center">' + (data.error || 'Upload failed') + '</div>';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
preview.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:.75rem;color:var(--color-error);padding:.5rem;text-align:center">Upload failed</div>';
|
||||
});
|
||||
}
|
||||
function removePreview(btn, url) {
|
||||
btn.closest('div').remove();
|
||||
var urls = document.getElementById('imageUrls');
|
||||
urls.value = urls.value.split('
|
||||
').filter(u => u.trim() !== url.trim()).join('
|
||||
');
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($product['images'])): ?>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem;">
|
||||
<?php foreach ($product['images'] as $img): ?>
|
||||
<img src="<?= htmlspecialchars($img) ?>" alt="Product image"
|
||||
style="width: 80px; height: 80px; object-fit: cover; border-radius: var(--admin-radius);">
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Status</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="is_active" value="1"
|
||||
<?= $product['is_active'] ? 'checked' : '' ?>>
|
||||
Active (visible in store)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="is_featured" value="1"
|
||||
<?= $product['is_featured'] ? 'checked' : '' ?>>
|
||||
Featured product
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Inventory</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Stock Quantity</label>
|
||||
<input type="number" name="stock" class="form-input" min="0"
|
||||
value="<?= htmlspecialchars($product['stock']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Low Stock Alert Threshold</label>
|
||||
<input type="number" name="low_stock_threshold" class="form-input" min="0"
|
||||
value="<?= htmlspecialchars($product['low_stock_threshold']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Identifiers</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">SKU</label>
|
||||
<input type="text" name="sku" class="form-input"
|
||||
value="<?= htmlspecialchars($product['sku'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Barcode</label>
|
||||
<input type="text" name="barcode" class="form-input"
|
||||
value="<?= htmlspecialchars($product['barcode'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
<i class="fas fa-save"></i> <?= $isEdit ? 'Update' : 'Create' ?> Product
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$pageTitle = 'Product Types';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create' || $action === 'update') {
|
||||
$typeId = $_POST['type_id'] ?? '';
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$slug = trim($_POST['slug'] ?? '') ?: slugify($name);
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
if (empty($name)) {
|
||||
setFlash('error', 'Product type name is required');
|
||||
} else {
|
||||
$data = ['name'=>$name,'slug'=>$slug,'description'=>$description,'is_active'=>$isActive];
|
||||
if ($action === 'update' && $typeId) {
|
||||
db()->update('product_types', $data, 'type_id = :id', ['id' => $typeId]);
|
||||
setFlash('success', 'Product type updated');
|
||||
} else {
|
||||
$data['type_id'] = generateId('pt_');
|
||||
db()->insert('product_types', $data);
|
||||
setFlash('success', 'Product type created');
|
||||
}
|
||||
}
|
||||
header('Location: /admin/product-types.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete' && !empty($_POST['type_id'])) {
|
||||
db()->delete('product_types', 'type_id = :id', ['id' => $_POST['type_id']]);
|
||||
setFlash('success', 'Product type deleted');
|
||||
header('Location: /admin/product-types.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$types = db()->fetchAll("SELECT * FROM product_types ORDER BY name ASC");
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Product Types</h1>
|
||||
<button class="btn btn-primary" onclick="openTypeModal()">
|
||||
<i class="fas fa-plus"></i> Add Product Type
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($types)): ?>
|
||||
<tr><td colspan="5" class="text-muted" style="text-align:center;padding:2rem">No product types yet. Create one above.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($types as $t): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($t['name']) ?></strong></td>
|
||||
<td class="text-muted"><?= htmlspecialchars($t['slug']) ?></td>
|
||||
<td class="text-muted"><?= htmlspecialchars(substr($t['description'] ?? '', 0, 60)) ?></td>
|
||||
<td>
|
||||
<?php if ($t['is_active']): ?>
|
||||
<span class="badge badge-success">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-error">Hidden</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openTypeModal(<?= json_encode($t) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="type_id" value="<?= htmlspecialchars($t['type_id']) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this product type?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="typeModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="typeModalTitle">Add Product Type</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('typeModal')">×</button>
|
||||
</div>
|
||||
<form method="POST">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="typeAction" value="create">
|
||||
<input type="hidden" name="type_id" id="typeId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Product Type Name *</label>
|
||||
<input type="text" name="name" id="typeName" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Slug</label>
|
||||
<input type="text" name="slug" id="typeSlug" class="form-input" placeholder="auto-generated if empty">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" id="typeDesc" class="form-textarea" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="is_active" id="typeActive" checked>
|
||||
Active (visible in product forms)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('typeModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="typeSubmitBtn">Add Product Type</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openTypeModal(type) {
|
||||
type = type || null;
|
||||
var isEdit = !!type;
|
||||
document.getElementById('typeModalTitle').textContent = isEdit ? 'Edit Product Type' : 'Add Product Type';
|
||||
document.getElementById('typeSubmitBtn').textContent = isEdit ? 'Update Product Type' : 'Add Product Type';
|
||||
document.getElementById('typeAction').value = isEdit ? 'update' : 'create';
|
||||
document.getElementById('typeId').value = isEdit ? type.type_id : '';
|
||||
document.getElementById('typeName').value = isEdit ? type.name : '';
|
||||
document.getElementById('typeSlug').value = isEdit ? type.slug : '';
|
||||
document.getElementById('typeDesc').value = isEdit ? (type.description || '') : '';
|
||||
document.getElementById('typeActive').checked = isEdit ? type.is_active == 1 : true;
|
||||
Modal.open('typeModal');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Products Page
|
||||
*/
|
||||
|
||||
$pageTitle = 'Products';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'delete' && !empty($_POST['product_id'])) {
|
||||
db()->delete('products', 'product_id = :id', ['id' => $_POST['product_id']]);
|
||||
setFlash('success', 'Product deleted successfully');
|
||||
header('Location: /admin/products.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'bulk_delete' && !empty($_POST['product_ids'])) {
|
||||
$ids = $_POST['product_ids'];
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
||||
db()->query("DELETE FROM products WHERE product_id IN ($placeholders)", $ids);
|
||||
setFlash('success', count($ids) . ' products deleted');
|
||||
header('Location: /admin/products.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'toggle_status' && !empty($_POST['product_id'])) {
|
||||
$product = db()->fetch("SELECT is_active FROM products WHERE product_id = :id", ['id' => $_POST['product_id']]);
|
||||
if ($product) {
|
||||
db()->update('products', ['is_active' => !$product['is_active']], 'product_id = :id', ['id' => $_POST['product_id']]);
|
||||
setFlash('success', 'Product status updated');
|
||||
}
|
||||
header('Location: /admin/products.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters
|
||||
$search = $_GET['search'] ?? '';
|
||||
$category = $_GET['category'] ?? '';
|
||||
$status = $_GET['status'] ?? '';
|
||||
$page = max(1, intval($_GET['page'] ?? 1));
|
||||
|
||||
// Build query
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if ($search) {
|
||||
$where[] = '(name LIKE :search OR sku LIKE :search)';
|
||||
$params['search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ($category) {
|
||||
$where[] = 'category = :category';
|
||||
$params['category'] = $category;
|
||||
}
|
||||
|
||||
if ($status === 'active') {
|
||||
$where[] = 'is_active = 1';
|
||||
} elseif ($status === 'inactive') {
|
||||
$where[] = 'is_active = 0';
|
||||
} elseif ($status === 'low_stock') {
|
||||
$where[] = 'stock <= low_stock_threshold';
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $where);
|
||||
|
||||
// Get total and paginate
|
||||
$totalProducts = db()->count('products', $whereClause, $params);
|
||||
$pagination = paginate($totalProducts, $page, ADMIN_ITEMS_PER_PAGE);
|
||||
|
||||
// Get products
|
||||
$products = db()->fetchAll(
|
||||
"SELECT * FROM products WHERE {$whereClause} ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||
array_merge($params, ['limit' => $pagination['per_page'], 'offset' => $pagination['offset']])
|
||||
);
|
||||
|
||||
// Get categories
|
||||
$categories = db()->fetchAll(
|
||||
"SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != '' ORDER BY category"
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Products</h1>
|
||||
<a href="/admin/product-edit.php" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Add Product
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<?= getFlash('success') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: end;">
|
||||
<div class="form-group mb-0" style="flex: 1; min-width: 200px;">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" name="search" class="form-input" placeholder="Name or SKU..."
|
||||
value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Category</label>
|
||||
<select name="category" class="form-select">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?= htmlspecialchars($cat['category']) ?>"
|
||||
<?= $category === $cat['category'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars(ucfirst($cat['category'])) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All</option>
|
||||
<option value="active" <?= $status === 'active' ? 'selected' : '' ?>>Active</option>
|
||||
<option value="inactive" <?= $status === 'inactive' ? 'selected' : '' ?>>Inactive</option>
|
||||
<option value="low_stock" <?= $status === 'low_stock' ? 'selected' : '' ?>>Low Stock</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
<i class="fas fa-filter"></i> Filter
|
||||
</button>
|
||||
|
||||
<?php if ($search || $category || $status): ?>
|
||||
<a href="/admin/products.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<span><?= $totalProducts ?> products found</span>
|
||||
<div id="bulkActions" style="display: none;">
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="bulk_delete">
|
||||
<input type="hidden" name="product_ids" id="selectedIds">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete selected products?">
|
||||
<i class="fas fa-trash"></i> Delete Selected
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;">
|
||||
<input type="checkbox" id="selectAll">
|
||||
</th>
|
||||
<th style="width: 60px;">Image</th>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Category</th>
|
||||
<th>Price</th>
|
||||
<th>Stock</th>
|
||||
<th>Status</th>
|
||||
<th style="width: 120px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($products)): ?>
|
||||
<tr>
|
||||
<td colspan="9" class="text-muted" style="text-align: center; padding: 2rem;">
|
||||
No products found
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($products as $product):
|
||||
$images = json_decode($product['images'] ?? '[]', true);
|
||||
$imageUrl = !empty($images) ? $images[0] : '/assets/images/placeholder-product.jpg';
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="product-checkbox" value="<?= $product['product_id'] ?>">
|
||||
</td>
|
||||
<td>
|
||||
<img src="<?= htmlspecialchars($imageUrl) ?>" alt=""
|
||||
style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;">
|
||||
</td>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($product['name']) ?></strong>
|
||||
<?php if ($product['is_featured']): ?>
|
||||
<span class="badge badge-primary">Featured</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-muted"><?= htmlspecialchars($product['sku'] ?? '-') ?></td>
|
||||
<td><?= htmlspecialchars(ucfirst($product['category'] ?? '-')) ?></td>
|
||||
<td>
|
||||
<?php if ($product['sale_price'] && $product['sale_price'] < $product['price']): ?>
|
||||
<span style="text-decoration: line-through; color: var(--admin-text-light);">
|
||||
<?= formatCurrency($product['price']) ?>
|
||||
</span><br>
|
||||
<strong class="text-success"><?= formatCurrency($product['sale_price']) ?></strong>
|
||||
<?php else: ?>
|
||||
<?= formatCurrency($product['price']) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($product['stock'] <= 0): ?>
|
||||
<span class="badge badge-error">Out of Stock</span>
|
||||
<?php elseif ($product['stock'] <= $product['low_stock_threshold']): ?>
|
||||
<span class="badge badge-warning"><?= $product['stock'] ?> left</span>
|
||||
<?php else: ?>
|
||||
<span class="text-success"><?= $product['stock'] ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($product['is_active']): ?>
|
||||
<span class="badge badge-success">Active</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-error">Inactive</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/product-edit.php?id=<?= $product['product_id'] ?>"
|
||||
class="btn btn-sm btn-secondary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="toggle_status">
|
||||
<input type="hidden" name="product_id" value="<?= $product['product_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-secondary"
|
||||
title="<?= $product['is_active'] ? 'Deactivate' : 'Activate' ?>">
|
||||
<i class="fas fa-<?= $product['is_active'] ? 'eye-slash' : 'eye' ?>"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="product_id" value="<?= $product['product_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
data-confirm="Delete this product?" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($pagination['total_pages'] > 1): ?>
|
||||
<div style="display: flex; justify-content: center; margin-top: 1rem;">
|
||||
<?= renderPagination($pagination, '/admin/products.php?search=' . urlencode($search) . '&category=' . urlencode($category) . '&status=' . $status) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
// Bulk selection
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
const checkboxes = document.querySelectorAll('.product-checkbox');
|
||||
const bulkActions = document.getElementById('bulkActions');
|
||||
const selectedIds = document.getElementById('selectedIds');
|
||||
|
||||
selectAll.addEventListener('change', function() {
|
||||
checkboxes.forEach(cb => cb.checked = this.checked);
|
||||
updateBulkActions();
|
||||
});
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
cb.addEventListener('change', updateBulkActions);
|
||||
});
|
||||
|
||||
function updateBulkActions() {
|
||||
const checked = document.querySelectorAll('.product-checkbox:checked');
|
||||
bulkActions.style.display = checked.length > 0 ? 'block' : 'none';
|
||||
selectedIds.value = Array.from(checked).map(cb => cb.value).join(',');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Reviews
|
||||
*/
|
||||
|
||||
$pageTitle = 'Reviews';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$reviewId = $_POST['review_id'] ?? '';
|
||||
|
||||
if ($action === 'approve' && $reviewId) {
|
||||
db()->update('reviews', ['is_approved' => 1], 'review_id = :id', ['id' => $reviewId]);
|
||||
setFlash('success', 'Review approved');
|
||||
}
|
||||
|
||||
if ($action === 'reject' && $reviewId) {
|
||||
db()->update('reviews', ['is_approved' => 0], 'review_id = :id', ['id' => $reviewId]);
|
||||
setFlash('success', 'Review rejected');
|
||||
}
|
||||
|
||||
if ($action === 'update' && $reviewId) {
|
||||
$rating = max(1, min(5, intval($_POST['rating'] ?? 5)));
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$comment = trim($_POST['comment'] ?? '');
|
||||
db()->update('reviews', [
|
||||
'rating' => $rating,
|
||||
'title' => $title ?: null,
|
||||
'comment' => $comment ?: null,
|
||||
], 'review_id = :id', ['id' => $reviewId]);
|
||||
setFlash('success', 'Review updated');
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $reviewId) {
|
||||
db()->delete('reviews', 'review_id = :id', ['id' => $reviewId]);
|
||||
setFlash('success', 'Review deleted');
|
||||
}
|
||||
|
||||
header('Location: /admin/reviews.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Filters
|
||||
$status = $_GET['status'] ?? '';
|
||||
$rating = $_GET['rating'] ?? '';
|
||||
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
if ($status === 'pending') {
|
||||
$where[] = 'is_approved = 0';
|
||||
} elseif ($status === 'approved') {
|
||||
$where[] = 'is_approved = 1';
|
||||
}
|
||||
|
||||
if ($rating) {
|
||||
$where[] = 'rating = :rating';
|
||||
$params['rating'] = $rating;
|
||||
}
|
||||
|
||||
$whereClause = implode(' AND ', $where);
|
||||
|
||||
$reviews = db()->fetchAll(
|
||||
"SELECT r.*, p.name as product_name FROM reviews r
|
||||
LEFT JOIN products p ON r.product_id = p.product_id
|
||||
WHERE {$whereClause} ORDER BY r.created_at DESC LIMIT 100",
|
||||
$params
|
||||
);
|
||||
|
||||
// Stats
|
||||
$totalReviews = db()->count('reviews');
|
||||
$pendingReviews = db()->count('reviews', 'is_approved = 0');
|
||||
$avgRating = db()->fetch("SELECT AVG(rating) as avg FROM reviews WHERE is_approved = 1")['avg'] ?? 0;
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Product Reviews</h1>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid" style="margin-bottom: 1.5rem;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon primary"><i class="fas fa-star"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= number_format($avgRating, 1) ?></div>
|
||||
<div class="stat-card-label">Average Rating</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon success"><i class="fas fa-comments"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= $totalReviews ?></div>
|
||||
<div class="stat-card-label">Total Reviews</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon <?= $pendingReviews > 0 ? 'warning' : 'success' ?>"><i class="fas fa-clock"></i></div>
|
||||
<div>
|
||||
<div class="stat-card-value"><?= $pendingReviews ?></div>
|
||||
<div class="stat-card-label">Pending Approval</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body">
|
||||
<form method="GET" style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: end;">
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Status</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="">All</option>
|
||||
<option value="pending" <?= $status === 'pending' ? 'selected' : '' ?>>Pending</option>
|
||||
<option value="approved" <?= $status === 'approved' ? 'selected' : '' ?>>Approved</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Rating</label>
|
||||
<select name="rating" class="form-select">
|
||||
<option value="">All Ratings</option>
|
||||
<?php for ($i = 5; $i >= 1; $i--): ?>
|
||||
<option value="<?= $i ?>" <?= $rating == $i ? 'selected' : '' ?>><?= $i ?> Star<?= $i > 1 ? 's' : '' ?></option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary"><i class="fas fa-filter"></i> Filter</button>
|
||||
<?php if ($status || $rating): ?>
|
||||
<a href="/admin/reviews.php" class="btn btn-secondary">Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reviews List -->
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<?php if (empty($reviews)): ?>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body text-center text-muted" style="padding: 3rem;">
|
||||
No reviews found
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($reviews as $review): ?>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<strong><?= htmlspecialchars($review['customer_name'] ?? 'Anonymous') ?></strong>
|
||||
<?php if ($review['is_verified_purchase']): ?>
|
||||
<span class="badge badge-success">Verified Purchase</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!$review['is_approved']): ?>
|
||||
<span class="badge badge-warning">Pending</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="text-muted" style="font-size: 0.875rem;">
|
||||
on <a href="/admin/product-edit.php?id=<?= $review['product_id'] ?>"><?= htmlspecialchars($review['product_name'] ?? 'Unknown Product') ?></a>
|
||||
• <?= formatDate($review['created_at']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<?php if (!$review['is_approved']): ?>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<input type="hidden" name="review_id" value="<?= $review['review_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-success"><i class="fas fa-check"></i> Approve</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<input type="hidden" name="review_id" value="<?= $review['review_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-secondary"><i class="fas fa-times"></i> Unapprove</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn btn-sm btn-secondary" title="Edit"
|
||||
onclick='openEditReview(<?= json_encode(['review_id' => $review['review_id'], 'rating' => $review['rating'], 'title' => $review['title'], 'comment' => $review['comment']]) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="review_id" value="<?= $review['review_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this review?"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 0.75rem;">
|
||||
<?php for ($i = 1; $i <= 5; $i++): ?>
|
||||
<i class="fas fa-star" style="color: <?= $i <= $review['rating'] ? '#F59E0B' : '#E5E7EB' ?>;"></i>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($review['title']): ?>
|
||||
<h4 style="margin-bottom: 0.5rem;"><?= htmlspecialchars($review['title']) ?></h4>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="margin: 0; color: var(--admin-text);">
|
||||
<?= nl2br(htmlspecialchars($review['comment'])) ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Edit Review Modal -->
|
||||
<div class="modal-overlay" id="editReviewModal">
|
||||
<div class="modal" style="max-width:560px;width:95vw">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Edit Review</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('editReviewModal')">×</button>
|
||||
</div>
|
||||
<form method="POST" id="editReviewForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="update">
|
||||
<input type="hidden" name="review_id" id="editReviewId">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Rating</label>
|
||||
<div id="starRating" style="display:flex;gap:.25rem;font-size:1.5rem;cursor:pointer;margin-bottom:.25rem">
|
||||
<?php for ($s = 1; $s <= 5; $s++): ?>
|
||||
<i class="fas fa-star" data-star="<?= $s ?>" style="color:#E5E7EB"></i>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
<input type="hidden" name="rating" id="editRating" value="5">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" name="title" id="editTitle" class="form-input" placeholder="Review title (optional)">
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Comment</label>
|
||||
<textarea name="comment" id="editComment" class="form-input" rows="5" style="resize:vertical"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('editReviewModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openEditReview(review) {
|
||||
document.getElementById('editReviewId').value = review.review_id;
|
||||
document.getElementById('editTitle').value = review.title || '';
|
||||
document.getElementById('editComment').value = review.comment || '';
|
||||
setStarRating(review.rating || 5);
|
||||
Modal.open('editReviewModal');
|
||||
}
|
||||
|
||||
function setStarRating(val) {
|
||||
document.getElementById('editRating').value = val;
|
||||
document.querySelectorAll('#starRating i').forEach(function(star) {
|
||||
star.style.color = parseInt(star.dataset.star) <= val ? '#F59E0B' : '#E5E7EB';
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('#starRating i').forEach(function(star) {
|
||||
star.addEventListener('click', function() { setStarRating(parseInt(this.dataset.star)); });
|
||||
star.addEventListener('mouseenter', function() {
|
||||
var val = parseInt(this.dataset.star);
|
||||
document.querySelectorAll('#starRating i').forEach(function(s) {
|
||||
s.style.color = parseInt(s.dataset.star) <= val ? '#F59E0B' : '#E5E7EB';
|
||||
});
|
||||
});
|
||||
star.addEventListener('mouseleave', function() {
|
||||
setStarRating(parseInt(document.getElementById('editRating').value));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Settings
|
||||
*/
|
||||
|
||||
$pageTitle = 'Store Settings';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$section = $_POST['section'] ?? '';
|
||||
|
||||
if ($section === 'general') {
|
||||
setSetting('store', [
|
||||
'name' => trim($_POST['store_name'] ?? ''),
|
||||
'email' => trim($_POST['store_email'] ?? ''),
|
||||
'phone' => trim($_POST['store_phone'] ?? ''),
|
||||
'address' => trim($_POST['store_address'] ?? ''),
|
||||
'currency' => $_POST['currency'] ?? 'USD',
|
||||
'timezone' => $_POST['timezone'] ?? 'America/New_York'
|
||||
]);
|
||||
setFlash('success', 'Store settings updated');
|
||||
}
|
||||
|
||||
if ($section === 'tax') {
|
||||
setSetting('tax', [
|
||||
'enabled' => isset($_POST['tax_enabled']),
|
||||
'rate' => floatval($_POST['tax_rate'] ?? 0),
|
||||
'included_in_price' => isset($_POST['tax_included'])
|
||||
]);
|
||||
setFlash('success', 'Tax settings updated');
|
||||
}
|
||||
|
||||
if ($section === 'checkout') {
|
||||
setSetting('checkout', [
|
||||
'guest_checkout' => isset($_POST['guest_checkout']),
|
||||
'require_phone' => isset($_POST['require_phone']),
|
||||
'order_notes' => isset($_POST['order_notes']),
|
||||
'terms_required' => isset($_POST['terms_required']),
|
||||
'terms_url' => trim($_POST['terms_url'] ?? '')
|
||||
]);
|
||||
setFlash('success', 'Checkout settings updated');
|
||||
}
|
||||
|
||||
header('Location: /admin/settings.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get current settings
|
||||
$store = getSetting('store', [
|
||||
'name' => "Tom's Java Jive",
|
||||
'email' => '',
|
||||
'phone' => '',
|
||||
'address' => '',
|
||||
'currency' => 'USD',
|
||||
'timezone' => 'America/New_York'
|
||||
]);
|
||||
|
||||
$tax = getSetting('tax', [
|
||||
'enabled' => false,
|
||||
'rate' => 0,
|
||||
'included_in_price' => false
|
||||
]);
|
||||
|
||||
$checkout = getSetting('checkout', [
|
||||
'guest_checkout' => true,
|
||||
'require_phone' => false,
|
||||
'order_notes' => true,
|
||||
'terms_required' => false,
|
||||
'terms_url' => ''
|
||||
]);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Store Settings</h1>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 200px 1fr; gap: 1.5rem;">
|
||||
<!-- Sidebar Navigation -->
|
||||
<div>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0.5rem;">
|
||||
<a href="/admin/settings.php" class="nav-item active"><i class="fas fa-store"></i> General</a>
|
||||
<a href="/admin/shipping.php" class="nav-item"><i class="fas fa-truck"></i> Shipping</a>
|
||||
<a href="/admin/payments.php" class="nav-item"><i class="fas fa-credit-card"></i> Payments</a>
|
||||
<a href="/admin/emails.php" class="nav-item"><i class="fas fa-envelope"></i> Emails</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Forms -->
|
||||
<div>
|
||||
<!-- General Settings -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="general">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">General Settings</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Store Name</label>
|
||||
<input type="text" name="store_name" class="form-input" value="<?= htmlspecialchars($store['name']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Store Email</label>
|
||||
<input type="email" name="store_email" class="form-input" value="<?= htmlspecialchars($store['email']) ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Store Phone</label>
|
||||
<input type="text" name="store_phone" class="form-input" value="<?= htmlspecialchars($store['phone']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Store Address</label>
|
||||
<textarea name="store_address" class="form-textarea" rows="2"><?= htmlspecialchars($store['address']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Currency</label>
|
||||
<select name="currency" class="form-select">
|
||||
<option value="USD" <?= $store['currency'] === 'USD' ? 'selected' : '' ?>>USD ($)</option>
|
||||
<option value="EUR" <?= $store['currency'] === 'EUR' ? 'selected' : '' ?>>EUR (€)</option>
|
||||
<option value="GBP" <?= $store['currency'] === 'GBP' ? 'selected' : '' ?>>GBP (£)</option>
|
||||
<option value="CAD" <?= $store['currency'] === 'CAD' ? 'selected' : '' ?>>CAD ($)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Timezone</label>
|
||||
<select name="timezone" class="form-select">
|
||||
<option value="America/New_York" <?= $store['timezone'] === 'America/New_York' ? 'selected' : '' ?>>Eastern Time</option>
|
||||
<option value="America/Chicago" <?= $store['timezone'] === 'America/Chicago' ? 'selected' : '' ?>>Central Time</option>
|
||||
<option value="America/Denver" <?= $store['timezone'] === 'America/Denver' ? 'selected' : '' ?>>Mountain Time</option>
|
||||
<option value="America/Los_Angeles" <?= $store['timezone'] === 'America/Los_Angeles' ? 'selected' : '' ?>>Pacific Time</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save General Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Tax Settings -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="tax">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Tax Settings</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="tax_enabled" <?= $tax['enabled'] ? 'checked' : '' ?>>
|
||||
Enable tax calculation
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tax Rate (%)</label>
|
||||
<input type="number" name="tax_rate" class="form-input" step="0.01" min="0" max="100" value="<?= $tax['rate'] ?>" style="max-width: 150px;">
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="tax_included" <?= $tax['included_in_price'] ? 'checked' : '' ?>>
|
||||
Prices include tax
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2">Save Tax Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Checkout Settings -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="section" value="checkout">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Checkout Settings</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="guest_checkout" <?= $checkout['guest_checkout'] ? 'checked' : '' ?>>
|
||||
Allow guest checkout
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="require_phone" <?= $checkout['require_phone'] ? 'checked' : '' ?>>
|
||||
Require phone number
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="order_notes" <?= $checkout['order_notes'] ? 'checked' : '' ?>>
|
||||
Allow order notes
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="terms_required" <?= $checkout['terms_required'] ? 'checked' : '' ?>>
|
||||
Require terms acceptance
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Terms & Conditions URL</label>
|
||||
<input type="url" name="terms_url" class="form-input" value="<?= htmlspecialchars($checkout['terms_url']) ?>" placeholder="https://example.com/terms">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2">Save Checkout Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Shipping Settings
|
||||
*/
|
||||
|
||||
$pageTitle = 'Shipping Settings';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
setSetting('shipping', [
|
||||
'flat_rate_enabled' => isset($_POST['flat_rate_enabled']),
|
||||
'flat_rate_amount' => floatval($_POST['flat_rate_amount'] ?? 0),
|
||||
'free_shipping_enabled' => isset($_POST['free_shipping_enabled']),
|
||||
'free_shipping_threshold' => floatval($_POST['free_shipping_threshold'] ?? 0),
|
||||
'local_pickup_enabled' => isset($_POST['local_pickup_enabled']),
|
||||
'processing_time' => trim($_POST['processing_time'] ?? '')
|
||||
]);
|
||||
setFlash('success', 'Shipping settings updated');
|
||||
header('Location: /admin/shipping.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$shipping = getSetting('shipping', [
|
||||
'flat_rate_enabled' => true,
|
||||
'flat_rate_amount' => 5.99,
|
||||
'free_shipping_enabled' => true,
|
||||
'free_shipping_threshold' => 50,
|
||||
'local_pickup_enabled' => false,
|
||||
'processing_time' => '1-2 business days'
|
||||
]);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Shipping Settings</h1>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 200px 1fr; gap: 1.5rem;">
|
||||
<div>
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0.5rem;">
|
||||
<a href="/admin/settings.php" class="nav-item"><i class="fas fa-store"></i> General</a>
|
||||
<a href="/admin/shipping.php" class="nav-item active"><i class="fas fa-truck"></i> Shipping</a>
|
||||
<a href="/admin/payments.php" class="nav-item"><i class="fas fa-credit-card"></i> Payments</a>
|
||||
<a href="/admin/emails.php" class="nav-item"><i class="fas fa-envelope"></i> Emails</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form method="POST">
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-header">
|
||||
<h3 class="admin-card-title">Shipping Methods</h3>
|
||||
</div>
|
||||
<div class="admin-card-body">
|
||||
<!-- Flat Rate -->
|
||||
<div style="border: 1px solid var(--admin-border); border-radius: var(--admin-radius); padding: 1rem; margin-bottom: 1rem;">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-weight: 600;">
|
||||
<input type="checkbox" name="flat_rate_enabled" <?= $shipping['flat_rate_enabled'] ? 'checked' : '' ?>>
|
||||
<i class="fas fa-box"></i> Flat Rate Shipping
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Flat Rate Amount</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>$</span>
|
||||
<input type="number" name="flat_rate_amount" class="form-input" step="0.01" min="0"
|
||||
value="<?= $shipping['flat_rate_amount'] ?>" style="max-width: 120px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Free Shipping -->
|
||||
<div style="border: 1px solid var(--admin-border); border-radius: var(--admin-radius); padding: 1rem; margin-bottom: 1rem;">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-weight: 600;">
|
||||
<input type="checkbox" name="free_shipping_enabled" <?= $shipping['free_shipping_enabled'] ? 'checked' : '' ?>>
|
||||
<i class="fas fa-gift"></i> Free Shipping (Threshold)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Minimum Order for Free Shipping</label>
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>$</span>
|
||||
<input type="number" name="free_shipping_threshold" class="form-input" step="0.01" min="0"
|
||||
value="<?= $shipping['free_shipping_threshold'] ?>" style="max-width: 120px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Local Pickup -->
|
||||
<div style="border: 1px solid var(--admin-border); border-radius: var(--admin-radius); padding: 1rem; margin-bottom: 1rem;">
|
||||
<div class="form-group mb-0">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-weight: 600;">
|
||||
<input type="checkbox" name="local_pickup_enabled" <?= $shipping['local_pickup_enabled'] ? 'checked' : '' ?>>
|
||||
<i class="fas fa-store"></i> Local Pickup
|
||||
</label>
|
||||
<small class="text-muted">Allow customers to pick up orders at your location</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Time -->
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label">Processing Time</label>
|
||||
<input type="text" name="processing_time" class="form-input"
|
||||
value="<?= htmlspecialchars($shipping['processing_time']) ?>"
|
||||
placeholder="e.g., 1-2 business days">
|
||||
<small class="text-muted">Displayed to customers during checkout</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-2">Save Shipping Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
ob_start();
|
||||
$pageTitle = 'Splash Box';
|
||||
$currentPage = 'splashes';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
/* ── Actions ─────────────────────────────────────── */
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$splashId = $_POST['splash_id'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if ($title) {
|
||||
db()->insert('homepage_splashes', [
|
||||
'splash_id' => generateId('spl_'),
|
||||
'icon' => trim($_POST['icon'] ?? 'fas fa-star'),
|
||||
'image_url' => trim($_POST['image_url'] ?? '') ?: null,
|
||||
'title' => $title,
|
||||
'description' => trim($_POST['description'] ?? '') ?: null,
|
||||
'sort_order' => intval($_POST['sort_order'] ?? 0),
|
||||
'is_active' => 1,
|
||||
]);
|
||||
setFlash('success', 'Splash block created');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'update' && $splashId) {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
if ($title) {
|
||||
db()->update('homepage_splashes', [
|
||||
'icon' => trim($_POST['icon'] ?? 'fas fa-star'),
|
||||
'image_url' => trim($_POST['image_url'] ?? '') ?: null,
|
||||
'title' => $title,
|
||||
'description' => trim($_POST['description'] ?? '') ?: null,
|
||||
'sort_order' => intval($_POST['sort_order'] ?? 0),
|
||||
'is_active' => isset($_POST['is_active']) ? 1 : 0,
|
||||
], 'splash_id = :id', ['id' => $splashId]);
|
||||
setFlash('success', 'Splash block updated');
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'delete' && $splashId) {
|
||||
db()->delete('homepage_splashes', 'splash_id = :id', ['id' => $splashId]);
|
||||
setFlash('success', 'Splash block deleted');
|
||||
}
|
||||
|
||||
if ($action === 'reorder') {
|
||||
$ids = json_decode($_POST['order'] ?? '[]', true);
|
||||
foreach ($ids as $pos => $sid) {
|
||||
db()->update('homepage_splashes', ['sort_order' => $pos + 1],
|
||||
'splash_id = :id', ['id' => $sid]);
|
||||
}
|
||||
echo json_encode(['ok' => true]); exit;
|
||||
}
|
||||
|
||||
header('Location: /admin/splashes.php'); exit;
|
||||
}
|
||||
|
||||
$splashes = db()->fetchAll(
|
||||
"SELECT * FROM homepage_splashes ORDER BY sort_order ASC, id ASC"
|
||||
);
|
||||
|
||||
$iconOptions = [
|
||||
'fas fa-leaf' => 'Leaf',
|
||||
'fas fa-fire' => 'Fire',
|
||||
'fas fa-truck' => 'Truck',
|
||||
'fas fa-heart' => 'Heart',
|
||||
'fas fa-star' => 'Star',
|
||||
'fas fa-coffee' => 'Coffee',
|
||||
'fas fa-mug-hot' => 'Mug Hot',
|
||||
'fas fa-seedling' => 'Seedling',
|
||||
'fas fa-shield-alt' => 'Shield',
|
||||
'fas fa-check-circle' => 'Check',
|
||||
'fas fa-gift' => 'Gift',
|
||||
'fas fa-globe' => 'Globe',
|
||||
'fas fa-award' => 'Award',
|
||||
'fas fa-smile' => 'Smile',
|
||||
'fas fa-bolt' => 'Bolt',
|
||||
'fas fa-recycle' => 'Recycle',
|
||||
'fas fa-hand-holding-heart'=> 'Care',
|
||||
'fas fa-crown' => 'Crown',
|
||||
'fas fa-gem' => 'Gem',
|
||||
'fas fa-thumbs-up' => 'Thumbs Up',
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Splash Box</h1>
|
||||
<button class="btn btn-primary" onclick="openSplashModal()">
|
||||
<i class="fas fa-plus"></i> Add Splash
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-card" style="margin-bottom:1.5rem">
|
||||
<div class="admin-card-body" style="padding:.85rem 1.5rem">
|
||||
<p class="text-muted" style="margin:0"><i class="fas fa-info-circle"></i>
|
||||
Drag rows to reorder — saves automatically.
|
||||
<?= count($splashes) ?> block<?= count($splashes) !== 1 ? 's' : '' ?> total.
|
||||
Homepage scrolls horizontally when more than 4 are active.
|
||||
Each block can show an icon <em>or</em> a custom image.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Preview -->
|
||||
<div class="admin-card" style="margin-bottom:1.5rem">
|
||||
<div class="admin-card-header"><h3><i class="fas fa-eye"></i> Homepage Preview</h3></div>
|
||||
<div class="admin-card-body" style="background:var(--admin-bg);border-radius:var(--radius-md);padding:0;overflow:hidden">
|
||||
<div id="splashPreview" style="display:flex;gap:1.5rem;overflow-x:auto;padding:2rem;scrollbar-width:thin">
|
||||
<?php foreach ($splashes as $sp): if (!$sp['is_active']) continue; ?>
|
||||
<div style="min-width:190px;text-align:center;padding:1.5rem 1rem;background:var(--admin-card);border-radius:var(--radius-lg);flex-shrink:0;box-shadow:0 1px 4px rgba(0,0,0,.08)">
|
||||
<div style="width:56px;height:56px;background:linear-gradient(135deg,var(--admin-primary),#c4420f);border-radius:12px;display:flex;align-items:center;justify-content:center;margin:0 auto .75rem;color:#fff;font-size:1.35rem;overflow:hidden">
|
||||
<?php if (!empty($sp['image_url'])): ?>
|
||||
<img src="<?= htmlspecialchars($sp['image_url']) ?>" style="width:100%;height:100%;object-fit:cover" alt="">
|
||||
<?php else: ?>
|
||||
<i class="<?= htmlspecialchars($sp['icon']) ?>"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="font-weight:600;font-size:.9rem;margin-bottom:.35rem"><?= htmlspecialchars($sp['title']) ?></div>
|
||||
<div style="font-size:.78rem;color:var(--admin-text-muted);line-height:1.4"><?= htmlspecialchars($sp['description'] ?? '') ?></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding:0">
|
||||
<?php if (empty($splashes)): ?>
|
||||
<div class="text-center text-muted" style="padding:3rem">No splash blocks yet. Click <strong>Add Splash</strong> to get started.</div>
|
||||
<?php else: ?>
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:30px"></th>
|
||||
<th style="width:60px">Visual</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th style="width:60px">Order</th>
|
||||
<th style="width:80px">Status</th>
|
||||
<th style="width:100px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="splashTbody">
|
||||
<?php foreach ($splashes as $sp): ?>
|
||||
<tr data-id="<?= $sp['splash_id'] ?>">
|
||||
<td style="color:var(--admin-text-muted);text-align:center;cursor:grab"><i class="fas fa-grip-vertical"></i></td>
|
||||
<td>
|
||||
<div style="width:42px;height:42px;background:linear-gradient(135deg,var(--admin-primary),#c4420f);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;overflow:hidden">
|
||||
<?php if (!empty($sp['image_url'])): ?>
|
||||
<img src="<?= htmlspecialchars($sp['image_url']) ?>" style="width:100%;height:100%;object-fit:cover" alt="">
|
||||
<?php else: ?>
|
||||
<i class="<?= htmlspecialchars($sp['icon']) ?>"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><strong><?= htmlspecialchars($sp['title']) ?></strong></td>
|
||||
<td style="max-width:260px;color:var(--admin-text-muted);font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
<?= htmlspecialchars($sp['description'] ?? '') ?>
|
||||
</td>
|
||||
<td class="sort-cell"><?= $sp['sort_order'] ?></td>
|
||||
<td>
|
||||
<?= $sp['is_active']
|
||||
? '<span class="badge badge-success">Active</span>'
|
||||
: '<span class="badge badge-error">Hidden</span>' ?>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openSplashModal(<?= json_encode($sp) ?>)' title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" style="display:inline">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="splash_id" value="<?= $sp['splash_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this splash block?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Modal ──────────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="splashModal">
|
||||
<div class="modal" style="max-width:620px;width:95vw">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="splashModalTitle">Add Splash Block</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('splashModal')">×</button>
|
||||
</div>
|
||||
<form method="POST" id="splashForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="splashAction" value="create">
|
||||
<input type="hidden" name="splash_id" id="splashId">
|
||||
<input type="hidden" name="image_url" id="splashImageUrl">
|
||||
|
||||
<!-- Image upload zone -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Image <span class="text-muted" style="font-weight:400">(optional — overrides icon when set)</span></label>
|
||||
<div id="dropZone" style="border:2px dashed var(--color-border);border-radius:var(--radius-md);padding:1.5rem;text-align:center;cursor:pointer;transition:all .2s;position:relative">
|
||||
<div id="dropPlaceholder">
|
||||
<i class="fas fa-cloud-upload-alt" style="font-size:2rem;color:var(--admin-text-muted);display:block;margin-bottom:.5rem"></i>
|
||||
<div style="font-size:.875rem;color:var(--admin-text-muted)">Drag & drop an image here, or <span style="color:var(--admin-primary);font-weight:600">browse</span></div>
|
||||
<div style="font-size:.75rem;color:var(--admin-text-muted);margin-top:.25rem">JPG, PNG, WebP, GIF · Max 5 MB</div>
|
||||
</div>
|
||||
<div id="dropPreview" style="display:none">
|
||||
<img id="dropPreviewImg" src="" alt="" style="max-height:120px;max-width:100%;border-radius:var(--radius-md);margin-bottom:.5rem">
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="clearImage(event)">
|
||||
<i class="fas fa-times"></i> Remove image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="dropInput" accept="image/*" style="position:absolute;inset:0;opacity:0;cursor:pointer">
|
||||
<div id="dropUploading" style="display:none;color:var(--admin-text-muted)"><i class="fas fa-spinner fa-spin"></i> Uploading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Icon picker -->
|
||||
<div class="form-group" id="iconGroup">
|
||||
<label class="form-label">Icon <span class="text-muted" style="font-weight:400">(used when no image is set)</span></label>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:.6rem">
|
||||
<?php foreach ($iconOptions as $cls => $label): ?>
|
||||
<button type="button" class="icon-opt btn btn-secondary" data-icon="<?= $cls ?>" title="<?= $label ?>"
|
||||
style="width:40px;height:40px;padding:0;display:flex;align-items:center;justify-content:center"
|
||||
onclick="pickIcon('<?= $cls ?>')">
|
||||
<i class="<?= $cls ?>"></i>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<input type="text" name="icon" id="splashIcon" class="form-input" placeholder="Custom FA class e.g. fas fa-mug-hot" value="fas fa-star">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" name="title" id="splashTitle" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" id="splashDesc" class="form-input" rows="3" style="resize:vertical"></textarea>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:1rem">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label class="form-label">Sort Order</label>
|
||||
<input type="number" name="sort_order" id="splashOrder" class="form-input" value="0" min="0">
|
||||
</div>
|
||||
<div class="form-group" id="splashStatusGroup" style="display:none;flex:1">
|
||||
<label class="form-label">Status</label>
|
||||
<label style="display:flex;align-items:center;gap:.5rem;margin-top:.6rem;cursor:pointer">
|
||||
<input type="checkbox" name="is_active" id="splashActive" checked> Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('splashModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="splashSubmitBtn">Add Splash</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.icon-opt.selected { background:var(--admin-primary)!important;color:#fff!important;border-color:var(--admin-primary)!important; }
|
||||
#dropZone.drag-active { border-color:var(--admin-primary);background:rgba(255,94,26,.04); }
|
||||
#splashTbody tr { cursor:grab; }
|
||||
#splashTbody tr.drag-over { background:rgba(255,94,26,.06); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
/* ── Icon picker ─────────────────────────────────── */
|
||||
function pickIcon(cls) {
|
||||
document.getElementById('splashIcon').value = cls;
|
||||
document.querySelectorAll('.icon-opt').forEach(function(b) {
|
||||
b.classList.toggle('selected', b.dataset.icon === cls);
|
||||
});
|
||||
}
|
||||
document.getElementById('splashIcon').addEventListener('input', function() {
|
||||
document.querySelectorAll('.icon-opt').forEach(function(b) {
|
||||
b.classList.toggle('selected', b.dataset.icon === this.value);
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
/* ── Image drop zone ─────────────────────────────── */
|
||||
var dropZone = document.getElementById('dropZone');
|
||||
var dropInput = document.getElementById('dropInput');
|
||||
|
||||
['dragenter','dragover'].forEach(function(ev) {
|
||||
dropZone.addEventListener(ev, function(e) { e.preventDefault(); dropZone.classList.add('drag-active'); });
|
||||
});
|
||||
['dragleave','drop'].forEach(function(ev) {
|
||||
dropZone.addEventListener(ev, function(e) { e.preventDefault(); dropZone.classList.remove('drag-active'); });
|
||||
});
|
||||
dropZone.addEventListener('drop', function(e) { handleFile(e.dataTransfer.files[0]); });
|
||||
dropInput.addEventListener('change', function() { if (this.files[0]) handleFile(this.files[0]); });
|
||||
|
||||
function handleFile(file) {
|
||||
if (!file) return;
|
||||
var allowed = ['image/jpeg','image/png','image/gif','image/webp'];
|
||||
if (!allowed.includes(file.type)) { alert('Please use JPG, PNG, WebP or GIF.'); return; }
|
||||
if (file.size > 5 * 1024 * 1024) { alert('File too large (max 5 MB).'); return; }
|
||||
|
||||
document.getElementById('dropPlaceholder').style.display = 'none';
|
||||
document.getElementById('dropPreview').style.display = 'none';
|
||||
document.getElementById('dropUploading').style.display = 'block';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('image', file);
|
||||
fetch('/admin/api/upload-splash.php', { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
document.getElementById('dropUploading').style.display = 'none';
|
||||
if (data.error) { alert(data.error); showDropPlaceholder(); return; }
|
||||
document.getElementById('splashImageUrl').value = data.url;
|
||||
document.getElementById('dropPreviewImg').src = data.url;
|
||||
document.getElementById('dropPreview').style.display = 'block';
|
||||
})
|
||||
.catch(function() { document.getElementById('dropUploading').style.display='none'; showDropPlaceholder(); });
|
||||
}
|
||||
|
||||
function showDropPlaceholder() {
|
||||
document.getElementById('dropPlaceholder').style.display = 'block';
|
||||
document.getElementById('dropPreview').style.display = 'none';
|
||||
}
|
||||
|
||||
function clearImage(e) {
|
||||
e.stopPropagation();
|
||||
document.getElementById('splashImageUrl').value = '';
|
||||
document.getElementById('dropInput').value = '';
|
||||
showDropPlaceholder();
|
||||
}
|
||||
|
||||
/* ── Open modal ──────────────────────────────────── */
|
||||
function openSplashModal(sp) {
|
||||
var isEdit = !!sp;
|
||||
document.getElementById('splashModalTitle').textContent = isEdit ? 'Edit Splash Block' : 'Add Splash Block';
|
||||
document.getElementById('splashSubmitBtn').textContent = isEdit ? 'Save Changes' : 'Add Splash';
|
||||
document.getElementById('splashAction').value = isEdit ? 'update' : 'create';
|
||||
document.getElementById('splashId').value = isEdit ? sp.splash_id : '';
|
||||
document.getElementById('splashTitle').value = isEdit ? sp.title : '';
|
||||
document.getElementById('splashDesc').value = isEdit ? (sp.description || '') : '';
|
||||
document.getElementById('splashOrder').value = isEdit ? sp.sort_order : 0;
|
||||
document.getElementById('splashActive').checked = isEdit ? !!parseInt(sp.is_active) : true;
|
||||
document.getElementById('splashStatusGroup').style.display = isEdit ? '' : 'none';
|
||||
pickIcon(isEdit && sp.icon ? sp.icon : 'fas fa-star');
|
||||
|
||||
// image
|
||||
var imgUrl = isEdit ? (sp.image_url || '') : '';
|
||||
document.getElementById('splashImageUrl').value = imgUrl;
|
||||
document.getElementById('dropInput').value = '';
|
||||
if (imgUrl) {
|
||||
document.getElementById('dropPreviewImg').src = imgUrl;
|
||||
document.getElementById('dropPreview').style.display = 'block';
|
||||
document.getElementById('dropPlaceholder').style.display = 'none';
|
||||
document.getElementById('dropUploading').style.display = 'none';
|
||||
} else {
|
||||
showDropPlaceholder();
|
||||
document.getElementById('dropUploading').style.display = 'none';
|
||||
}
|
||||
|
||||
Modal.open('splashModal');
|
||||
}
|
||||
|
||||
/* ── Drag-to-reorder rows ────────────────────────── */
|
||||
(function() {
|
||||
var tbody = document.getElementById('splashTbody');
|
||||
if (!tbody) return;
|
||||
var dragging = null;
|
||||
|
||||
tbody.querySelectorAll('tr').forEach(function(row) {
|
||||
row.draggable = true;
|
||||
row.addEventListener('dragstart', function() { dragging = this; this.style.opacity = '.4'; });
|
||||
row.addEventListener('dragend', function() { this.style.opacity = ''; dragging = null; saveOrder(); });
|
||||
row.addEventListener('dragover', function(e) { e.preventDefault(); this.classList.add('drag-over'); });
|
||||
row.addEventListener('dragleave', function() { this.classList.remove('drag-over'); });
|
||||
row.addEventListener('drop', function(e) {
|
||||
e.preventDefault(); this.classList.remove('drag-over');
|
||||
if (dragging && dragging !== this) {
|
||||
var rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
if (rows.indexOf(dragging) < rows.indexOf(this)) this.after(dragging);
|
||||
else this.before(dragging);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function saveOrder() {
|
||||
var ids = Array.from(tbody.querySelectorAll('tr')).map(function(r) { return r.dataset.id; });
|
||||
fetch('/admin/splashes.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'action=reorder&order=' + encodeURIComponent(JSON.stringify(ids))
|
||||
});
|
||||
tbody.querySelectorAll('tr').forEach(function(r, i) {
|
||||
r.querySelector('.sort-cell').textContent = i + 1;
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Tom's Java Jive - Admin Image Upload Handler
|
||||
*/
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['image'])) {
|
||||
echo json_encode(['error' => 'No file received']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['image'];
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
$maxSize = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
if (!in_array($file['type'], $allowedTypes)) {
|
||||
echo json_encode(['error' => 'Invalid file type. Use JPG, PNG, WebP, or GIF.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($file['size'] > $maxSize) {
|
||||
echo json_encode(['error' => 'File too large. Maximum 5MB.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Create upload directory
|
||||
$uploadDir = __DIR__ . '/../uploads/products/';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'product_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . strtolower($ext);
|
||||
$filepath = $uploadDir . $filename;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $filepath)) {
|
||||
$url = '/uploads/products/' . $filename;
|
||||
echo json_encode(['success' => true, 'url' => $url]);
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to save file. Check directory permissions.']);
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
ob_start();
|
||||
/**
|
||||
* Tom's Java Jive - Admin Users Management
|
||||
*/
|
||||
|
||||
$pageTitle = 'Admin Users';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Handle actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create' || $action === 'update') {
|
||||
$userId = $_POST['user_id'] ?? '';
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$isMaster = isset($_POST['is_master']) ? 1 : 0;
|
||||
|
||||
$permissions = [
|
||||
'dashboard' => isset($_POST['perm_dashboard']),
|
||||
'pos' => isset($_POST['perm_pos']),
|
||||
'products' => isset($_POST['perm_products']),
|
||||
'orders' => isset($_POST['perm_orders']),
|
||||
'customers' => isset($_POST['perm_customers']),
|
||||
'settings_payment' => isset($_POST['perm_settings']),
|
||||
'settings_shipping' => isset($_POST['perm_settings']),
|
||||
'settings_email' => isset($_POST['perm_settings']),
|
||||
'admin_management' => isset($_POST['perm_admin'])
|
||||
];
|
||||
|
||||
if (empty($email) || empty($name)) {
|
||||
setFlash('error', 'Email and name are required');
|
||||
} else {
|
||||
$data = [
|
||||
'email' => strtolower($email),
|
||||
'name' => $name,
|
||||
'is_master' => $isMaster,
|
||||
'permissions' => json_encode($permissions)
|
||||
];
|
||||
|
||||
if ($action === 'update' && $userId) {
|
||||
if (!empty($password)) {
|
||||
$data['password_hash'] = hashPassword($password);
|
||||
}
|
||||
db()->update('admin_users', $data, 'user_id = :id', ['id' => $userId]);
|
||||
setFlash('success', 'Admin user updated');
|
||||
} else {
|
||||
if (empty($password)) {
|
||||
setFlash('error', 'Password is required for new users');
|
||||
} else {
|
||||
$existing = db()->fetch("SELECT id FROM admin_users WHERE email = :email", ['email' => strtolower($email)]);
|
||||
if ($existing) {
|
||||
setFlash('error', 'Email already exists');
|
||||
} else {
|
||||
$data['user_id'] = generateId('admin_');
|
||||
$data['password_hash'] = hashPassword($password);
|
||||
$data['is_admin'] = 1;
|
||||
db()->insert('admin_users', $data);
|
||||
setFlash('success', 'Admin user created');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: /admin/users.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete' && !empty($_POST['user_id'])) {
|
||||
// Don't allow deleting self or last master
|
||||
$user = db()->fetch("SELECT is_master FROM admin_users WHERE user_id = :id", ['id' => $_POST['user_id']]);
|
||||
if ($user && $user['is_master']) {
|
||||
$masterCount = db()->count('admin_users', 'is_master = 1');
|
||||
if ($masterCount <= 1) {
|
||||
setFlash('error', 'Cannot delete the last master admin');
|
||||
header('Location: /admin/users.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
db()->delete('admin_users', 'user_id = :id', ['id' => $_POST['user_id']]);
|
||||
setFlash('success', 'Admin user deleted');
|
||||
header('Location: /admin/users.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$users = db()->fetchAll("SELECT * FROM admin_users ORDER BY is_master DESC, name ASC");
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Admin Users</h1>
|
||||
<button class="btn btn-primary" onclick="openUserModal()">
|
||||
<i class="fas fa-plus"></i> Add Admin User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (hasFlash('success')): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (hasFlash('error')): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-card">
|
||||
<div class="admin-card-body" style="padding: 0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($user['name']) ?></strong>
|
||||
<?php if ($user['user_id'] === $adminUser['user_id']): ?>
|
||||
<span class="badge badge-primary">You</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($user['email']) ?></td>
|
||||
<td>
|
||||
<?php if ($user['is_master']): ?>
|
||||
<span class="badge badge-warning">Master Admin</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-primary">Admin</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-muted"><?= formatDate($user['created_at']) ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary" onclick='openUserModal(<?= json_encode($user) ?>)'>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<?php if ($user['user_id'] !== $adminUser['user_id']): ?>
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="user_id" value="<?= $user['user_id'] ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger" data-confirm="Delete this admin user?">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Modal -->
|
||||
<div class="modal-overlay" id="userModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="userModalTitle">Add Admin User</h3>
|
||||
<button type="button" class="modal-close" onclick="Modal.close('userModal')">×</button>
|
||||
</div>
|
||||
<form method="POST" id="userForm">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="userAction" value="create">
|
||||
<input type="hidden" name="user_id" id="userId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Name *</label>
|
||||
<input type="text" name="name" id="userName" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email *</label>
|
||||
<input type="email" name="email" id="userEmail" class="form-input" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password <span id="passwordRequired">*</span></label>
|
||||
<input type="password" name="password" id="userPassword" class="form-input">
|
||||
<small class="text-muted" id="passwordHint">Leave blank to keep current password</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="is_master" id="userMaster">
|
||||
Master Admin (full access)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="permissionsSection">
|
||||
<h4 style="margin-bottom: 0.75rem; font-size: 0.9rem;">Permissions</h4>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="perm_dashboard" id="permDashboard" checked> Dashboard
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="perm_pos" id="permPos" checked> POS
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="perm_products" id="permProducts" checked> Products
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="perm_orders" id="permOrders" checked> Orders
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="perm_customers" id="permCustomers" checked> Customers
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="perm_settings" id="permSettings"> Settings
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" name="perm_admin" id="permAdmin"> Admin Users
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="Modal.close('userModal')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" id="userSubmitBtn">Add User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('userMaster').addEventListener('change', function() {
|
||||
document.getElementById('permissionsSection').style.display = this.checked ? 'none' : 'block';
|
||||
});
|
||||
|
||||
function openUserModal(user = null) {
|
||||
const isEdit = !!user;
|
||||
|
||||
document.getElementById('userModalTitle').textContent = isEdit ? 'Edit Admin User' : 'Add Admin User';
|
||||
document.getElementById('userSubmitBtn').textContent = isEdit ? 'Update User' : 'Add User';
|
||||
document.getElementById('userAction').value = isEdit ? 'update' : 'create';
|
||||
document.getElementById('userId').value = isEdit ? user.user_id : '';
|
||||
document.getElementById('userName').value = isEdit ? user.name : '';
|
||||
document.getElementById('userEmail').value = isEdit ? user.email : '';
|
||||
document.getElementById('userPassword').value = '';
|
||||
document.getElementById('userMaster').checked = isEdit ? user.is_master : false;
|
||||
|
||||
document.getElementById('passwordRequired').style.display = isEdit ? 'none' : 'inline';
|
||||
document.getElementById('passwordHint').style.display = isEdit ? 'block' : 'none';
|
||||
document.getElementById('userPassword').required = !isEdit;
|
||||
|
||||
if (isEdit) {
|
||||
const perms = JSON.parse(user.permissions || '{}');
|
||||
document.getElementById('permDashboard').checked = perms.dashboard ?? true;
|
||||
document.getElementById('permPos').checked = perms.pos ?? true;
|
||||
document.getElementById('permProducts').checked = perms.products ?? true;
|
||||
document.getElementById('permOrders').checked = perms.orders ?? true;
|
||||
document.getElementById('permCustomers').checked = perms.customers ?? true;
|
||||
document.getElementById('permSettings').checked = perms.settings_payment ?? false;
|
||||
document.getElementById('permAdmin').checked = perms.admin_management ?? false;
|
||||
}
|
||||
|
||||
document.getElementById('permissionsSection').style.display =
|
||||
document.getElementById('userMaster').checked ? 'none' : 'block';
|
||||
|
||||
Modal.open('userModal');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
Reference in New Issue
Block a user