optimizations
This commit is contained in:
342
app/Http/Controllers/Settings/CollectionInsightsController.php
Normal file
342
app/Http/Controllers/Settings/CollectionInsightsController.php
Normal file
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Collections\CollectionBulkActionsRequest;
|
||||
use App\Http\Requests\Collections\CollectionOwnerSearchRequest;
|
||||
use App\Http\Requests\Collections\CollectionTargetActionRequest;
|
||||
use App\Http\Requests\Collections\UpdateCollectionWorkflowRequest;
|
||||
use App\Models\Collection;
|
||||
use App\Models\CollectionHistory;
|
||||
use App\Services\CollectionAiOperationsService;
|
||||
use App\Services\CollectionAnalyticsService;
|
||||
use App\Services\CollectionBackgroundJobService;
|
||||
use App\Services\CollectionBulkActionService;
|
||||
use App\Services\CollectionCanonicalService;
|
||||
use App\Services\CollectionDashboardService;
|
||||
use App\Services\CollectionHistoryService;
|
||||
use App\Services\CollectionHealthService;
|
||||
use App\Services\CollectionMergeService;
|
||||
use App\Services\CollectionSearchService;
|
||||
use App\Services\CollectionService;
|
||||
use App\Services\CollectionWorkflowService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class CollectionInsightsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionDashboardService $dashboard,
|
||||
private readonly CollectionAnalyticsService $analytics,
|
||||
private readonly CollectionHistoryService $history,
|
||||
private readonly CollectionAiOperationsService $aiOperations,
|
||||
private readonly CollectionBackgroundJobService $backgroundJobs,
|
||||
private readonly CollectionBulkActionService $bulkActions,
|
||||
private readonly CollectionService $collections,
|
||||
private readonly CollectionSearchService $search,
|
||||
private readonly CollectionHealthService $health,
|
||||
private readonly CollectionWorkflowService $workflow,
|
||||
private readonly CollectionCanonicalService $canonical,
|
||||
private readonly CollectionMergeService $merge,
|
||||
) {
|
||||
}
|
||||
|
||||
public function dashboard(Request $request): Response
|
||||
{
|
||||
$payload = $this->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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user