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

322 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\CollectionComment;
use App\Models\NovaCardComment;
use App\Models\StoryComment;
use App\Models\User;
use App\Support\AvatarUrl;
use Illuminate\Support\Facades\DB;
final class CreatorStudioOverviewService
{
public function __construct(
private readonly CreatorStudioContentService $content,
private readonly CreatorStudioAnalyticsService $analytics,
private readonly CreatorStudioScheduledService $scheduled,
private readonly CreatorStudioActivityService $activity,
private readonly CreatorStudioPreferenceService $preferences,
private readonly CreatorStudioChallengeService $challenges,
private readonly CreatorStudioGrowthService $growth,
) {
}
public function build(User $user): array
{
$analytics = $this->analytics->overview($user);
$moduleSummaries = $this->content->moduleSummaries($user);
$preferences = $this->preferences->forUser($user);
$challengeData = $this->challenges->build($user);
$growthData = $this->growth->build($user, $preferences['analytics_range_days']);
$featuredContent = $this->content->selectedItems($user, $preferences['featured_content']);
return [
'kpis' => [
'total_content' => $analytics['totals']['content_count'],
'views_30d' => $analytics['totals']['views'],
'appreciation_30d' => $analytics['totals']['appreciation'],
'shares_30d' => $analytics['totals']['shares'],
'comments_30d' => $analytics['totals']['comments'],
'followers' => $analytics['totals']['followers'],
],
'module_summaries' => $moduleSummaries,
'quick_create' => $this->content->quickCreate(),
'continue_working' => $this->content->continueWorking($user, $preferences['draft_behavior']),
'scheduled_items' => $this->scheduled->upcoming($user, 5),
'recent_activity' => $this->activity->recent($user, 6),
'top_performers' => $analytics['top_content'],
'recent_comments' => $this->recentComments($user),
'recent_followers' => $this->recentFollowers($user),
'draft_reminders' => $this->content->draftReminders($user),
'stale_drafts' => $this->content->staleDrafts($user),
'recent_publishes' => $this->content->recentPublished($user, 6),
'growth_hints' => $this->growthHints($user, $moduleSummaries),
'active_challenges' => [
'summary' => $challengeData['summary'],
'spotlight' => $challengeData['spotlight'],
'items' => collect($challengeData['active_challenges'] ?? [])->take(3)->values()->all(),
],
'creator_health' => [
'score' => (int) round(collect($growthData['checkpoints'] ?? [])->avg('score') ?? 0),
'summary' => $growthData['summary'],
'checkpoints' => collect($growthData['checkpoints'] ?? [])->take(3)->values()->all(),
],
'featured_status' => $this->featuredStatus($preferences, $featuredContent),
'workflow_focus' => $this->workflowFocus($user),
'command_center' => $this->commandCenter($user),
'insight_blocks' => $analytics['insight_blocks'] ?? [],
'preferences' => [
'widget_visibility' => $preferences['widget_visibility'],
'widget_order' => $preferences['widget_order'],
'card_density' => $preferences['card_density'],
],
];
}
private function featuredStatus(array $preferences, array $featuredContent): array
{
$modules = ['artworks', 'cards', 'collections', 'stories'];
$selectedModules = array_values(array_filter($modules, fn (string $module): bool => isset($featuredContent[$module]) && is_array($featuredContent[$module])));
$missingModules = array_values(array_diff($modules, $selectedModules));
return [
'selected_count' => count($selectedModules),
'target_count' => count($modules),
'featured_modules' => $preferences['featured_modules'],
'missing_modules' => $missingModules,
'items' => collect($featuredContent)
->filter(fn ($item): bool => is_array($item))
->values()
->take(4)
->all(),
];
}
private function workflowFocus(User $user): array
{
$continue = collect($this->content->continueWorking($user, 'resume-last', 6));
return [
'priority_drafts' => $continue
->filter(fn (array $item): bool => (bool) ($item['workflow']['is_stale_draft'] ?? false) || ! (bool) ($item['workflow']['readiness']['can_publish'] ?? false))
->take(3)
->values()
->all(),
'ready_to_schedule' => $continue
->filter(fn (array $item): bool => (bool) ($item['workflow']['readiness']['can_publish'] ?? false))
->take(3)
->values()
->all(),
];
}
private function commandCenter(User $user): array
{
$scheduled = collect($this->scheduled->upcoming($user, 16));
$inbox = collect($this->activity->recent($user, 16));
$todayStart = now()->startOfDay();
$todayEnd = now()->endOfDay();
return [
'publishing_today' => $scheduled->filter(function (array $item) use ($todayStart, $todayEnd): bool {
$timestamp = strtotime((string) ($item['scheduled_at'] ?? $item['published_at'] ?? '')) ?: 0;
return $timestamp >= $todayStart->getTimestamp() && $timestamp <= $todayEnd->getTimestamp();
})->values()->all(),
'attention_now' => $inbox->filter(fn (array $item): bool => in_array((string) ($item['type'] ?? ''), ['comment', 'notification'], true))->take(4)->values()->all(),
];
}
private function growthHints(User $user, array $moduleSummaries): array
{
$user->loadMissing('profile');
$summaries = collect($moduleSummaries)->keyBy('key');
$hints = [];
if (blank($user->profile?->bio) || blank($user->profile?->tagline)) {
$hints[] = [
'title' => 'Complete your creator profile',
'body' => 'Add a tagline and bio so your public presence matches the work you are publishing.',
'url' => route('studio.profile'),
'label' => 'Update profile',
];
}
if (((int) ($summaries->get('cards')['count'] ?? 0)) === 0) {
$hints[] = [
'title' => 'Publish your first card',
'body' => 'Cards now live inside Creator Studio, making short-form publishing a first-class workflow.',
'url' => route('studio.cards.create'),
'label' => 'Create card',
];
}
if (((int) ($summaries->get('collections')['count'] ?? 0)) === 0) {
$hints[] = [
'title' => 'Create a featured collection',
'body' => 'Curated collections give your profile a stronger editorial shape and a better publishing shelf.',
'url' => route('settings.collections.create'),
'label' => 'Start collection',
];
}
if (((int) ($summaries->get('artworks')['count'] ?? 0)) === 0) {
$hints[] = [
'title' => 'Upload your first artwork',
'body' => 'Seed the workspace with a first long-form piece so analytics, drafts, and collections have something to build on.',
'url' => '/upload',
'label' => 'Upload artwork',
];
}
return collect($hints)->take(3)->values()->all();
}
public function recentComments(User $user, int $limit = 12): array
{
$artworkComments = DB::table('artwork_comments')
->join('artworks', 'artworks.id', '=', 'artwork_comments.artwork_id')
->join('users', 'users.id', '=', 'artwork_comments.user_id')
->where('artworks.user_id', $user->id)
->whereNull('artwork_comments.deleted_at')
->select([
'artwork_comments.id',
'artwork_comments.content as body',
'artwork_comments.created_at',
'users.name as author_name',
'artworks.title as item_title',
'artworks.slug as item_slug',
'artworks.id as item_id',
])
->orderByDesc('artwork_comments.created_at')
->limit($limit)
->get()
->map(fn ($row): array => [
'id' => sprintf('artworks:%d', (int) $row->id),
'module' => 'artworks',
'module_label' => 'Artworks',
'author_name' => $row->author_name,
'item_title' => $row->item_title,
'body' => $row->body,
'created_at' => $this->normalizeDate($row->created_at),
'context_url' => route('art.show', ['id' => $row->item_id, 'slug' => $row->item_slug]),
]);
$cardComments = NovaCardComment::query()
->with(['user:id,name,username', 'card:id,title,slug'])
->whereNull('deleted_at')
->whereHas('card', fn ($query) => $query->where('user_id', $user->id))
->latest('created_at')
->limit($limit)
->get()
->map(fn (NovaCardComment $comment): array => [
'id' => sprintf('cards:%d', (int) $comment->id),
'module' => 'cards',
'module_label' => 'Cards',
'author_name' => $comment->user?->name ?: $comment->user?->username ?: 'Creator',
'item_title' => $comment->card?->title,
'body' => $comment->body,
'created_at' => $comment->created_at?->toIso8601String(),
'context_url' => $comment->card ? route('studio.cards.analytics', ['id' => $comment->card->id]) : route('studio.cards.index'),
]);
$collectionComments = CollectionComment::query()
->with(['user:id,name,username', 'collection:id,title'])
->whereNull('deleted_at')
->whereHas('collection', fn ($query) => $query->where('user_id', $user->id))
->latest('created_at')
->limit($limit)
->get()
->map(fn (CollectionComment $comment): array => [
'id' => sprintf('collections:%d', (int) $comment->id),
'module' => 'collections',
'module_label' => 'Collections',
'author_name' => $comment->user?->name ?: $comment->user?->username ?: 'Creator',
'item_title' => $comment->collection?->title,
'body' => $comment->body,
'created_at' => $comment->created_at?->toIso8601String(),
'context_url' => $comment->collection ? route('settings.collections.show', ['collection' => $comment->collection->id]) : route('studio.collections'),
]);
$storyComments = StoryComment::query()
->with(['user:id,name,username', 'story:id,title'])
->whereNull('deleted_at')
->where('is_approved', true)
->whereHas('story', fn ($query) => $query->where('creator_id', $user->id))
->latest('created_at')
->limit($limit)
->get()
->map(fn (StoryComment $comment): array => [
'id' => sprintf('stories:%d', (int) $comment->id),
'module' => 'stories',
'module_label' => 'Stories',
'author_name' => $comment->user?->name ?: $comment->user?->username ?: 'Creator',
'item_title' => $comment->story?->title,
'body' => $comment->content,
'created_at' => $comment->created_at?->toIso8601String(),
'context_url' => $comment->story ? route('creator.stories.analytics', ['story' => $comment->story->id]) : route('studio.stories'),
]);
return $artworkComments
->concat($cardComments)
->concat($collectionComments)
->concat($storyComments)
->sortByDesc(fn (array $comment): int => $this->timestamp($comment['created_at'] ?? null))
->take($limit)
->values()
->all();
}
public function recentFollowers(User $user, int $limit = 8): array
{
return DB::table('user_followers as uf')
->join('users as follower', 'follower.id', '=', 'uf.follower_id')
->leftJoin('user_profiles as profile', 'profile.user_id', '=', 'follower.id')
->where('uf.user_id', $user->id)
->whereNull('follower.deleted_at')
->orderByDesc('uf.created_at')
->limit($limit)
->get([
'follower.id',
'follower.username',
'follower.name',
'profile.avatar_hash',
'uf.created_at',
])
->map(fn ($row): array => [
'id' => (int) $row->id,
'name' => $row->name ?: '@' . $row->username,
'username' => $row->username,
'profile_url' => '/@' . strtolower((string) $row->username),
'avatar_url' => AvatarUrl::forUser((int) $row->id, $row->avatar_hash, 64),
'created_at' => $this->normalizeDate($row->created_at),
])
->values()
->all();
}
private function normalizeDate(mixed $value): ?string
{
if ($value instanceof \DateTimeInterface) {
return $value->format(DATE_ATOM);
}
if (is_string($value) && $value !== '') {
return $value;
}
return null;
}
private function timestamp(mixed $value): int
{
if (! is_string($value) || $value === '') {
return 0;
}
return strtotime($value) ?: 0;
}
}