Initial commit

This commit is contained in:
2026-05-22 12:52:44 +00:00
commit 996ca0d621
122 changed files with 22749 additions and 0 deletions
+292
View File
@@ -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')">&times;</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 &nbsp;|&nbsp; <i class="fas fa-level-down-alt fa-rotate-90"></i> Single line break = &lt;br&gt;</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/&lt;br&gt;/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'; ?>
+714
View File
@@ -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'; ?>
+15
View File
@@ -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'=>[]]);
}
+29
View File
@@ -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.']);
}
+642
View 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;
}
+200
View File
@@ -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);
+243
View File
@@ -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'; ?>
+206
View File
@@ -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')">&times;</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'; ?>
+260
View File
@@ -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')">&times;</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'; ?>
+528
View File
@@ -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')">&times;</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')+' &times; '+(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')">&times;</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'; ?>
+218
View File
@@ -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'; ?>
+326
View File
@@ -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')">&times;</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')">&times;</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'; ?>
+582
View File
@@ -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'; ?>
+8
View File
@@ -0,0 +1,8 @@
</main>
</div>
</div>
<script src="/admin/assets/admin.js"></script>
<?php if (isset($extraScripts)) echo $extraScripts; ?>
</body>
</html>
+182
View File
@@ -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
View File
@@ -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'; ?>
+523
View File
@@ -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')">&times;</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'; ?>
+337
View File
@@ -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')">&times;</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'; ?>
+68
View File
@@ -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>
+10
View File
@@ -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
View File
@@ -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'; ?>
+279
View File
@@ -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')">&times;</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'; ?>
+169
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+400
View File
@@ -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&#10;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">&times;</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'; ?>
+163
View File
@@ -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')">&times;</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'; ?>
+290
View File
@@ -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'; ?>
+281
View File
@@ -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')">&times;</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'; ?>
+233
View File
@@ -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'; ?>
+124
View File
@@ -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'; ?>
+410
View File
@@ -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')">&times;</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 &amp; 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'; ?>
+44
View File
@@ -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
View File
@@ -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')">&times;</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'; ?>