optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,459 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Collection;
use App\Models\CollectionSurfaceDefinition;
use App\Models\CollectionSurfacePlacement;
use App\Services\CollectionCampaignService;
use App\Services\CollectionHistoryService;
use App\Services\CollectionService;
use App\Services\CollectionSurfaceService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class CollectionSurfaceController extends Controller
{
public function __construct(
private readonly CollectionSurfaceService $surfaces,
private readonly CollectionService $collections,
private readonly CollectionHistoryService $history,
private readonly CollectionCampaignService $campaigns,
) {
}
public function index(Request $request): Response
{
$this->authorizeStaff($request);
$conflicts = $this->surfaces->placementConflicts();
$conflictPlacementIds = $conflicts->pluck('placement_ids')->flatten()->unique()->map(fn ($id) => (int) $id)->all();
$definitions = $this->surfaces->definitions()->map(function ($definition): array {
return $this->mapDefinition($definition);
})->values()->all();
$placements = $this->surfaces->placements()->map(function ($placement) use ($conflictPlacementIds): array {
return array_merge($this->mapPlacement($placement), [
'has_conflict' => in_array((int) $placement->id, $conflictPlacementIds, true),
]);
})->values()->all();
return Inertia::render('Collection/CollectionStaffSurfaces', [
'definitions' => $definitions,
'placements' => $placements,
'conflicts' => $conflicts->all(),
'surfaceKeyOptions' => collect($definitions)->pluck('surface_key')->values()->all(),
'collectionOptions' => $this->collections->mapCollectionCardPayloads(
Collection::query()->public()->orderByDesc('ranking_score')->limit(30)->get(),
true,
),
'endpoints' => [
'definitionsStore' => route('settings.collections.surfaces.definitions.store'),
'definitionsUpdatePattern' => route('settings.collections.surfaces.definitions.update', ['definition' => '__DEFINITION__']),
'definitionsDeletePattern' => route('settings.collections.surfaces.definitions.destroy', ['definition' => '__DEFINITION__']),
'placementsStore' => route('settings.collections.surfaces.placements.store'),
'placementsUpdatePattern' => route('settings.collections.surfaces.placements.update', ['placement' => '__PLACEMENT__']),
'placementsDeletePattern' => route('settings.collections.surfaces.placements.destroy', ['placement' => '__PLACEMENT__']),
'previewPattern' => route('settings.collections.surfaces.preview', ['definition' => '__DEFINITION__']),
'batchEditorial' => route('settings.collections.surfaces.batch-editorial'),
],
'seo' => [
'title' => 'Collection Surfaces - Skinbase Nova',
'description' => 'Staff tools for homepage, discovery, and campaign collection surfaces.',
'canonical' => route('settings.collections.surfaces.index'),
'robots' => 'noindex,follow',
],
])->rootView('collections');
}
public function storeDefinition(Request $request): JsonResponse
{
$this->authorizeStaff($request);
$definition = $this->surfaces->upsertDefinition($this->validateDefinition($request));
return response()->json([
'ok' => true,
'definition' => $this->mapDefinition($definition->fresh()),
]);
}
public function updateDefinition(Request $request, CollectionSurfaceDefinition $definition): JsonResponse
{
$this->authorizeStaff($request);
$payload = $this->validateDefinition($request);
$payload['surface_key'] = $definition->surface_key;
$updatedDefinition = $this->surfaces->upsertDefinition($payload);
return response()->json([
'ok' => true,
'definition' => $this->mapDefinition($updatedDefinition->fresh()),
]);
}
public function destroyDefinition(Request $request, CollectionSurfaceDefinition $definition): JsonResponse
{
$this->authorizeStaff($request);
abort_if(
CollectionSurfacePlacement::query()->where('surface_key', $definition->surface_key)->exists(),
422,
'Remove all placements from this surface before deleting the definition.'
);
$deletedId = (int) $definition->id;
$definition->delete();
return response()->json([
'ok' => true,
'deleted_definition_id' => $deletedId,
]);
}
public function storePlacement(Request $request): JsonResponse
{
$this->authorizeStaff($request);
$payload = $this->validatePlacement($request);
$payload['created_by_user_id'] = $request->user()?->id;
$collection = Collection::query()->findOrFail((int) $payload['collection_id']);
abort_unless($collection->isFeatureablePublicly(), 422, 'Only public, active collections can be placed on public surfaces.');
$placement = $this->surfaces->upsertPlacement($payload)->loadMissing([
'collection.user:id,username,name',
'collection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
]);
$this->history->record(
$collection->fresh(),
$request->user(),
'placement_assigned',
'Collection assigned to a staff surface.',
null,
$this->placementHistoryPayload($placement)
);
return response()->json([
'ok' => true,
'placement' => $this->mapPlacement($placement),
'conflicts' => $this->surfaces->placementConflicts()->all(),
]);
}
public function updatePlacement(Request $request, CollectionSurfacePlacement $placement): JsonResponse
{
$this->authorizeStaff($request);
$before = $this->placementHistoryPayload($placement);
$originalCollectionId = (int) $placement->collection_id;
$payload = $this->validatePlacement($request);
$payload['id'] = (int) $placement->id;
$payload['created_by_user_id'] = $placement->created_by_user_id ?: $request->user()?->id;
$collection = Collection::query()->findOrFail((int) $payload['collection_id']);
abort_unless($collection->isFeatureablePublicly(), 422, 'Only public, active collections can be placed on public surfaces.');
$updatedPlacement = $this->surfaces->upsertPlacement($payload)->loadMissing([
'collection.user:id,username,name',
'collection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
]);
if ($originalCollectionId !== (int) $updatedPlacement->collection_id) {
$originalCollection = Collection::query()->find($originalCollectionId);
if ($originalCollection) {
$this->history->record(
$originalCollection->fresh(),
$request->user(),
'placement_removed',
'Collection removed from a staff surface assignment.',
$before,
null
);
}
$this->history->record(
$collection->fresh(),
$request->user(),
'placement_assigned',
'Collection assigned to a staff surface.',
null,
$this->placementHistoryPayload($updatedPlacement)
);
} else {
$this->history->record(
$collection->fresh(),
$request->user(),
'placement_updated',
'Collection staff surface assignment updated.',
$before,
$this->placementHistoryPayload($updatedPlacement)
);
}
return response()->json([
'ok' => true,
'placement' => $this->mapPlacement($updatedPlacement),
'conflicts' => $this->surfaces->placementConflicts()->all(),
]);
}
public function destroyPlacement(Request $request, CollectionSurfacePlacement $placement): JsonResponse
{
$this->authorizeStaff($request);
$before = $this->placementHistoryPayload($placement);
$collection = $placement->collection()->first();
$deletedId = (int) $placement->id;
$this->surfaces->deletePlacement($placement);
if ($collection) {
$this->history->record(
$collection->fresh(),
$request->user(),
'placement_removed',
'Collection removed from a staff surface assignment.',
$before,
null
);
}
return response()->json([
'ok' => true,
'deleted_placement_id' => $deletedId,
'conflicts' => $this->surfaces->placementConflicts()->all(),
]);
}
public function preview(Request $request, CollectionSurfaceDefinition $definition): JsonResponse
{
$this->authorizeStaff($request);
$items = $this->surfaces->resolveSurfaceItems(
$definition->surface_key,
(int) $request->integer('limit', (int) $definition->max_items)
);
return response()->json([
'ok' => true,
'collections' => $this->collections->mapCollectionCardPayloads($items, true),
]);
}
public function batchEditorial(Request $request): JsonResponse
{
$this->authorizeStaff($request);
$payload = $this->validateBatchEditorial($request);
$collectionIds = collect($payload['collection_ids'] ?? [])->map(fn ($id) => (int) $id)->filter()->values()->all();
abort_if(count($collectionIds) === 0, 422, 'Choose at least one collection for the batch editorial run.');
if (! ($payload['apply'] ?? false)) {
return response()->json([
'ok' => true,
'mode' => 'preview',
'plan' => $this->campaigns->batchEditorialPlan($collectionIds, $payload),
]);
}
$result = DB::transaction(function () use ($collectionIds, $payload, $request): array {
$actor = $request->user();
$applied = $this->campaigns->applyBatchEditorialPlan($collectionIds, $payload, $actor);
$resultsByCollection = collect($applied['results'] ?? [])->keyBy('collection_id');
foreach ($applied['plan']['items'] as $item) {
$collectionId = (int) Arr::get($item, 'collection.id');
$collection = Collection::query()->find($collectionId);
$itemResult = $resultsByCollection->get($collectionId, []);
if (! $collection) {
continue;
}
$summaryParts = [];
if (count($item['campaign_updates'] ?? []) > 0) {
$summaryParts[] = 'campaign metadata refreshed';
}
if (is_array($item['placement'] ?? null)) {
$summaryParts[] = ($item['placement']['eligible'] ?? false)
? sprintf('placement planned for %s', (string) $item['placement']['surface_key'])
: 'placement skipped';
}
if (($itemResult['placement']['status'] ?? null) === 'created') {
$this->history->record(
$collection->fresh(),
$actor,
'placement_assigned',
'Collection assigned to a staff surface via batch editorial tools.',
null,
$item['placement'] ?? null
);
}
if (($itemResult['placement']['status'] ?? null) === 'updated') {
$this->history->record(
$collection->fresh(),
$actor,
'placement_updated',
'Collection staff surface assignment updated via batch editorial tools.',
null,
$item['placement'] ?? null
);
}
$this->history->record(
$collection->fresh(),
$actor,
'batch_editorial_updated',
'Staff batch editorial tools updated campaign planning.',
null,
[
'summary' => $summaryParts,
'campaign_updates' => $item['campaign_updates'] ?? [],
'placement' => $item['placement'] ?? null,
]
);
}
return $applied;
});
return response()->json([
'ok' => true,
'mode' => 'apply',
'plan' => $result['plan'],
'results' => $result['results'],
'placements' => $this->surfaces->placements()->map(fn ($placement): array => $this->mapPlacement($placement))->values()->all(),
'conflicts' => $this->surfaces->placementConflicts()->all(),
]);
}
private function authorizeStaff(Request $request): void
{
$user = $request->user();
abort_unless($user && ($user->isAdmin() || $user->isModerator()), 403);
}
private function validateDefinition(Request $request): array
{
return $request->validate([
'surface_key' => ['required', 'string', 'max:120'],
'title' => ['required', 'string', 'max:160'],
'description' => ['nullable', 'string', 'max:400'],
'mode' => ['required', 'in:manual,automatic,hybrid'],
'ranking_mode' => ['required', 'in:ranking_score,recent_activity,quality_score'],
'max_items' => ['nullable', 'integer', 'min:1', 'max:24'],
'is_active' => ['nullable', 'boolean'],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after:starts_at'],
'fallback_surface_key' => ['nullable', 'string', 'max:120', 'different:surface_key'],
'rules_json' => ['nullable', 'array'],
]);
}
private function validatePlacement(Request $request): array
{
return $request->validate([
'id' => ['nullable', 'integer', 'exists:collection_surface_placements,id'],
'surface_key' => ['required', 'string', 'max:120'],
'collection_id' => ['required', 'integer', 'exists:collections,id'],
'placement_type' => ['required', 'in:manual,campaign,scheduled_override'],
'priority' => ['nullable', 'integer', 'min:-100', 'max:100'],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after:starts_at'],
'is_active' => ['nullable', 'boolean'],
'campaign_key' => ['nullable', 'string', 'max:80'],
'notes' => ['nullable', 'string', 'max:1000'],
]);
}
private function validateBatchEditorial(Request $request): array
{
return $request->validate([
'collection_ids' => ['required', 'array', 'min:1', 'max:24'],
'collection_ids.*' => ['integer', 'distinct', 'exists:collections,id'],
'campaign_key' => ['nullable', 'string', 'max:80'],
'campaign_label' => ['nullable', 'string', 'max:120'],
'event_key' => ['nullable', 'string', 'max:80'],
'event_label' => ['nullable', 'string', 'max:120'],
'season_key' => ['nullable', 'string', 'max:80'],
'banner_text' => ['nullable', 'string', 'max:160'],
'badge_label' => ['nullable', 'string', 'max:80'],
'spotlight_style' => ['nullable', 'string', 'max:60'],
'editorial_notes' => ['nullable', 'string', 'max:4000'],
'surface_key' => ['nullable', 'string', 'max:120'],
'placement_type' => ['nullable', 'string', 'in:manual,campaign,scheduled_override'],
'priority' => ['nullable', 'integer', 'min:-100', 'max:100'],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after_or_equal:starts_at'],
'is_active' => ['nullable', 'boolean'],
'notes' => ['nullable', 'string', 'max:1000'],
'apply' => ['nullable', 'boolean'],
]);
}
private function mapDefinition(CollectionSurfaceDefinition $definition): array
{
return [
'id' => (int) $definition->id,
'surface_key' => $definition->surface_key,
'title' => $definition->title,
'description' => $definition->description,
'mode' => $definition->mode,
'ranking_mode' => $definition->ranking_mode,
'max_items' => (int) $definition->max_items,
'is_active' => (bool) $definition->is_active,
'starts_at' => $definition->starts_at?->toISOString(),
'ends_at' => $definition->ends_at?->toISOString(),
'fallback_surface_key' => $definition->fallback_surface_key,
'rules_json' => $definition->rules_json,
];
}
private function mapPlacement(CollectionSurfacePlacement $placement): array
{
return [
'id' => (int) $placement->id,
'surface_key' => $placement->surface_key,
'placement_type' => $placement->placement_type,
'priority' => (int) $placement->priority,
'starts_at' => $placement->starts_at?->toISOString(),
'ends_at' => $placement->ends_at?->toISOString(),
'is_active' => (bool) $placement->is_active,
'campaign_key' => $placement->campaign_key,
'notes' => $placement->notes,
'collection' => $placement->collection
? ($this->collections->mapCollectionCardPayloads(collect([$placement->collection]), true)[0] ?? null)
: null,
];
}
private function placementHistoryPayload(CollectionSurfacePlacement $placement): array
{
return [
'placement_id' => (int) $placement->id,
'surface_key' => (string) $placement->surface_key,
'collection_id' => (int) $placement->collection_id,
'placement_type' => (string) $placement->placement_type,
'priority' => (int) $placement->priority,
'starts_at' => $placement->starts_at?->toISOString(),
'ends_at' => $placement->ends_at?->toISOString(),
'is_active' => (bool) $placement->is_active,
'campaign_key' => $placement->campaign_key,
'notes' => $placement->notes,
];
}
}