Files
SkinbaseNova/app/Services/Studio/CreatorStudioAnalyticsService.php

242 lines
9.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\User;
use App\Services\Studio\Contracts\CreatorStudioProvider;
use Illuminate\Support\Facades\DB;
use function collect;
use function now;
final class CreatorStudioAnalyticsService
{
public function __construct(
private readonly CreatorStudioContentService $content,
) {
}
public function overview(User $user, int $days = 30): array
{
$providers = collect($this->content->providers());
$moduleBreakdown = $providers->map(function (CreatorStudioProvider $provider) use ($user): array {
$summary = $provider->summary($user);
$analytics = $provider->analytics($user);
return [
'key' => $provider->key(),
'label' => $provider->label(),
'icon' => $provider->icon(),
'count' => $summary['count'],
'draft_count' => $summary['draft_count'],
'published_count' => $summary['published_count'],
'archived_count' => $summary['archived_count'],
'views' => $analytics['views'],
'appreciation' => $analytics['appreciation'],
'shares' => $analytics['shares'],
'comments' => $analytics['comments'],
'saves' => $analytics['saves'],
'index_url' => $provider->indexUrl(),
];
})->values();
$followers = (int) DB::table('user_followers')->where('user_id', $user->id)->count();
$totals = [
'views' => (int) $moduleBreakdown->sum('views'),
'appreciation' => (int) $moduleBreakdown->sum('appreciation'),
'shares' => (int) $moduleBreakdown->sum('shares'),
'comments' => (int) $moduleBreakdown->sum('comments'),
'saves' => (int) $moduleBreakdown->sum('saves'),
'followers' => $followers,
'content_count' => (int) $moduleBreakdown->sum('count'),
];
$topContent = $providers
->flatMap(fn (CreatorStudioProvider $provider) => $provider->topItems($user, 4))
->sortByDesc(fn (array $item): int => (int) ($item['engagement_score'] ?? 0))
->take(8)
->values()
->all();
return [
'totals' => $totals,
'module_breakdown' => $moduleBreakdown->all(),
'top_content' => $topContent,
'views_trend' => $this->trendSeries($user, $days, 'views'),
'engagement_trend' => $this->trendSeries($user, $days, 'engagement'),
'publishing_timeline' => $this->publishingTimeline($user, $days),
'comparison' => $this->comparison($user, $days),
'insight_blocks' => $this->insightBlocks($moduleBreakdown, $totals, $days),
'range_days' => $days,
];
}
private function insightBlocks($moduleBreakdown, array $totals, int $days): array
{
$strongest = $moduleBreakdown->sortByDesc('appreciation')->first();
$busiest = $moduleBreakdown->sortByDesc('count')->first();
$conversation = $moduleBreakdown->sortByDesc('comments')->first();
$insights = [];
if ($strongest) {
$insights[] = [
'key' => 'strongest_module',
'title' => $strongest['label'] . ' is driving the strongest reaction',
'body' => sprintf(
'%s generated %s reactions in the last %d days, making it the strongest appreciation surface in Studio right now.',
$strongest['label'],
number_format((int) $strongest['appreciation']),
$days,
),
'tone' => 'positive',
'icon' => 'fa-solid fa-sparkles',
'href' => $strongest['index_url'],
'cta' => 'Open module',
];
}
if ($conversation && (int) ($conversation['comments'] ?? 0) > 0) {
$insights[] = [
'key' => 'conversation_module',
'title' => 'Conversation is concentrating in ' . $conversation['label'],
'body' => sprintf(
'%s collected %s comments in this window. That is the clearest place to check for follow-up and community signals.',
$conversation['label'],
number_format((int) $conversation['comments']),
),
'tone' => 'action',
'icon' => 'fa-solid fa-comments',
'href' => route('studio.inbox', ['module' => $conversation['key'], 'type' => 'comment']),
'cta' => 'Open inbox',
];
}
if ($busiest && (int) ($busiest['draft_count'] ?? 0) > 0) {
$insights[] = [
'key' => 'draft_pressure',
'title' => $busiest['label'] . ' has the heaviest backlog',
'body' => sprintf(
'%s currently has %s drafts. That is the best candidate for your next cleanup, publish, or scheduling pass.',
$busiest['label'],
number_format((int) $busiest['draft_count']),
),
'tone' => 'warning',
'icon' => 'fa-solid fa-layer-group',
'href' => route('studio.drafts', ['module' => $busiest['key']]),
'cta' => 'Review drafts',
];
}
if ((int) ($totals['followers'] ?? 0) > 0) {
$insights[] = [
'key' => 'audience_presence',
'title' => 'Audience footprint is active across the workspace',
'body' => sprintf(
'You now have %s followers connected to this creator profile. Keep featured content and your publishing cadence aligned with that audience.',
number_format((int) $totals['followers']),
),
'tone' => 'neutral',
'icon' => 'fa-solid fa-user-group',
'href' => route('studio.featured'),
'cta' => 'Manage featured',
];
}
return collect($insights)->take(4)->values()->all();
}
private function publishingTimeline(User $user, int $days): array
{
$timeline = collect(range($days - 1, 0))->map(function (int $offset): array {
$date = now()->subDays($offset)->startOfDay();
return [
'date' => $date->toDateString(),
'count' => 0,
];
})->keyBy('date');
collect($this->content->recentPublished($user, 120))
->each(function (array $item) use ($timeline): void {
$publishedAt = $item['published_at'] ?? $item['updated_at'] ?? null;
if (! $publishedAt) {
return;
}
$date = date('Y-m-d', strtotime((string) $publishedAt));
if (! $timeline->has($date)) {
return;
}
$row = $timeline->get($date);
$row['count']++;
$timeline->put($date, $row);
});
return $timeline->values()->all();
}
private function trendSeries(User $user, int $days, string $metric): array
{
$series = collect(range($days - 1, 0))->map(function (int $offset): array {
$date = now()->subDays($offset)->toDateString();
return [
'date' => $date,
'value' => 0,
];
})->keyBy('date');
collect($this->content->providers())
->flatMap(fn (CreatorStudioProvider $provider) => $provider->items($user, 'all', 240))
->each(function (array $item) use ($series, $metric): void {
$dateSource = $item['published_at'] ?? $item['updated_at'] ?? $item['created_at'] ?? null;
if (! $dateSource) {
return;
}
$date = date('Y-m-d', strtotime((string) $dateSource));
if (! $series->has($date)) {
return;
}
$row = $series->get($date);
$increment = $metric === 'views'
? (int) ($item['metrics']['views'] ?? 0)
: (int) ($item['engagement_score'] ?? 0);
$row['value'] += $increment;
$series->put($date, $row);
});
return $series->values()->all();
}
private function comparison(User $user, int $days): array
{
$windowStart = now()->subDays($days)->getTimestamp();
return collect($this->content->providers())
->map(function (CreatorStudioProvider $provider) use ($user, $windowStart): array {
$items = $provider->items($user, 'all', 240)
->filter(function (array $item) use ($windowStart): bool {
$date = $item['published_at'] ?? $item['updated_at'] ?? $item['created_at'] ?? null;
return $date !== null && strtotime((string) $date) >= $windowStart;
});
return [
'key' => $provider->key(),
'label' => $provider->label(),
'icon' => $provider->icon(),
'views' => (int) $items->sum(fn (array $item): int => (int) ($item['metrics']['views'] ?? 0)),
'engagement' => (int) $items->sum(fn (array $item): int => (int) ($item['engagement_score'] ?? 0)),
'published_count' => (int) $items->filter(fn (array $item): bool => ($item['status'] ?? null) === 'published')->count(),
];
})
->values()
->all();
}
}