v1.0.0 - Initial backup

This commit is contained in:
2026-05-16 23:00:37 -05:00
commit bf8b6225a3
114 changed files with 21120 additions and 0 deletions
+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'; ?>