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;
}
}