withTrashed()->where('user_id', $user->id); $count = (clone $baseQuery) ->whereNull('deleted_at') ->count(); $draftCount = (clone $baseQuery) ->whereNull('deleted_at') ->where(function (Builder $query): void { $query->where('is_public', false) ->orWhere('artwork_status', 'draft'); }) ->count(); $publishedCount = (clone $baseQuery) ->whereNull('deleted_at') ->where('is_public', true) ->whereNotNull('published_at') ->count(); $recentPublishedCount = (clone $baseQuery) ->whereNull('deleted_at') ->where('is_public', true) ->where('published_at', '>=', now()->subDays(30)) ->count(); $archivedCount = Artwork::onlyTrashed() ->where('user_id', $user->id) ->count(); return [ 'key' => $this->key(), 'label' => $this->label(), 'icon' => $this->icon(), 'count' => $count, 'draft_count' => $draftCount, 'published_count' => $publishedCount, 'archived_count' => $archivedCount, 'trend_value' => $recentPublishedCount, 'trend_label' => 'published in 30d', 'quick_action_label' => 'Upload artwork', 'index_url' => $this->indexUrl(), 'create_url' => $this->createUrl(), ]; } public function items(User $user, string $bucket = 'all', int $limit = 200): Collection { $query = Artwork::query() ->withTrashed() ->where('user_id', $user->id) ->with(['stats', 'categories', 'tags']) ->orderByDesc('updated_at') ->limit($limit); if ($bucket === 'drafts') { $query->whereNull('deleted_at') ->where(function (Builder $builder): void { $builder->where('is_public', false) ->orWhere('artwork_status', 'draft'); }); } elseif ($bucket === 'scheduled') { $query->whereNull('deleted_at') ->where('artwork_status', 'scheduled'); } elseif ($bucket === 'archived') { $query->onlyTrashed(); } elseif ($bucket === 'published') { $query->whereNull('deleted_at') ->where('is_public', true) ->whereNotNull('published_at'); } else { $query->whereNull('deleted_at'); } return $query->get()->map(fn (Artwork $artwork): array => $this->mapItem($artwork)); } public function topItems(User $user, int $limit = 5): Collection { return Artwork::query() ->where('user_id', $user->id) ->whereNull('deleted_at') ->where('is_public', true) ->with(['stats', 'categories', 'tags']) ->whereHas('stats') ->orderByDesc( ArtworkStats::select('ranking_score') ->whereColumn('artwork_stats.artwork_id', 'artworks.id') ->limit(1) ) ->limit($limit) ->get() ->map(fn (Artwork $artwork): array => $this->mapItem($artwork)); } public function analytics(User $user): array { $totals = DB::table('artwork_stats') ->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id') ->where('artworks.user_id', $user->id) ->whereNull('artworks.deleted_at') ->selectRaw('COALESCE(SUM(artwork_stats.views), 0) as views') ->selectRaw('COALESCE(SUM(artwork_stats.favorites), 0) as appreciation') ->selectRaw('COALESCE(SUM(artwork_stats.shares_count), 0) as shares') ->selectRaw('COALESCE(SUM(artwork_stats.comments_count), 0) as comments') ->selectRaw('COALESCE(SUM(artwork_stats.downloads), 0) as saves') ->first(); return [ 'views' => (int) ($totals->views ?? 0), 'appreciation' => (int) ($totals->appreciation ?? 0), 'shares' => (int) ($totals->shares ?? 0), 'comments' => (int) ($totals->comments ?? 0), 'saves' => (int) ($totals->saves ?? 0), ]; } public function scheduledItems(User $user, int $limit = 50): Collection { return $this->items($user, 'scheduled', $limit); } private function mapItem(Artwork $artwork): array { $stats = $artwork->stats; $status = $artwork->deleted_at ? 'archived' : ($artwork->artwork_status === 'scheduled' ? 'scheduled' : ((bool) $artwork->is_public ? 'published' : 'draft')); $category = $artwork->categories->first(); $visibility = $artwork->visibility ?: ((bool) $artwork->is_public ? Artwork::VISIBILITY_PUBLIC : Artwork::VISIBILITY_PRIVATE); return [ 'id' => sprintf('%s:%d', $this->key(), (int) $artwork->id), 'numeric_id' => (int) $artwork->id, 'module' => $this->key(), 'module_label' => $this->label(), 'module_icon' => $this->icon(), 'title' => $artwork->title, 'subtitle' => $category?->name, 'description' => $artwork->description, 'status' => $status, 'visibility' => $visibility, 'image_url' => $artwork->thumbUrl('md'), 'preview_url' => $artwork->published_at ? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]) : route('studio.artworks.edit', ['id' => $artwork->id]), 'view_url' => $artwork->published_at ? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]) : route('studio.artworks.edit', ['id' => $artwork->id]), 'edit_url' => route('studio.artworks.edit', ['id' => $artwork->id]), 'manage_url' => route('studio.artworks.edit', ['id' => $artwork->id]), 'analytics_url' => route('studio.artworks.analytics', ['id' => $artwork->id]), 'create_url' => $this->createUrl(), 'actions' => $this->actionsFor($artwork, $status), 'created_at' => $artwork->created_at?->toIso8601String(), 'updated_at' => $artwork->updated_at?->toIso8601String(), 'published_at' => $artwork->published_at?->toIso8601String(), 'scheduled_at' => $artwork->publish_at?->toIso8601String(), 'schedule_timezone' => $artwork->artwork_timezone, 'featured' => false, 'metrics' => [ 'views' => (int) ($stats?->views ?? 0), 'appreciation' => (int) ($stats?->favorites ?? 0), 'shares' => (int) ($stats?->shares_count ?? 0), 'comments' => (int) ($stats?->comments_count ?? 0), 'saves' => (int) ($stats?->downloads ?? 0), ], 'engagement_score' => (int) ($stats?->views ?? 0) + ((int) ($stats?->favorites ?? 0) * 2) + ((int) ($stats?->comments_count ?? 0) * 3) + ((int) ($stats?->shares_count ?? 0) * 2), 'taxonomies' => [ 'categories' => $artwork->categories->map(fn ($entry): array => [ 'id' => (int) $entry->id, 'name' => (string) $entry->name, 'slug' => (string) $entry->slug, ])->values()->all(), 'tags' => $artwork->tags->map(fn ($entry): array => [ 'id' => (int) $entry->id, 'name' => (string) $entry->name, 'slug' => (string) $entry->slug, ])->values()->all(), ], ]; } private function actionsFor(Artwork $artwork, string $status): array { $actions = []; if ($status === 'draft') { $actions[] = $this->requestAction('publish', 'Publish', 'fa-solid fa-rocket', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'publish']); } if ($status === 'scheduled') { $actions[] = $this->requestAction('publish_now', 'Publish now', 'fa-solid fa-bolt', route('api.studio.schedule.publishNow', ['module' => 'artworks', 'id' => $artwork->id]), []); $actions[] = $this->requestAction('unschedule', 'Unschedule', 'fa-solid fa-calendar-xmark', route('api.studio.schedule.unschedule', ['module' => 'artworks', 'id' => $artwork->id]), []); } if ($status === 'published') { $actions[] = $this->requestAction('unpublish', 'Unpublish', 'fa-solid fa-eye-slash', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'unpublish']); $actions[] = $this->requestAction('archive', 'Archive', 'fa-solid fa-box-archive', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'archive']); } if ($status === 'archived') { $actions[] = $this->requestAction('restore', 'Restore', 'fa-solid fa-rotate-left', route('api.studio.artworks.toggle', ['id' => $artwork->id]), ['action' => 'unarchive']); } $actions[] = $this->requestAction( 'delete', 'Delete', 'fa-solid fa-trash', route('api.studio.artworks.bulk'), [ 'action' => 'delete', 'artwork_ids' => [$artwork->id], 'confirm' => 'DELETE', ], 'Delete this artwork permanently?' ); return $actions; } private function requestAction(string $key, string $label, string $icon, string $url, array $payload, ?string $confirm = null): array { return [ 'key' => $key, 'label' => $label, 'icon' => $icon, 'type' => 'request', 'method' => 'post', 'url' => $url, 'payload' => $payload, 'confirm' => $confirm, ]; } }