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/ai.naniguide.com/app/Services/Customer/DashboardService.php
<?php

namespace App\Services\Customer;

use App\Library\Cache\AppCache;
use App\Model\Customer;
use Carbon\Carbon;
use Illuminate\Support\Collection;

class DashboardService
{
    protected Customer $customer;

    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
    }

    /**
     * Get quota usage data — pre-formatted for view consumption.
     * Each section returns:
     *   - count (int): current usage
     *   - max_display (string): 'Not included' / 'Unlimited' / formatted N (handles 4-state)
     *   - used (float 0..1): ratio for progress bar
     *   - max (int|null|false): raw union (in case view needs to branch further)
     */
    public function getQuotaUsage(): array
    {
        $quotas = app(\App\Services\Plans\Quotas\QuotasService::class);
        $QK = \App\Services\Plans\Quotas\QuotaKey::class;
        $lc = $this->customer->local();

        $listsCount       = $lc->listsCount();
        $campaignsCount   = $lc->campaignsCount();
        $subscribersCount = AppCache::for($lc)->read('SubscriberCount', 0);

        return [
            'lists' => [
                'count'       => $listsCount,
                'max'         => $quotas->limit($this->customer, $QK::MAX_LISTS),
                'max_display' => $quotas->display($this->customer, $QK::MAX_LISTS),
                'used'        => $quotas->usedRatio($this->customer, $QK::MAX_LISTS),
            ],
            'campaigns' => [
                'count'       => $campaignsCount,
                'max'         => $quotas->limit($this->customer, $QK::MAX_CAMPAIGNS),
                'max_display' => $quotas->display($this->customer, $QK::MAX_CAMPAIGNS),
                'used'        => $quotas->usedRatio($this->customer, $QK::MAX_CAMPAIGNS),
            ],
            'subscribers' => [
                'count'       => $subscribersCount,
                'max'         => $quotas->limit($this->customer, $QK::MAX_SUBSCRIBERS),
                'max_display' => $quotas->display($this->customer, $QK::MAX_SUBSCRIBERS),
                'used'        => $quotas->usedRatio($this->customer, $QK::MAX_SUBSCRIBERS),
            ],
        ];
    }

    /**
     * Get smart context for welcome banner.
     */
    public function getBannerContext(): array
    {
        $lc = $this->customer->local();
        $lastCampaign = $lc->sentCampaigns()->first();
        $totalCampaigns = $lc->campaignsCount();
        $totalSubscribers = (int) AppCache::for($lc)->read('SubscriberCount', 0);

        $context = [
            'has_campaigns' => $totalCampaigns > 0,
            'total_campaigns' => $totalCampaigns,
            'total_subscribers' => $totalSubscribers,
            'last_campaign' => null,
            'last_campaign_open_rate' => 0,
            'last_sent_ago' => null,
        ];

        if ($lastCampaign) {
            $context['last_campaign'] = $lastCampaign->name;
            $context['last_campaign_open_rate'] = round((float) AppCache::for($lastCampaign)->read('UniqOpenRate', 0) * 100, 1);
            $context['last_sent_ago'] = $lastCampaign->updated_at->diffForHumans();
        }

        return $context;
    }

    /**
     * Get aggregate email metrics for the last 7 days.
     *
     * Reads from per-customer cache (LocalCustomer::cacheManifest -> EmailMetrics_7).
     * Returns zeros on cache miss — user triggers refresh via dashboard button.
     */
    public function getEmailMetrics(): array
    {
        return AppCache::for($this->customer->local())->read('EmailMetrics_7', [
            'total_sent' => 0,
            'open_rate' => 0,
            'click_rate' => 0,
            'bounce_rate' => 0,
        ]);
    }

    /**
     * Get sending volume for last N days (sparklines).
     *
     * @return array<int, int>
     */
    public function getSendingTrend(int $days = 7): array
    {
        return array_map(
            fn ($row) => (int) $row['sent'],
            $this->getCampaignPerformance($days)
        );
    }

    /**
     * Get campaign performance data for chart.
     *
     * Reads from per-customer cache (LocalCustomer::cacheManifest -> SentByDay_*).
     * Cache miss → AppCache invokes the manifest closure to compute + store.
     *
     * @return array<int, array{date: string, sent: int}>
     */
    public function getCampaignPerformance(int $days = 7): array
    {
        return AppCache::for($this->customer->local())->read('SentByDay_' . $days, []);
    }

    /**
     * Get top campaigns by open rate.
     */
    public function getTopCampaigns(int $limit = 5): Collection
    {
        $lc = $this->customer->local();

        return $lc->sentCampaigns()
            ->take(30)
            ->get()
            ->map(function ($campaign) {
                return [
                    'name' => $campaign->name,
                    'uid' => $campaign->uid,
                    'delivered' => (int) AppCache::for($campaign)->read('DeliveredCount', 0),
                    'open_rate' => round((float) AppCache::for($campaign)->read('UniqOpenRate', 0) * 100, 1),
                    'click_rate' => round((float) AppCache::for($campaign)->read('ClickedRate', 0) * 100, 1),
                ];
            })
            ->filter(fn ($c) => $c['delivered'] > 0)
            ->sortByDesc('open_rate')
            ->take($limit)
            ->values();
    }

    /**
     * Get top countries by opens (from last campaign).
     *
     * Reads from per-customer cache (LocalCustomer::cacheManifest -> TopCountries_5).
     * Returns empty collection on cache miss.
     */
    public function getTopCountries(int $limit = 5): Collection
    {
        $data = AppCache::for($this->customer->local())->read('TopCountries_5', []);
        return collect(array_slice($data, 0, $limit));
    }

    /**
     * Get recent campaigns.
     */
    public function getRecentCampaigns(int $limit = 5): Collection
    {
        return $this->customer->local()
            ->campaigns()
            ->orderBy('updated_at', 'desc')
            ->limit($limit)
            ->get();
    }

    /**
     * Get activity logs.
     */
    public function getActivityLogs(int $limit = 10): Collection
    {
        return $this->customer->activityLogs()->take($limit)->get();
    }

    /**
     * Get setup checklist status with rich metadata for each step.
     */
    public function getSetupChecklist(): array
    {
        $lc = $this->customer->local();
        $hasList = $lc->listsCount() > 0;
        $firstList = $hasList ? $this->customer->mailLists()->first() : null;

        $steps = [
            'has_list' => [
                'done' => $hasList,
                'icon' => 'list',
                'route' => route('refactor.lists.index'),
                'action_route' => route('refactor.lists.create'),
            ],
            'has_subscriber' => [
                'done' => (int) AppCache::for($lc)->read('SubscriberCount', 0) > 0,
                'icon' => 'person-add',
                'route' => $firstList ? route('refactor.lists.overview', $firstList->uid) : route('refactor.lists.index'),
                'action_route' => $firstList ? route('refactor.lists.subscriber_import', $firstList->uid) : route('refactor.lists.index'),
            ],
            'has_campaign' => [
                'done' => $lc->campaignsCount() > 0,
                'icon' => 'mail',
                'route' => route('refactor.campaigns.index'),
                'action_route' => route('refactor.campaigns.create'),
            ],
            'has_sending_server' => [
                'done' => $this->customer->activeSendingServers()->count() > 0
                    || $this->planProvidesBuiltInSending(),
                'icon' => 'send',
                'route' => route('refactor.sending.servers'),
                'action_route' => route('refactor.sending.server_select'),
            ],
        ];

        $completed = count(array_filter(array_column($steps, 'done')));
        $total = count($steps);

        return [
            'steps' => $steps,
            'completed' => $completed,
            'total' => $total,
            'all_done' => $completed === $total,
            'percentage' => $total > 0 ? round(($completed / $total) * 100) : 0,
        ];
    }

    private function planProvidesBuiltInSending(): bool
    {
        $sub = $this->customer->getCurrentActiveSubscription();
        if ($sub === null) {
            return false;
        }
        $plan = $sub->plan;
        return $plan !== null
            && $plan->useSystemSendingServer()
            && $plan->hasPrimarySendingServer();
    }

    /**
     * Get subscriber breakdown by status.
     */
    public function getSubscriberBreakdown(): array
    {
        $lc = $this->customer->local();
        $total = (int) AppCache::for($lc)->read('SubscriberCount', 0);
        $subscribed = (int) AppCache::for($lc)->read('SubscribedCount', 0);
        $blacklisted = (int) AppCache::for($lc)->read('BlacklistedCount', 0);
        $spamReported = (int) AppCache::for($lc)->read('SpamReportedCount', 0);
        $unconfirmed = max(0, $total - $subscribed - $blacklisted - $spamReported);

        return [
            'total' => $total,
            'subscribed' => $subscribed,
            'blacklisted' => $blacklisted,
            'spam_reported' => $spamReported,
            'unconfirmed' => $unconfirmed,
        ];
    }

    /**
     * Oldest write timestamp among the big-table cache entries that back this dashboard.
     * Null = none of the tracked entries has been computed yet ("never").
     */
    public function getDashboardComputedAt(): ?Carbon
    {
        $scope = AppCache::for($this->customer->local());
        $timestamps = array_filter([
            $scope->lastUpdatedAt('EmailMetrics_7'),
            $scope->lastUpdatedAt('TopCountries_5'),
            $scope->lastUpdatedAt('SubscriberCount'),
        ]);
        return $timestamps ? min($timestamps) : null;
    }
}