HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/be.naniguide.com/app/Http/Controllers/Refactor/Ads/AdDashboardController.php
<?php

namespace App\Http\Controllers\Refactor\Ads;

use App\Http\Controllers\Controller;
use App\Model\AdActivityLog;
use App\Model\AdAudience;
use App\Model\AdCampaign;
use App\Model\AdMetric;
use App\Model\AdPixel;
use App\Model\AdPlatform;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class AdDashboardController extends Controller
{
    /**
     * Ads dashboard — unified analytics for the current customer.
     */
    public function index(Request $request)
    {
        $customer = $request->user()->customer;
        $local = $customer->local();

        $platformCount = $local->adPlatforms()->active()->count();
        $onboarding = $this->computeOnboarding($customer->id, $local, $platformCount);

        if ($platformCount === 0) {
            return view('refactor.pages.ads.dashboard.index', [
                'hasPlatforms' => false,
                'onboarding' => $onboarding,
            ]);
        }

        $period = $request->input('period', '30d');
        $days = match ($period) {
            '7d' => 7,
            '30d' => 30,
            '90d' => 90,
            default => 30,
        };
        $startDate = now()->startOfDay()->subDays($days - 1);
        $prevStart = (clone $startDate)->subDays($days);
        $prevEnd = (clone $startDate)->subDay();

        $campaignIds = $local->adCampaigns()->pluck('id');

        // Current period totals
        $current = AdMetric::whereIn('ad_campaign_id', $campaignIds)
            ->where('date', '>=', $startDate)
            ->selectRaw('COALESCE(SUM(impressions),0) as impressions, COALESCE(SUM(clicks),0) as clicks, COALESCE(SUM(spend),0) as spend, COALESCE(SUM(conversions),0) as conversions')
            ->first();

        // Previous period totals — for delta computation
        $previous = AdMetric::whereIn('ad_campaign_id', $campaignIds)
            ->whereBetween('date', [$prevStart, $prevEnd])
            ->selectRaw('COALESCE(SUM(impressions),0) as impressions, COALESCE(SUM(clicks),0) as clicks, COALESCE(SUM(spend),0) as spend, COALESCE(SUM(conversions),0) as conversions')
            ->first();

        $ctr = $current->impressions > 0 ? round(($current->clicks / $current->impressions) * 100, 2) : 0;
        $roas = $current->spend > 0 ? round(($current->conversions * 50) / $current->spend, 2) : 0;

        $pctDelta = function ($a, $b) {
            if ($b == 0) {
                return $a > 0 ? 100 : 0;
            }
            return round((($a - $b) / $b) * 100, 1);
        };

        $deltas = [
            'spend' => $pctDelta((float) $current->spend, (float) $previous->spend),
            'impressions' => $pctDelta((int) $current->impressions, (int) $previous->impressions),
            'clicks' => $pctDelta((int) $current->clicks, (int) $previous->clicks),
            'conversions' => $pctDelta((int) $current->conversions, (int) $previous->conversions),
        ];

        // Daily series for chart + sparklines — fill zeroes for missing days
        $rawDaily = AdMetric::whereIn('ad_campaign_id', $campaignIds)
            ->where('date', '>=', $startDate)
            ->selectRaw('date, SUM(impressions) as impressions, SUM(clicks) as clicks, SUM(spend) as spend, SUM(conversions) as conversions')
            ->groupBy('date')
            ->orderBy('date')
            ->get()
            ->keyBy(fn ($row) => (string) $row->date->format('Y-m-d'));

        $daily = [];
        for ($i = 0; $i < $days; $i++) {
            $d = (clone $startDate)->addDays($i)->format('Y-m-d');
            $row = $rawDaily[$d] ?? null;
            $daily[] = [
                'date' => $d,
                'impressions' => (int) ($row->impressions ?? 0),
                'clicks' => (int) ($row->clicks ?? 0),
                'spend' => (float) ($row->spend ?? 0),
                'conversions' => (int) ($row->conversions ?? 0),
            ];
        }

        // Sparkline arrays for each metric
        $sparklines = [
            'spend' => array_map(fn ($r) => $r['spend'], $daily),
            'impressions' => array_map(fn ($r) => $r['impressions'], $daily),
            'clicks' => array_map(fn ($r) => $r['clicks'], $daily),
            'conversions' => array_map(fn ($r) => $r['conversions'], $daily),
        ];

        // Per-platform breakdown
        $platformBreakdown = AdMetric::whereIn('ad_campaign_id', $campaignIds)
            ->where('date', '>=', $startDate)
            ->selectRaw('platform, SUM(spend) as spend, SUM(impressions) as impressions, SUM(clicks) as clicks, SUM(conversions) as conversions')
            ->groupBy('platform')
            ->get();

        $totalSpendAll = max(0.01, (float) $current->spend);
        $platformBreakdown = $platformBreakdown->map(function ($row) use ($totalSpendAll) {
            return [
                'platform' => $row->platform,
                'label' => AdPlatform::PLATFORM_LABELS[$row->platform] ?? ucfirst((string) $row->platform),
                'spend' => (float) $row->spend,
                'impressions' => (int) $row->impressions,
                'clicks' => (int) $row->clicks,
                'conversions' => (int) $row->conversions,
                'share' => round(((float) $row->spend / $totalSpendAll) * 100, 1),
            ];
        })->sortByDesc('spend')->values();

        // Top campaigns by spend in period
        $topCampaignStats = AdMetric::whereIn('ad_campaign_id', $campaignIds)
            ->where('date', '>=', $startDate)
            ->selectRaw('ad_campaign_id, SUM(spend) as spend, SUM(impressions) as impressions, SUM(clicks) as clicks, SUM(conversions) as conversions')
            ->groupBy('ad_campaign_id')
            ->orderByDesc('spend')
            ->limit(5)
            ->get();

        $topCampaignModels = AdCampaign::whereIn('id', $topCampaignStats->pluck('ad_campaign_id'))->get()->keyBy('id');
        $topCampaigns = $topCampaignStats->map(function ($row) use ($topCampaignModels) {
            $campaign = $topCampaignModels[$row->ad_campaign_id] ?? null;
            if (!$campaign) return null;
            $ctrLocal = $row->impressions > 0 ? round(($row->clicks / $row->impressions) * 100, 2) : 0;
            return [
                'model' => $campaign,
                'spend' => (float) $row->spend,
                'impressions' => (int) $row->impressions,
                'clicks' => (int) $row->clicks,
                'conversions' => (int) $row->conversions,
                'ctr' => $ctrLocal,
            ];
        })->filter()->values();

        // Recent campaigns list (for the main table)
        $campaigns = $local->adCampaigns()
            ->orderByDesc('updated_at')
            ->limit(10)
            ->get();

        // Campaign status counts
        $statusCounts = $local->adCampaigns()
            ->selectRaw('status, COUNT(*) as cnt')
            ->groupBy('status')
            ->pluck('cnt', 'status')
            ->toArray();

        $statusSummary = [
            'active' => (int) ($statusCounts[AdCampaign::STATUS_ACTIVE] ?? 0),
            'draft' => (int) ($statusCounts[AdCampaign::STATUS_DRAFT] ?? 0),
            'paused' => (int) ($statusCounts[AdCampaign::STATUS_PAUSED] ?? 0),
            'completed' => (int) ($statusCounts[AdCampaign::STATUS_COMPLETED] ?? 0),
            'error' => (int) ($statusCounts[AdCampaign::STATUS_ERROR] ?? 0),
        ];

        // Recent activity — last 6 events
        $recentActivity = AdActivityLog::ofCustomer($local)
            ->orderByDesc('created_at')
            ->limit(6)
            ->get();

        // Alerts — platforms with unhealthy status or token expiry soon
        $platforms = $local->adPlatforms()->active()->get();
        $alerts = [];
        foreach ($platforms as $p) {
            if (!$p->isHealthy() && $p->health_status === 'error') {
                $alerts[] = [
                    'severity' => 'error',
                    'title' => $p->name . ' — connection error',
                    'desc' => 'Reconnect this platform to resume publishing.',
                    'action_label' => 'Reconnect',
                    'action_url' => route('refactor.ads.platforms.index'),
                ];
            } elseif ($p->health_status === 'degraded') {
                $alerts[] = [
                    'severity' => 'warning',
                    'title' => $p->name . ' — degraded',
                    'desc' => 'Platform is responding slowly. We will retry automatically.',
                    'action_label' => 'View',
                    'action_url' => route('refactor.ads.platforms.index'),
                ];
            }
        }
        if ($statusSummary['error'] > 0) {
            $alerts[] = [
                'severity' => 'error',
                'title' => $statusSummary['error'] . ' campaign(s) in error state',
                'desc' => 'Publishing failed. Review the publish logs for details.',
                'action_label' => 'Review',
                'action_url' => route('refactor.ads.campaigns.index'),
            ];
        }

        return view('refactor.pages.ads.dashboard.index', [
            'hasPlatforms' => true,
            'onboarding' => $onboarding,
            'period' => $period,
            'days' => $days,
            'totals' => [
                'impressions' => (int) $current->impressions,
                'clicks' => (int) $current->clicks,
                'spend' => (float) $current->spend,
                'conversions' => (int) $current->conversions,
                'ctr' => $ctr,
                'roas' => $roas,
            ],
            'deltas' => $deltas,
            'daily' => $daily,
            'sparklines' => $sparklines,
            'platformBreakdown' => $platformBreakdown,
            'topCampaigns' => $topCampaigns,
            'campaigns' => $campaigns,
            'statusSummary' => $statusSummary,
            'recentActivity' => $recentActivity,
            'alerts' => $alerts,
            'platformCount' => $platformCount,
            'campaignCount' => $local->adCampaigns()->count(),
            'platforms' => $platforms,
        ]);
    }

    /**
     * Compute the 4-step ads onboarding checklist state for the customer.
     *
     * Ordering reflects USE_CASES.md §1-5: connect platform → create audience →
     * launch first campaign → review results. Each step unlocks the next — a
     * step is "locked" until its prerequisite is done, "current" when it's the
     * next actionable step, "done" when the detection query succeeds.
     */
    protected function computeOnboarding(int $customerId, $local, int $platformCount): array
    {
        $hasPlatform = $platformCount > 0;
        $hasAudience = AdAudience::where('customer_id', $customerId)->exists();
        $hasPublishedCampaign = AdCampaign::where('customer_id', $customerId)
            ->whereIn('status', [AdCampaign::STATUS_ACTIVE, AdCampaign::STATUS_PAUSED, AdCampaign::STATUS_COMPLETED])
            ->exists();
        $campaignIds = $local->adCampaigns()->pluck('id');
        $hasMetrics = AdMetric::whereIn('ad_campaign_id', $campaignIds)->exists();

        // Live stats for the left-rail step list — computed once, used for the
        // second line of each row (see _onboarding.blade.php). Stays cheap:
        // all queries are indexed + scoped to this customer.
        $platformNames = $local->adPlatforms()->active()
            ->pluck('platform')
            ->map(fn ($p) => AdPlatform::PLATFORM_LABELS[$p] ?? ucfirst($p))
            ->take(3)
            ->implode(', ');
        $audienceCount = AdAudience::where('customer_id', $customerId)->count();
        $activeCampaignCount = AdCampaign::where('customer_id', $customerId)
            ->where('status', AdCampaign::STATUS_ACTIVE)
            ->count();
        $completedCampaignCount = AdCampaign::where('customer_id', $customerId)
            ->where('status', AdCampaign::STATUS_COMPLETED)
            ->count();
        $totalCampaignCount = AdCampaign::where('customer_id', $customerId)->count();
        $metricTotals = AdMetric::whereIn('ad_campaign_id', $campaignIds)
            ->where('date', '>=', now()->subDays(30)->startOfDay())
            ->selectRaw('COALESCE(SUM(spend),0) as spend, COALESCE(SUM(clicks),0) as clicks, COALESCE(SUM(impressions),0) as impressions')
            ->first();
        $ctr30d = ($metricTotals && $metricTotals->impressions > 0)
            ? round(($metricTotals->clicks / $metricTotals->impressions) * 100, 2)
            : 0;

        $platformStat = $hasPlatform
            ? trans('refactor/ads.onboarding.stat_platform_done', [
                'count' => $platformCount,
                'names' => $platformNames ?: trans('refactor/ads.onboarding.stat_platform_none'),
            ])
            : trans('refactor/ads.onboarding.stat_platform_pending', ['time' => '2 min']);

        if (!$hasPlatform) {
            $audienceStat = trans('refactor/ads.onboarding.stat_audience_locked');
        } elseif ($hasAudience) {
            $audienceStat = trans('refactor/ads.onboarding.stat_audience_done', ['count' => $audienceCount]);
        } else {
            $audienceStat = trans('refactor/ads.onboarding.stat_audience_pending', ['time' => '3 min']);
        }

        if (!$hasAudience) {
            $campaignStat = trans('refactor/ads.onboarding.stat_campaign_locked');
        } elseif ($hasPublishedCampaign) {
            $campaignStat = trans('refactor/ads.onboarding.stat_campaign_done', [
                'total' => $totalCampaignCount,
                'active' => $activeCampaignCount,
                'completed' => $completedCampaignCount,
            ]);
        } else {
            $campaignStat = trans('refactor/ads.onboarding.stat_campaign_pending', ['time' => '10 min']);
        }

        if (!$hasPublishedCampaign) {
            $resultsStat = trans('refactor/ads.onboarding.stat_results_locked');
        } elseif ($hasMetrics) {
            $resultsStat = trans('refactor/ads.onboarding.stat_results_done', [
                'spend' => number_format((float) ($metricTotals->spend ?? 0), 2),
                'clicks' => number_format((int) ($metricTotals->clicks ?? 0)),
                'ctr' => $ctr30d,
            ]);
        } else {
            $resultsStat = trans('refactor/ads.onboarding.stat_results_pending', ['time' => '1 min']);
        }

        $steps = [
            [
                'key' => 'connect_platform',
                'done' => $hasPlatform,
                'locked' => false,
                'time' => '2 min',
                'stat' => $platformStat,
                'cta_route' => 'refactor.ads.platforms.index',
            ],
            [
                'key' => 'create_audience',
                'done' => $hasAudience,
                'locked' => !$hasPlatform,
                'time' => '3 min',
                'stat' => $audienceStat,
                'cta_route' => 'refactor.ads.audiences.index',
            ],
            [
                'key' => 'launch_campaign',
                'done' => $hasPublishedCampaign,
                'locked' => !$hasAudience,
                'time' => '10 min',
                'stat' => $campaignStat,
                'cta_route' => 'refactor.ads.campaigns.create',
            ],
            [
                'key' => 'review_results',
                'done' => $hasMetrics,
                'locked' => !$hasPublishedCampaign,
                'time' => '1 min',
                'stat' => $resultsStat,
                'cta_route' => 'refactor.ads.dashboard.index',
            ],
        ];

        $total = count($steps);
        $completed = count(array_filter($steps, fn ($s) => $s['done']));

        $currentIndex = null;
        foreach ($steps as $i => $s) {
            if (!$s['done'] && !$s['locked']) {
                $currentIndex = $i;
                break;
            }
        }
        if ($currentIndex === null) {
            // All done or all locked — fall back to first incomplete, else last
            foreach ($steps as $i => $s) {
                if (!$s['done']) {
                    $currentIndex = $i;
                    break;
                }
            }
        }
        if ($currentIndex === null) {
            $currentIndex = $total - 1;
        }

        return [
            'steps' => $steps,
            'completed' => $completed,
            'total' => $total,
            'percent' => (int) round(($completed / $total) * 100),
            'current_index' => $currentIndex,
            'all_done' => $completed === $total,
        ];
    }
}