queueStats(); $status = $this->initialStatus($request, $stats); $routes = $this->routeNamesForRequest($request); return Inertia::render('Moderation/ArtworkMaturityQueue', [ 'title' => 'Artwork Maturity Queue', 'initialItems' => $this->queueItems($status), 'initialFilters' => [ 'status' => $status, 'ai_action' => 'all', 'ai_status' => 'all', ], 'stats' => $stats, 'endpoints' => [ 'list' => route($routes['list']), 'reviewPattern' => route($routes['review'], ['artwork' => '__ARTWORK__']), ], 'filterOptions' => [ 'aiAction' => [ ['value' => 'all', 'label' => 'All actions'], ['value' => ArtworkMaturityService::AI_ACTION_SAFE, 'label' => 'Safe'], ['value' => ArtworkMaturityService::AI_ACTION_REVIEW, 'label' => 'Review'], ['value' => ArtworkMaturityService::AI_ACTION_FLAG_HIGH, 'label' => 'Flag high'], ], 'aiStatus' => [ ['value' => 'all', 'label' => 'All statuses'], ['value' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'label' => 'Succeeded'], ['value' => ArtworkMaturityService::AI_STATUS_PENDING, 'label' => 'Pending'], ['value' => ArtworkMaturityService::AI_STATUS_FAILED, 'label' => 'Failed'], ['value' => ArtworkMaturityService::AI_STATUS_SKIPPED, 'label' => 'Skipped'], ], ], 'reviewActions' => [ ['value' => 'mark_safe', 'label' => 'Mark safe'], ['value' => 'mark_mature', 'label' => 'Mark mature'], ['value' => 'confirm_current', 'label' => 'Confirm current state'], ], ])->rootView('moderation'); } public function list(Request $request): JsonResponse { $status = $this->normalizeStatus((string) $request->query('status', 'suspected')); $aiAction = strtolower((string) $request->query('ai_action', 'all')); $aiStatus = strtolower((string) $request->query('ai_status', 'all')); return response()->json([ 'data' => $this->queueItems($status, $aiAction, $aiStatus), 'meta' => [ 'stats' => $this->queueStats(), 'status' => $status, 'filters' => [ 'ai_action' => $aiAction, 'ai_status' => $aiStatus, ], ], ]); } public function review(Request $request, Artwork $artwork): JsonResponse { $validated = $request->validate([ 'action' => ['required', 'in:mark_safe,mark_mature,confirm_current'], 'note' => ['nullable', 'string', 'max:2000'], ]); /** @var User $moderator */ $moderator = $request->user('controlpanel') ?? $request->user() ?? abort(403, 'Admin access required.'); $artwork = $this->maturity->review($artwork, (string) $validated['action'], $moderator, $validated['note'] ?? null); $this->audit->resolveFindingForReview($artwork, $moderator, (string) $validated['action'], $validated['note'] ?? null); return response()->json([ 'success' => true, 'artwork' => $this->mapQueueItem($artwork->loadMissing(['user.profile', 'group', 'categories.contentType'])), 'stats' => $this->queueStats(), ]); } /** * @return array> */ private function queueItems(string $status, string $aiAction = 'all', string $aiStatus = 'all'): array { if ($status === 'audit') { return $this->auditQueueItems($aiAction, $aiStatus); } $query = Artwork::query() ->with(['user.profile', 'group', 'categories.contentType']) ->where(function ($builder): void { $builder->where('maturity_status', ArtworkMaturityService::STATUS_SUSPECTED) ->orWhere(function ($reviewed): void { $reviewed->where('maturity_status', ArtworkMaturityService::STATUS_REVIEWED) ->whereNotNull('maturity_reviewed_at'); }); }) ->latest('maturity_flagged_at') ->latest('published_at') ->limit(100); if ($status === 'reviewed') { $query->where('maturity_status', ArtworkMaturityService::STATUS_REVIEWED); } else { $query->where('maturity_status', ArtworkMaturityService::STATUS_SUSPECTED); } if (in_array($aiAction, [ ArtworkMaturityService::AI_ACTION_SAFE, ArtworkMaturityService::AI_ACTION_REVIEW, ArtworkMaturityService::AI_ACTION_FLAG_HIGH, ], true)) { $query->where('maturity_ai_action_hint', $aiAction); } if (in_array($aiStatus, [ ArtworkMaturityService::AI_STATUS_SUCCEEDED, ArtworkMaturityService::AI_STATUS_PENDING, ArtworkMaturityService::AI_STATUS_FAILED, ArtworkMaturityService::AI_STATUS_SKIPPED, ], true)) { $query->where('maturity_ai_status', $aiStatus); } return $query->get()->map(fn (Artwork $artwork): array => $this->mapQueueItem($artwork))->all(); } /** * @return array> */ private function auditQueueItems(string $aiAction = 'all', string $aiStatus = 'all'): array { $query = $this->audit->openFindingsQuery() ->latest('detected_at') ->latest('updated_at') ->limit(100); if (in_array($aiAction, [ ArtworkMaturityService::AI_ACTION_SAFE, ArtworkMaturityService::AI_ACTION_REVIEW, ArtworkMaturityService::AI_ACTION_FLAG_HIGH, ], true)) { $query->where('ai_action_hint', $aiAction); } if (in_array($aiStatus, [ ArtworkMaturityService::AI_STATUS_SUCCEEDED, ArtworkMaturityService::AI_STATUS_PENDING, ArtworkMaturityService::AI_STATUS_FAILED, ArtworkMaturityService::AI_STATUS_SKIPPED, ArtworkMaturityService::AI_STATUS_NOT_REQUESTED, ], true)) { $query->where('ai_status', $aiStatus); } return $query->get()->map(fn (ArtworkMaturityAuditFinding $finding): array => $this->mapAuditQueueItem($finding))->all(); } /** * @return array */ private function queueStats(): array { return [ 'suspected' => (int) Artwork::query()->where('maturity_status', ArtworkMaturityService::STATUS_SUSPECTED)->count(), 'audit' => $this->audit->openFindingsCount(), 'reviewed' => (int) Artwork::query()->where('maturity_status', ArtworkMaturityService::STATUS_REVIEWED)->count(), 'mature' => (int) Artwork::query()->where('is_mature', true)->count(), ]; } /** * @return array */ private function mapAuditQueueItem(ArtworkMaturityAuditFinding $finding): array { $artwork = $finding->artwork; return $this->mapQueueItem($artwork, [ 'status' => (string) $finding->status, 'thumbnail_variant' => $finding->thumbnail_variant, 'detected_at' => optional($finding->detected_at)->toIsoString(), 'last_scanned_at' => optional($finding->last_scanned_at)->toIsoString(), 'ai_label' => $finding->ai_label, 'ai_confidence' => $finding->ai_confidence, 'ai_score' => $finding->ai_score, 'ai_labels' => $finding->ai_labels, 'ai_model' => $finding->ai_model, 'ai_threshold_used' => $finding->ai_threshold_used, 'ai_analysis_time_ms' => $finding->ai_analysis_time_ms, 'ai_action_hint' => $finding->ai_action_hint, 'ai_status' => $finding->ai_status, 'ai_advisory' => $finding->ai_advisory, 'legacy_unset' => $this->audit->isArtworkEligible($artwork), ]); } /** * @return array */ private function mapQueueItem(Artwork $artwork, ?array $audit = null): array { $category = $artwork->categories->sortBy('sort_order')->first(); $publisherName = $artwork->group?->name ?: $artwork->user?->name ?: $artwork->user?->username ?: 'Artist'; $thumb = ThumbnailPresenter::present($artwork, 'md'); $preview = ThumbnailPresenter::present($artwork, 'xl'); return [ 'id' => (int) $artwork->id, 'title' => (string) $artwork->title, 'url' => route('art.show', ['id' => $artwork->id, 'slug' => Str::slug((string) ($artwork->slug ?: $artwork->title)) ?: (string) $artwork->id]), 'admin_url' => route('admin.cp.artworks.edit', ['id' => $artwork->id]), 'thumbnail' => $thumb['url'] ?? null, 'preview_image' => $preview['url'] ?? ($thumb['url'] ?? null), 'publisher' => $publisherName, 'published_at' => optional($artwork->published_at)->toIsoString(), 'content_type' => $category?->contentType?->name, 'category' => $category?->name, 'maturity' => $this->maturity->presentation($artwork, null), 'audit' => $audit, 'review' => [ 'reviewed_at' => optional($artwork->maturity_reviewed_at)->toIsoString(), 'reviewed_by' => $artwork->maturity_reviewed_by, 'reviewer_note' => $artwork->maturity_reviewer_note, ], ]; } private function normalizeStatus(string $status): string { $normalized = Str::lower(trim($status)); return in_array($normalized, ['suspected', 'reviewed', 'audit'], true) ? $normalized : 'suspected'; } /** * @param array $stats */ private function initialStatus(Request $request, array $stats): string { if ($request->query->has('status')) { return $this->normalizeStatus((string) $request->query('status')); } if (($stats['suspected'] ?? 0) > 0) { return 'suspected'; } if (($stats['audit'] ?? 0) > 0) { return 'audit'; } if (($stats['reviewed'] ?? 0) > 0) { return 'reviewed'; } return 'suspected'; } /** * @return array{list: string, review: string} */ private function routeNamesForRequest(Request $request): array { $routeName = (string) $request->route()?->getName(); if (Str::startsWith($routeName, 'admin.cp.artworks.maturity.')) { return [ 'list' => 'admin.cp.artworks.maturity.queue', 'review' => 'admin.cp.artworks.maturity.review', ]; } return [ 'list' => 'cp.maturity.list', 'review' => 'cp.maturity.review', ]; } }