dashboard->build($request->user()); return Inertia::render('Collection/CollectionDashboard', [ 'summary' => $payload['summary'], 'topPerforming' => $this->collections->mapCollectionCardPayloads($payload['top_performing'], true), 'needsAttention' => $this->collections->mapCollectionCardPayloads($payload['needs_attention'], true), 'expiringCampaigns' => $this->collections->mapCollectionCardPayloads($payload['expiring_campaigns'], true), 'healthWarnings' => $payload['health_warnings'], 'filterOptions' => [ 'types' => [ Collection::TYPE_PERSONAL, Collection::TYPE_COMMUNITY, Collection::TYPE_EDITORIAL, ], 'visibilities' => [ Collection::VISIBILITY_PUBLIC, Collection::VISIBILITY_UNLISTED, Collection::VISIBILITY_PRIVATE, ], 'lifecycleStates' => [ Collection::LIFECYCLE_DRAFT, Collection::LIFECYCLE_SCHEDULED, Collection::LIFECYCLE_PUBLISHED, Collection::LIFECYCLE_FEATURED, Collection::LIFECYCLE_ARCHIVED, Collection::LIFECYCLE_HIDDEN, Collection::LIFECYCLE_RESTRICTED, Collection::LIFECYCLE_UNDER_REVIEW, Collection::LIFECYCLE_EXPIRED, ], 'workflowStates' => [ Collection::WORKFLOW_DRAFT, Collection::WORKFLOW_IN_REVIEW, Collection::WORKFLOW_APPROVED, Collection::WORKFLOW_PROGRAMMED, Collection::WORKFLOW_ARCHIVED, ], 'healthStates' => [ Collection::HEALTH_HEALTHY, Collection::HEALTH_NEEDS_METADATA, Collection::HEALTH_STALE, Collection::HEALTH_LOW_CONTENT, Collection::HEALTH_BROKEN_ITEMS, Collection::HEALTH_WEAK_COVER, Collection::HEALTH_LOW_ENGAGEMENT, Collection::HEALTH_ATTRIBUTION_INCOMPLETE, Collection::HEALTH_NEEDS_REVIEW, Collection::HEALTH_DUPLICATE_RISK, Collection::HEALTH_MERGE_CANDIDATE, ], ], 'endpoints' => [ 'managePattern' => route('settings.collections.show', ['collection' => '__COLLECTION__']), 'analyticsPattern' => route('settings.collections.analytics', ['collection' => '__COLLECTION__']), 'historyPattern' => route('settings.collections.history', ['collection' => '__COLLECTION__']), 'healthPattern' => route('settings.collections.health', ['collection' => '__COLLECTION__']), 'search' => route('settings.collections.search'), 'bulkActions' => route('settings.collections.bulk-actions'), ], 'seo' => [ 'title' => 'Collections Dashboard — Skinbase Nova', 'description' => 'Overview of collection lifecycle, quality, activity, and upcoming collection campaigns.', 'canonical' => route('settings.collections.dashboard'), 'robots' => 'noindex,follow', ], ])->rootView('collections'); } public function analytics(Request $request, Collection $collection): Response { $this->authorize('update', $collection); $collection->loadMissing(['user.profile', 'coverArtwork']); return Inertia::render('Collection/CollectionAnalytics', [ 'collection' => $this->collections->mapCollectionDetailPayload($collection, true), 'analytics' => $this->analytics->overview($collection, (int) $request->integer('days', 30)), 'historyUrl' => route('settings.collections.history', ['collection' => $collection->id]), 'dashboardUrl' => route('settings.collections.dashboard'), 'seo' => [ 'title' => sprintf('%s Analytics — Skinbase Nova', $collection->title), 'description' => sprintf('Analytics and performance history for the %s collection.', $collection->title), 'canonical' => route('settings.collections.analytics', ['collection' => $collection->id]), 'robots' => 'noindex,follow', ], ])->rootView('collections'); } public function history(Request $request, Collection $collection): Response { $this->authorize('update', $collection); $collection->loadMissing(['user.profile', 'coverArtwork']); $history = $this->history->historyFor($collection, (int) $request->integer('per_page', 40)); return Inertia::render('Collection/CollectionHistory', [ 'collection' => $this->collections->mapCollectionDetailPayload($collection, true), 'history' => $this->history->mapPaginator($history), 'canRestoreHistory' => $this->isStaff($request), 'dashboardUrl' => route('settings.collections.dashboard'), 'analyticsUrl' => route('settings.collections.analytics', ['collection' => $collection->id]), 'restorePattern' => route('settings.collections.history.restore', ['collection' => $collection->id, 'history' => '__HISTORY__']), 'seo' => [ 'title' => sprintf('%s History — Skinbase Nova', $collection->title), 'description' => sprintf('Audit history and lifecycle changes for the %s collection.', $collection->title), 'canonical' => route('settings.collections.history', ['collection' => $collection->id]), 'robots' => 'noindex,follow', ], ])->rootView('collections'); } public function restoreHistory(Request $request, Collection $collection, CollectionHistory $history): JsonResponse { $this->authorize('update', $collection); abort_unless($this->isStaff($request), 403); $collection = $this->history->restore($collection->loadMissing('user'), $history, $request->user()); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'health' => $this->health->summary($collection), 'restored_history_entry_id' => (int) $history->id, ]); } public function qualityReview(Request $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); return response()->json([ 'ok' => true, 'review' => $this->aiOperations->qualityReview($collection->loadMissing(['user.profile', 'coverArtwork'])), ]); } public function search(CollectionOwnerSearchRequest $request): JsonResponse { $filters = $request->validated(); $results = $this->search->ownerSearch($request->user(), $filters, (int) config('collections.v5.search.owner_per_page', 20)); return response()->json([ 'ok' => true, 'collections' => $this->collections->mapCollectionCardPayloads($results->items(), true), 'filters' => $filters, 'meta' => [ 'current_page' => $results->currentPage(), 'last_page' => $results->lastPage(), 'per_page' => $results->perPage(), 'total' => $results->total(), ], ]); } public function bulkActions(CollectionBulkActionsRequest $request): JsonResponse { $payload = $request->validated(); $result = $this->bulkActions->apply($request->user(), $payload); $dashboard = $this->dashboard->build($request->user()); return response()->json([ 'ok' => true, 'action' => $result['action'], 'count' => $result['count'], 'message' => $result['message'], 'collections' => $this->collections->mapCollectionCardPayloads($result['collections'], true), 'items' => $result['items'], 'summary' => $dashboard['summary'], 'healthWarnings' => $dashboard['health_warnings'], ]); } public function health(Request $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $collection->loadMissing(['user.profile', 'coverArtwork']); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection, true), 'health' => $this->health->summary($collection), 'duplicate_candidates' => $this->collections->mapCollectionCardPayloads($this->merge->duplicateCandidates($collection), true), 'history_url' => route('settings.collections.history', ['collection' => $collection->id]), ]); } public function workflowUpdate(UpdateCollectionWorkflowRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $payload = $request->validated(); if (! $this->isStaff($request)) { unset($payload['program_key'], $payload['partner_key'], $payload['experiment_key'], $payload['placement_eligibility']); } $collection = $this->workflow->update($collection->loadMissing('user'), $payload, $request->user()); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'health' => $this->health->summary($collection), ]); } public function qualityRefresh(Request $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); return response()->json([ 'ok' => true, 'queued' => true, 'result' => $this->backgroundJobs->dispatchQualityRefresh($collection->loadMissing('user'), $request->user()), 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'health' => $this->health->summary($collection), ]); } public function canonicalize(CollectionTargetActionRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $payload = $request->validated(); $target = Collection::query()->findOrFail((int) $payload['target_collection_id']); $this->authorize('update', $target); $collection = $this->canonical->designate($collection->loadMissing('user'), $target->loadMissing('user'), $request->user()); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'target' => $this->collections->mapCollectionCardPayloads([$target->fresh()->loadMissing('user')], true)[0], 'canonical_target' => $this->collections->mapCollectionCardPayloads([$target->fresh()->loadMissing('user')], true)[0], 'duplicate_candidates' => $this->merge->reviewCandidates($collection->fresh()->loadMissing('user'), true), ]); } public function merge(CollectionTargetActionRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $payload = $request->validated(); $target = Collection::query()->findOrFail((int) $payload['target_collection_id']); $this->authorize('update', $target); $result = $this->merge->mergeInto($collection->loadMissing('user'), $target->loadMissing('user'), $request->user()); return response()->json([ 'ok' => true, 'source' => $this->collections->mapCollectionDetailPayload($result['source']->loadMissing('user'), true), 'target' => $this->collections->mapCollectionDetailPayload($result['target']->loadMissing('user'), true), 'attached_artwork_ids' => $result['attached_artwork_ids'], 'canonical_target' => $this->collections->mapCollectionCardPayloads([$result['target']->loadMissing('user')], true)[0], 'duplicate_candidates' => $this->merge->reviewCandidates($result['source']->fresh()->loadMissing('user'), true), ]); } public function rejectDuplicate(CollectionTargetActionRequest $request, Collection $collection): JsonResponse { $this->authorize('update', $collection); $payload = $request->validated(); $target = Collection::query()->findOrFail((int) $payload['target_collection_id']); $this->authorize('update', $target); $collection = $this->merge->rejectCandidate($collection->loadMissing('user'), $target->loadMissing('user'), $request->user()); return response()->json([ 'ok' => true, 'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true), 'duplicate_candidates' => $this->merge->reviewCandidates($collection->fresh()->loadMissing('user'), true), 'canonical_target' => $collection->canonical_collection_id ? $this->collections->mapCollectionCardPayloads([ Collection::query()->findOrFail((int) $collection->canonical_collection_id)->loadMissing('user'), ], true)[0] : null, ]); } private function isStaff(Request $request): bool { $user = $request->user(); return $user !== null && ($user->isAdmin() || $user->isModerator()); } }