Files
supplier-dashboard/src/resources/views/ecommerce.blade.php

630 lines
35 KiB
PHP

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>E-Commerce Dashboard - Stargas</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { background: linear-gradient(135deg, #0a0a0a 0%, #1a0a0a 30%, #0d0d0d 60%, #0a0a0a 100%); min-height: 100vh; font-family: 'Inter', sans-serif; }
.glass { background: rgba(255, 255, 255, 0.04); backdrop-filter: blur(12px); border: 1px solid rgba(227, 25, 55, 0.12); }
.stargas-accent { background: linear-gradient(90deg, #E31937, #FFD700, #E31937); height: 2px; }
/* Shimmer effect for loading states */
.loading-shimmer {
background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 37%, rgba(255,255,255,0.03) 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
}
@keyframes shimmer {
0% { background-position: 100% 50%; }
100% { background-position: 0 50%; }
}
</style>
</head>
<body class="text-white antialiased">
<!-- Stargas Accent Line -->
<div class="stargas-accent"></div>
<!-- Header -->
<header class="glass sticky top-0 z-50 border-b border-white/5">
<div class="max-w-[1600px] mx-auto px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4">
<img src="/images/stargas-logo.svg" alt="Stargas Energies" class="h-12">
<div class="border-l border-white/10 pl-4">
<h1 class="text-lg font-bold tracking-tight text-purple-400">E-Commerce Dashboard</h1>
<p class="text-xs text-slate-400">WooCommerce Analytics</p>
</div>
</div>
<div class="flex items-center gap-4">
<a href="/" class="px-3 py-2 text-xs font-medium rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-white/10 flex items-center gap-1.5 text-slate-300">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
Hub
</a>
<span id="last-updated" class="text-xs text-slate-500"></span>
<button onclick="loadAllData()" class="px-4 py-2 text-xs font-medium rounded-lg bg-purple-700 hover:bg-purple-600 transition-colors flex items-center gap-2 border border-purple-600/30">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
Refresh
</button>
</div>
</div>
<!-- Date Range Filter Bar -->
<div class="max-w-[1600px] mx-auto px-6 py-2 flex items-center gap-3 border-t border-white/5">
<svg class="w-4 h-4 text-slate-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
<label class="text-xs text-slate-400">Financial Year</label>
<select id="filter-fy" onchange="applyFinancialYear()" class="bg-white/5 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500/30">
<option value="" class="bg-slate-900 text-white">Custom / YTD</option>
<script>
const currentYear = new Date().getFullYear();
const currentFYStart = new Date().getMonth() < 2 ? currentYear - 1 : currentYear;
for (let y = currentFYStart; y >= 2020; y--) {
document.write(`<option value="${y}" class="bg-slate-900 text-white">FY ${y}/${(y+1).toString().substring(2)}</option>`);
}
</script>
</select>
<div class="h-4 border-l border-white/10 mx-1"></div>
<label class="text-xs text-slate-400">From</label>
<input type="date" id="filter-from" class="bg-white/5 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500/30" />
<label class="text-xs text-slate-400">To</label>
<input type="date" id="filter-to" class="bg-white/5 border border-white/10 rounded-lg px-3 py-1.5 text-xs text-white focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500/30" />
<button onclick="loadAllData()" class="px-3 py-1.5 text-xs font-medium rounded-lg bg-purple-700/80 hover:bg-purple-600 transition-colors border border-purple-600/30">Apply</button>
<button onclick="clearDateFilter()" class="px-3 py-1.5 text-xs font-medium rounded-lg bg-white/5 hover:bg-white/10 transition-colors border border-white/10">Clear</button>
<span id="filter-status" class="text-xs text-slate-500 ml-auto"></span>
</div>
</header>
<main class="max-w-[1600px] mx-auto px-6 py-8 space-y-8">
<div id="api-error" class="hidden glass border-red-500/50 bg-red-500/10 p-4 rounded-xl text-red-400 text-sm font-medium flex-col gap-2">
<div class="flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
WooCommerce API Connection Error
</div>
<p class="text-xs text-red-400/80 font-normal">Please check your WooCommerce credentials in the <a href="/admin/settings/ecommerce" class="underline hover:text-white">Admin Panel</a>.</p>
</div>
<!-- KPI Cards -->
<section class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-4 gap-4">
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Net Sales</span>
<div class="w-8 h-8 rounded-lg bg-purple-500/10 flex items-center justify-center text-purple-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-sales" class="text-2xl font-bold text-white tracking-tight">R 0.00</div>
</div>
</div>
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Total Orders</span>
<div class="w-8 h-8 rounded-lg bg-blue-500/10 flex items-center justify-center text-blue-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-orders" class="text-2xl font-bold text-white tracking-tight">0</div>
</div>
</div>
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Avg Order Value</span>
<div class="w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center text-emerald-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-aov" class="text-2xl font-bold text-white tracking-tight">R 0.00</div>
</div>
</div>
<div class="kpi-card glass rounded-2xl p-5">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-medium text-slate-400 uppercase tracking-wider">Total Customers</span>
<div class="w-8 h-8 rounded-lg bg-orange-500/10 flex items-center justify-center text-orange-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
</div>
</div>
<div class="flex items-baseline gap-2">
<div id="kpi-customers" class="text-2xl font-bold text-white tracking-tight">0</div>
</div>
</div>
</section>
<!-- Tables Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Top Selling Products -->
<div class="glass rounded-2xl flex flex-col h-full overflow-hidden">
<div class="p-5 border-b border-white/5 flex items-center justify-between bg-white/5">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/></svg>
Top Selling Products
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-black/20 text-xs text-slate-400 uppercase tracking-wider">
<th class="p-3 font-medium">Product</th>
<th class="p-3 font-medium text-right">Qty Sold</th>
</tr>
</thead>
<tbody id="table-products" class="text-sm divide-y divide-white/5">
<!-- Populated via JS -->
</tbody>
</table>
</div>
</div>
<!-- Geographic Sales Distribution -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Geographic Sales Distribution
</h3>
</div>
<div class="relative w-full h-[300px]">
<canvas id="geoChart"></canvas>
</div>
</div>
</div>
<!-- Marketing & Growth Row -->
<h2 class="text-xl font-bold text-white mb-4 mt-8 flex items-center gap-2">
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
Marketing & Growth (Meta Ads)
</h2>
<div id="marketing-unconfigured" class="glass rounded-2xl p-8 flex flex-col items-center justify-center text-center mb-8 hidden">
<svg class="w-12 h-12 text-slate-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<h3 class="text-lg font-bold text-white mb-2">Connect Facebook Business</h3>
<p class="text-sm text-slate-400 max-w-md">Unlock powerful Return on Ad Spend (ROAS) analytics by configuring your Facebook Graph API credentials in the Admin Panel.</p>
<a href="/admin/settings/ecommerce" class="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded-lg transition-colors">Configure Now</a>
</div>
<div id="marketing-dashboard" class="mb-8 hidden">
<!-- Marketing KPIs -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<!-- Ad Spend -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-blue-500/5 group-hover:bg-blue-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">Ad Spend</p>
<p class="text-2xl font-bold text-white" id="mkt-spend"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
<!-- ROAS -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-purple-500/5 group-hover:bg-purple-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">ROAS</p>
<p class="text-2xl font-bold text-emerald-400" id="mkt-roas"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
<!-- CPC -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-rose-500/5 group-hover:bg-rose-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">Cost Per Click</p>
<p class="text-2xl font-bold text-white" id="mkt-cpc"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
<!-- Clicks -->
<div class="glass p-5 rounded-2xl relative overflow-hidden group">
<div class="absolute inset-0 bg-orange-500/5 group-hover:bg-orange-500/10 transition-colors"></div>
<div class="relative z-10">
<p class="text-sm font-medium text-slate-400 tracking-wide uppercase mb-1">Link Clicks</p>
<p class="text-2xl font-bold text-white" id="mkt-clicks"><div class="h-8 w-24 rounded loading-shimmer mkt-loading"></div></p>
</div>
</div>
</div>
<!-- Top Campaigns Table -->
<div class="glass rounded-2xl flex flex-col overflow-hidden">
<div class="p-5 border-b border-white/5 flex items-center justify-between bg-white/5">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"/></svg>
Top Performing Campaigns
</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-black/20 text-xs text-slate-400 uppercase tracking-wider">
<th class="p-3 font-medium">Campaign Name</th>
<th class="p-3 font-medium text-right">Spend</th>
<th class="p-3 font-medium text-right">Clicks</th>
<th class="p-3 font-medium text-right">CPC</th>
</tr>
</thead>
<tbody id="table-campaigns" class="text-sm divide-y divide-white/5">
<!-- Populated via JS -->
</tbody>
</table>
</div>
</div>
</div>
<h2 class="text-xl font-bold text-white mb-4 mt-8 flex items-center gap-2">
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Advanced Custom Analytics
</h2>
<!-- Advanced Analytics Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Customer Retention -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
Customer Retention
</h3>
</div>
<div class="relative w-full max-w-[240px] aspect-square">
<canvas id="retentionChart"></canvas>
</div>
</div>
<!-- Device Breakthrough -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/></svg>
Device Breakdown
</h3>
</div>
<div class="relative w-full max-w-[240px] aspect-square">
<canvas id="deviceChart"></canvas>
</div>
</div>
<!-- Top Referral Sources -->
<div class="glass rounded-2xl p-5 flex flex-col items-center">
<div class="w-full flex items-center justify-between mb-4">
<h3 class="font-bold text-white tracking-tight flex items-center gap-2">
<svg class="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
Top Referral Sources
</h3>
</div>
<div class="relative w-full h-[240px] mt-4">
<canvas id="sourceChart"></canvas>
</div>
</div>
</div>
</main>
<!-- JS Logic -->
<script>
const fmtNum = new Intl.NumberFormat('en-ZA').format;
const fmt = new Intl.NumberFormat('en-ZA', { style: 'currency', currency: 'ZAR', minimumFractionDigits: 0 }).format;
// Date filter helpers
function getDateParams() {
let from = document.getElementById('filter-from').value;
let to = document.getElementById('filter-to').value;
// Default to YTD
if (!from && !to) {
const now = new Date();
from = `${now.getFullYear()}-01-01`;
to = now.toISOString().split('T')[0];
document.getElementById('filter-from').value = from;
document.getElementById('filter-to').value = to;
setTimeout(updateFilterStatus, 50);
}
const params = new URLSearchParams();
if (from) params.set('from', from);
if (to) params.set('to', to);
return params.toString();
}
function clearDateFilter() {
document.getElementById('filter-fy').value = '';
document.getElementById('filter-from').value = '';
document.getElementById('filter-to').value = '';
loadAllData();
}
function applyFinancialYear() {
const fyYear = document.getElementById('filter-fy').value;
if (!fyYear) return;
const startYear = parseInt(fyYear);
const endYear = startYear + 1;
const from = `${startYear}-03-01`;
const isLeapYear = (endYear % 4 === 0 && endYear % 100 !== 0) || (endYear % 400 === 0);
const to = `${endYear}-02-${isLeapYear ? '29' : '28'}`;
document.getElementById('filter-from').value = from;
document.getElementById('filter-to').value = to;
updateFilterStatus();
loadAllData();
}
function updateFilterStatus() {
const from = document.getElementById('filter-from').value;
const to = document.getElementById('filter-to').value;
const el = document.getElementById('filter-status');
if (from || to) {
const parts = [];
if (from) parts.push('from ' + from);
if (to) parts.push('to ' + to);
el.textContent = 'Filtered: ' + parts.join(' ');
el.classList.add('text-purple-400');
el.classList.remove('text-slate-500');
} else {
el.textContent = 'Showing all time';
}
}
// Fetch data wrapper
async function fetchJson(url) {
const qs = getDateParams();
const separator = url.includes('?') ? '&' : '?';
const fullUrl = qs ? url + separator + qs : url;
const res = await fetch(fullUrl);
const data = await res.json();
if (!res.ok) {
document.getElementById('api-error').classList.remove('hidden');
document.getElementById('api-error').classList.add('flex');
throw new Error(data.error || `Failed to fetch ${fullUrl}`);
} else {
document.getElementById('api-error').classList.add('hidden');
document.getElementById('api-error').classList.remove('flex');
}
return data;
}
function setKpisLoading() {
const ids = ['kpi-sales', 'kpi-orders', 'kpi-aov', 'kpi-customers'];
ids.forEach(id => {
document.getElementById(id).innerHTML = '<div class="h-8 w-24 rounded loading-shimmer"></div>';
});
}
async function loadSummary() {
setKpisLoading();
try {
const d = await fetchJson('/api/ecommerce/summary');
document.getElementById('kpi-sales').textContent = fmt(d.net_sales);
document.getElementById('kpi-orders').textContent = fmtNum(d.total_orders);
document.getElementById('kpi-aov').textContent = fmt(d.avg_order_value);
document.getElementById('kpi-customers').textContent = fmtNum(d.total_customers);
} catch (e) {
console.error(e);
}
}
async function loadTopProducts() {
const tb = document.getElementById('table-products');
tb.innerHTML = Array(5).fill('<tr><td colspan="2" class="p-3"><div class="h-4 w-full rounded loading-shimmer"></div></td></tr>').join('');
try {
const products = await fetchJson('/api/ecommerce/top-products');
if (!products || products.length === 0) {
tb.innerHTML = '<tr><td colspan="2" class="p-4 text-center text-slate-500">No products found for this period.</td></tr>';
return;
}
tb.innerHTML = products.map(p => `
<tr class="hover:bg-white/5 transition-colors">
<td class="p-3 font-medium text-white truncate max-w-[200px]" title="${p.name}">${p.name || 'Unknown Product'}</td>
<td class="p-3 text-right font-medium text-blue-400">${fmtNum(p.quantity)}</td>
</tr>
`).join('');
} catch(e) { console.error(e); }
}
let deviceChartInstance = null;
let sourceChartInstance = null;
let geoChartInstance = null;
let retentionChartInstance = null;
// Custom darker Chart JS defaults for dark UI
Chart.defaults.color = '#94a3b8';
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.05)';
Chart.defaults.font.family = "'Inter', sans-serif";
async function loadInsights() {
try {
const data = await fetchJson('/api/ecommerce/insights');
// Geo Bar Chart
const ctxGeo = document.getElementById('geoChart').getContext('2d');
if (geoChartInstance) geoChartInstance.destroy();
geoChartInstance = new Chart(ctxGeo, {
type: 'bar',
data: {
labels: data.geographic.labels,
datasets: [{
label: 'Sales (ZAR)',
data: data.geographic.values,
backgroundColor: '#e11d48', // Rose color
borderRadius: 4,
barThickness: 'flex',
maxBarThickness: 40
}]
},
options: {
indexAxis: 'y', // Horizontal bar to accommodate province names
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return fmt(context.raw);
}
}
}
},
scales: {
x: { beginAtZero: true, grid: { color: 'rgba(255, 255, 255, 0.05)' } },
y: { grid: { display: false } }
}
}
});
// Retention Doughnut
const ctxRet = document.getElementById('retentionChart').getContext('2d');
if (retentionChartInstance) retentionChartInstance.destroy();
retentionChartInstance = new Chart(ctxRet, {
type: 'doughnut',
data: {
labels: data.retention.labels,
datasets: [{
data: data.retention.values,
backgroundColor: ['#10b981', '#3b82f6', '#475569'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '75%',
plugins: {
legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } }
}
}
});
} catch(e) { console.error(e); }
}
async function loadAnalytics() {
try {
const data = await fetchJson('/api/ecommerce/analytics');
// Device Doughnut
const ctxDevice = document.getElementById('deviceChart').getContext('2d');
if (deviceChartInstance) deviceChartInstance.destroy();
deviceChartInstance = new Chart(ctxDevice, {
type: 'doughnut',
data: {
labels: data.devices.labels,
datasets: [{
data: data.devices.values,
backgroundColor: ['#3b82f6', '#8b5cf6', '#10b981', '#64748b'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '75%',
plugins: {
legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } }
}
}
});
// Source Bar Chart
const ctxSource = document.getElementById('sourceChart').getContext('2d');
if (sourceChartInstance) sourceChartInstance.destroy();
sourceChartInstance = new Chart(ctxSource, {
type: 'bar',
data: {
labels: data.sources.labels,
datasets: [{
label: 'Orders',
data: data.sources.values,
backgroundColor: '#f97316',
borderRadius: 4,
barThickness: 'flex',
maxBarThickness: 40
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, grid: { color: 'rgba(255, 255, 255, 0.05)' } },
x: { grid: { display: false } }
}
}
});
} catch(e) { console.error(e); }
}
async function loadMarketing() {
document.querySelectorAll('.mkt-loading').forEach(el => el.classList.remove('hidden'));
const tb = document.getElementById('table-campaigns');
if (tb) tb.innerHTML = Array(5).fill('<tr><td colspan="4" class="p-3"><div class="h-4 w-full rounded loading-shimmer"></div></td></tr>').join('');
try {
const data = await fetchJson('/api/ecommerce/marketing');
if (!data.configured) {
document.getElementById('marketing-unconfigured').classList.remove('hidden');
document.getElementById('marketing-dashboard').classList.add('hidden');
return;
}
document.getElementById('marketing-unconfigured').classList.add('hidden');
document.getElementById('marketing-dashboard').classList.remove('hidden');
document.getElementById('mkt-spend').textContent = fmt(data.spend);
document.getElementById('mkt-roas').textContent = data.roas.toFixed(2) + 'x';
document.getElementById('mkt-cpc').textContent = fmt(data.cpc);
document.getElementById('mkt-clicks').textContent = fmtNum(data.clicks);
if (!data.campaigns || data.campaigns.length === 0) {
tb.innerHTML = '<tr><td colspan="4" class="p-4 text-center text-slate-500">No active campaigns found in this period.</td></tr>';
} else {
tb.innerHTML = data.campaigns.map(c => `
<tr class="hover:bg-white/5 transition-colors">
<td class="p-3 font-medium text-white max-w-[200px] truncate" title="${c.name}">${c.name}</td>
<td class="p-3 text-right font-medium text-blue-400">${fmt(c.spend)}</td>
<td class="p-3 text-right text-slate-300">${fmtNum(c.clicks)}</td>
<td class="p-3 text-right text-slate-300">${fmt(c.clicks > 0 ? (c.spend / c.clicks) : 0)}</td>
</tr>
`).join('');
}
} catch(e) {
console.error(e);
}
}
function loadAllData() {
updateFilterStatus();
loadSummary();
loadTopProducts();
loadAnalytics();
loadInsights();
loadMarketing();
const now = new Date();
document.getElementById('last-updated').textContent = 'Updated ' + now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0');
}
// Init
document.addEventListener('DOMContentLoaded', loadAllData);
</script>
</body>
</html>