feat: add advanced device and referral analytics charts
This commit is contained in:
@@ -176,4 +176,86 @@ public function topProducts(Request $request)
|
|||||||
return response()->json(['error' => 'Failed to fetch WooCommerce data.'], 500);
|
return response()->json(['error' => 'Failed to fetch WooCommerce data.'], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensive Analytics (Device Types, Referrals).
|
||||||
|
*/
|
||||||
|
public function analytics(Request $request)
|
||||||
|
{
|
||||||
|
$client = $this->wooClient();
|
||||||
|
if (!$client) {
|
||||||
|
return response()->json(['error' => 'API credentials missing.'], 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
[$from, $to] = $this->dateRange($request);
|
||||||
|
|
||||||
|
// Fetch up to 100 recent orders for analytics sampling
|
||||||
|
$params = [
|
||||||
|
'per_page' => 100,
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'desc',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($from) $params['after'] = $from . 'T00:00:00';
|
||||||
|
if ($to) $params['before'] = $to . 'T23:59:59';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resp = $client->get('orders', $params);
|
||||||
|
$orders = $resp->successful() ? $resp->json() : [];
|
||||||
|
|
||||||
|
$devices = [];
|
||||||
|
$sources = [];
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if (!isset($order['meta_data'])) continue;
|
||||||
|
|
||||||
|
$device = 'Unknown';
|
||||||
|
$source = 'Direct / Organic';
|
||||||
|
|
||||||
|
foreach ($order['meta_data'] as $meta) {
|
||||||
|
if ($meta['key'] === '_wc_order_attribution_device_type') {
|
||||||
|
$device = ucfirst(strtolower($meta['value']));
|
||||||
|
}
|
||||||
|
if ($meta['key'] === '_wc_order_attribution_utm_source') {
|
||||||
|
$source = ucfirst(strtolower($meta['value']));
|
||||||
|
} elseif ($meta['key'] === '_wc_order_attribution_source_type' && strtolower($meta['value']) === 'organic') {
|
||||||
|
if ($source === 'Direct / Organic') {
|
||||||
|
$source = 'Organic Search';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate Device
|
||||||
|
if (!isset($devices[$device])) {
|
||||||
|
$devices[$device] = 0;
|
||||||
|
}
|
||||||
|
$devices[$device]++;
|
||||||
|
|
||||||
|
// Aggregate Source
|
||||||
|
if (!isset($sources[$source])) {
|
||||||
|
$sources[$source] = 0;
|
||||||
|
}
|
||||||
|
$sources[$source]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort sources by count DESC
|
||||||
|
arsort($sources);
|
||||||
|
$topSources = array_slice($sources, 0, 5, true);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'devices' => [
|
||||||
|
'labels' => array_keys($devices),
|
||||||
|
'values' => array_values($devices)
|
||||||
|
],
|
||||||
|
'sources' => [
|
||||||
|
'labels' => array_keys($topSources),
|
||||||
|
'values' => array_values($topSources)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('WooCommerce API Error (analytics): ' . $e->getMessage());
|
||||||
|
return response()->json(['error' => 'Failed to fetch WooCommerce data.'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>E-Commerce Dashboard - Stargas</title>
|
<title>E-Commerce Dashboard - Stargas</title>
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@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">
|
<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>
|
<style>
|
||||||
body { background: linear-gradient(135deg, #0a0a0a 0%, #1a0a0a 30%, #0d0d0d 60%, #0a0a0a 100%); min-height: 100vh; font-family: 'Inter', sans-serif; }
|
body { background: linear-gradient(135deg, #0a0a0a 0%, #1a0a0a 30%, #0d0d0d 60%, #0a0a0a 100%); min-height: 100vh; font-family: 'Inter', sans-serif; }
|
||||||
@@ -193,6 +194,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Analytics Charts Row -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
<!-- 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-[300px] 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-[300px]">
|
||||||
|
<canvas id="sourceChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- JS Logic -->
|
<!-- JS Logic -->
|
||||||
@@ -352,11 +384,80 @@ function getStatusBadge(status) {
|
|||||||
} catch(e) { console.error(e); }
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deviceChartInstance = null;
|
||||||
|
let sourceChartInstance = 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 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); }
|
||||||
|
}
|
||||||
|
|
||||||
function loadAllData() {
|
function loadAllData() {
|
||||||
updateFilterStatus();
|
updateFilterStatus();
|
||||||
loadSummary();
|
loadSummary();
|
||||||
loadRecentOrders();
|
loadRecentOrders();
|
||||||
loadTopProducts();
|
loadTopProducts();
|
||||||
|
loadAnalytics();
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
document.getElementById('last-updated').textContent = 'Updated ' + now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0');
|
document.getElementById('last-updated').textContent = 'Updated ' + now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0');
|
||||||
|
|||||||
@@ -36,4 +36,5 @@
|
|||||||
Route::get('/summary', [EcommerceController::class, 'summary']);
|
Route::get('/summary', [EcommerceController::class, 'summary']);
|
||||||
Route::get('/recent-orders', [EcommerceController::class, 'recentOrders']);
|
Route::get('/recent-orders', [EcommerceController::class, 'recentOrders']);
|
||||||
Route::get('/top-products', [EcommerceController::class, 'topProducts']);
|
Route::get('/top-products', [EcommerceController::class, 'topProducts']);
|
||||||
|
Route::get('/analytics', [EcommerceController::class, 'analytics']);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user