242 lines
9.6 KiB
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();
|
|
}
|
|
} |