From 38ab842b52fe1a3e83c8b770bbfad0ca4324a56b Mon Sep 17 00:00:00 2001 From: theRAD Date: Fri, 6 Mar 2026 05:33:03 +0200 Subject: [PATCH] feat: integrate facebook marketing roi on ecommerce dashboard --- src/app/Http/Controllers/AdminController.php | 4 + .../Http/Controllers/EcommerceController.php | 123 ++++++++++++++++++ src/resources/views/admin/index.blade.php | 2 +- src/resources/views/ecommerce.blade.php | 122 +++++++++++++++++ src/routes/web.php | 1 + 5 files changed, 251 insertions(+), 1 deletion(-) diff --git a/src/app/Http/Controllers/AdminController.php b/src/app/Http/Controllers/AdminController.php index 86248ee..052c010 100644 --- a/src/app/Http/Controllers/AdminController.php +++ b/src/app/Http/Controllers/AdminController.php @@ -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); diff --git a/src/app/Http/Controllers/EcommerceController.php b/src/app/Http/Controllers/EcommerceController.php index da876c6..c418176 100644 --- a/src/app/Http/Controllers/EcommerceController.php +++ b/src/app/Http/Controllers/EcommerceController.php @@ -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); + } + } } diff --git a/src/resources/views/admin/index.blade.php b/src/resources/views/admin/index.blade.php index 05704b1..ecf9df6 100644 --- a/src/resources/views/admin/index.blade.php +++ b/src/resources/views/admin/index.blade.php @@ -21,7 +21,7 @@

E-Commerce Settings

-

Configure WooCommerce API credentials and integration parameters for the online store dashboard.

+

Configure WooCommerce API and Facebook Business credentials for the marketing dashboard.

diff --git a/src/resources/views/ecommerce.blade.php b/src/resources/views/ecommerce.blade.php index a8620ec..f6bb749 100644 --- a/src/resources/views/ecommerce.blade.php +++ b/src/resources/views/ecommerce.blade.php @@ -182,6 +182,87 @@ + +

+ + Marketing & Growth (Meta Ads) +

+ + + + + +

+ + Advanced Custom Analytics +

+
@@ -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('
').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 = 'No active campaigns found in this period.'; + } else { + tb.innerHTML = data.campaigns.map(c => ` + + ${c.name} + ${fmt(c.spend)} + ${fmtNum(c.clicks)} + ${fmt(c.clicks > 0 ? (c.spend / c.clicks) : 0)} + + `).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'); diff --git a/src/routes/web.php b/src/routes/web.php index dcc6607..14d5851 100644 --- a/src/routes/web.php +++ b/src/routes/web.php @@ -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']); });