feat: integrate facebook marketing roi on ecommerce dashboard

This commit is contained in:
theRAD
2026-03-06 05:33:03 +02:00
parent c1e4027db8
commit 38ab842b52
5 changed files with 251 additions and 1 deletions

View File

@@ -27,6 +27,10 @@ public function dashboardSettings($dashboard)
['key' => 'woo_store_url', 'name' => 'WooCommerce Store URL', 'type' => 'string', 'default' => 'https://example.com', 'description' => 'The base URL of your WooCommerce store (e.g. https://shop.stargas.co.za).'],
['key' => 'woo_consumer_key', 'name' => 'Consumer Key', 'type' => 'string', 'default' => '', 'description' => 'WooCommerce REST API Consumer Key (ck_...).'],
['key' => 'woo_consumer_secret', 'name' => 'Consumer Secret', 'type' => 'string', 'default' => '', 'description' => 'WooCommerce REST API Consumer Secret (cs_...).'],
['key' => 'fb_app_id', 'name' => 'Facebook App ID', 'type' => 'string', 'default' => '', 'description' => 'Your Facebook Application ID.'],
['key' => 'fb_app_secret', 'name' => 'Facebook App Secret', 'type' => 'string', 'default' => '', 'description' => 'Your Facebook Application Secret.'],
['key' => 'fb_access_token', 'name' => 'Facebook Access Token', 'type' => 'string', 'default' => '', 'description' => 'Long-lived Facebook Access Token for API calls.'],
['key' => 'fb_ad_account_id', 'name' => 'Facebook Ad Account ID', 'type' => 'string', 'default' => '', 'description' => 'Your Facebook Ad Account ID (e.g. act_123456789).'],
];
} else {
abort(404);

View File

@@ -371,4 +371,127 @@ public function insights(Request $request)
return response()->json(['error' => 'Failed to fetch WooCommerce insights data.'], 500);
}
}
}
/**
* Marketing & ROI (Facebook Ads + WooCommerce)
*/
public function marketing(Request $request)
{
$fbAppId = Setting::getValue('ecommerce', 'fb_app_id');
$fbToken = Setting::getValue('ecommerce', 'fb_access_token');
$fbAccountId = Setting::getValue('ecommerce', 'fb_ad_account_id');
if (!$fbToken || !$fbAccountId) {
return response()->json(['error' => 'Facebook credentials not configured in Admin settings.', 'configured' => false], 200);
}
// Base WooCommerce Sales calculation to determine ROAS
$client = $this->wooClient();
$netSales = 0;
[$from, $to] = $this->dateRange($request);
if ($client) {
$params = [];
if ($from) $params['date_min'] = $from;
if ($to) $params['date_max'] = $to;
try {
$salesResp = $client->get('reports/sales', $params);
if ($salesResp->successful()) {
$salesData = $salesResp->json();
if (!empty($salesData)) {
$netSales = (float) ($salesData[0]['net_sales'] ?? 0);
}
}
} catch (\Exception $e) {
// Silently drop woo error mapped to marketing
}
}
// Facebook Date Filtering
$fbParams = [
'level' => 'account',
'fields' => 'spend,impressions,clicks,cpc,cpa',
];
if ($from && $to) {
$fbParams['time_range'] = json_encode(['since' => $from, 'until' => $to]);
} else {
// Default to last 30 days if no explicit date range is given
$fbParams['date_preset'] = 'last_30d';
}
try {
// Remove 'act_' prefix if user included it, then add it back to be safe
$accountId = str_replace('act_', '', $fbAccountId);
$fbUrl = "https://graph.facebook.com/v19.0/act_{$accountId}";
// 1. Fetch Account-level Insights
$fbResp = Http::withToken($fbToken)->get("{$fbUrl}/insights", $fbParams);
$spend = 0;
$impressions = 0;
$clicks = 0;
if ($fbResp->successful()) {
$fbData = $fbResp->json();
if (isset($fbData['data']) && count($fbData['data']) > 0) {
$insight = $fbData['data'][0];
$spend = (float) ($insight['spend'] ?? 0);
$impressions = (int) ($insight['impressions'] ?? 0);
$clicks = (int) ($insight['clicks'] ?? 0);
}
}
// 2. Fetch Top Campaigns by Spend
$campParams = [
'level' => 'campaign',
'fields' => 'campaign_name,spend,clicks,impressions',
'sort' => ['spend_descending'],
'limit' => 5
];
if ($from && $to) {
$campParams['time_range'] = json_encode(['since' => $from, 'until' => $to]);
} else {
$campParams['date_preset'] = 'last_30d';
}
$campResp = Http::withToken($fbToken)->get("{$fbUrl}/insights", $campParams);
$campaigns = [];
if ($campResp->successful()) {
$campData = $campResp->json();
if (isset($campData['data'])) {
foreach ($campData['data'] as $cmp) {
$campaigns[] = [
'name' => $cmp['campaign_name'] ?? 'Unknown',
'spend' => (float) ($cmp['spend'] ?? 0),
'clicks' => (int) ($cmp['clicks'] ?? 0),
'impressions' => (int) ($cmp['impressions'] ?? 0),
];
}
}
}
// Calculate ROAS / CPC
$roas = $spend > 0 ? ($netSales / $spend) : 0;
$cpc = $clicks > 0 ? ($spend / $clicks) : 0;
return response()->json([
'configured' => true,
'spend' => $spend,
'impressions' => $impressions,
'clicks' => $clicks,
'cpc' => $cpc,
'roas' => $roas,
'woo_net_sales' => $netSales,
'campaigns' => $campaigns
]);
} catch (\Exception $e) {
Log::error('Facebook API Error: ' . $e->getMessage());
return response()->json(['error' => 'Failed to fetch Facebook data.'], 500);
}
}
}

View File

@@ -21,7 +21,7 @@
<svg class="w-6 h-6" 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>
<h3 class="text-lg font-bold text-slate-300 mb-2">E-Commerce Settings</h3>
<p class="text-sm text-slate-500">Configure WooCommerce API credentials and integration parameters for the online store dashboard.</p>
<p class="text-sm text-slate-500">Configure WooCommerce API and Facebook Business credentials for the marketing dashboard.</p>
</a>
</div>

View File

@@ -182,6 +182,87 @@
</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">
@@ -489,12 +570,53 @@ function setKpisLoading() {
} 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');

View File

@@ -38,4 +38,5 @@
Route::get('/top-products', [EcommerceController::class, 'topProducts']);
Route::get('/analytics', [EcommerceController::class, 'analytics']);
Route::get('/insights', [EcommerceController::class, 'insights']);
Route::get('/marketing', [EcommerceController::class, 'marketing']);
});