optimizations
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Collection;
|
||||
use App\Models\CollectionMember;
|
||||
use App\Services\CollectionCollaborationService;
|
||||
use App\Services\CollectionModerationService;
|
||||
use App\Services\CollectionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class CollectionModerationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionModerationService $moderation,
|
||||
private readonly CollectionService $collections,
|
||||
private readonly CollectionCollaborationService $collaborators,
|
||||
) {
|
||||
}
|
||||
|
||||
public function updateModeration(Request $request, Collection $collection): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'moderation_status' => ['required', 'in:active,under_review,restricted,hidden'],
|
||||
]);
|
||||
|
||||
$collection = $this->moderation->updateStatus($collection->loadMissing('user'), (string) $data['moderation_status']);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true),
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateInteractions(Request $request, Collection $collection): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'allow_comments' => ['sometimes', 'boolean'],
|
||||
'allow_submissions' => ['sometimes', 'boolean'],
|
||||
'allow_saves' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$collection = $this->moderation->updateInteractions($collection->loadMissing('user'), $data);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true),
|
||||
]);
|
||||
}
|
||||
|
||||
public function unfeature(Request $request, Collection $collection): JsonResponse
|
||||
{
|
||||
$collection = $this->moderation->unfeature($collection->loadMissing('user'));
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'collection' => $this->collections->mapCollectionDetailPayload($collection->loadMissing('user'), true),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroyMember(Request $request, Collection $collection, CollectionMember $member): JsonResponse
|
||||
{
|
||||
$this->moderation->removeMember($collection, $member);
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'collection' => $this->collections->mapCollectionDetailPayload($collection->fresh()->loadMissing('user'), true),
|
||||
'members' => $this->collaborators->mapMembers($collection->fresh(), $request->user()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Analytics\DiscoveryFeedbackReportService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class DiscoveryFeedbackReportController extends Controller
|
||||
{
|
||||
public function __construct(private readonly DiscoveryFeedbackReportService $reportService) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'from' => ['nullable', 'date_format:Y-m-d'],
|
||||
'to' => ['nullable', 'date_format:Y-m-d'],
|
||||
'limit' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
]);
|
||||
|
||||
$from = (string) ($validated['from'] ?? now()->subDays(13)->toDateString());
|
||||
$to = (string) ($validated['to'] ?? now()->toDateString());
|
||||
$limit = (int) ($validated['limit'] ?? 20);
|
||||
|
||||
if ($from > $to) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid date range: from must be before or equal to to.',
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$report = $this->reportService->buildReport($from, $to, $limit);
|
||||
|
||||
return response()->json([
|
||||
'meta' => [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'limit' => $limit,
|
||||
'generated_at' => now()->toISOString(),
|
||||
],
|
||||
'overview' => $report['overview'],
|
||||
'daily_feedback' => $report['daily_feedback'],
|
||||
'trend_summary' => $report['trend_summary'],
|
||||
'by_surface' => $report['by_surface'],
|
||||
'by_algo_surface' => $report['by_algo_surface'],
|
||||
'top_artworks' => $report['top_artworks'],
|
||||
'latest_aggregated_date' => $report['latest_aggregated_date'],
|
||||
], Response::HTTP_OK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Recommendations\RecommendationFeedResolver;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
final class FeedEngineDecisionController extends Controller
|
||||
{
|
||||
public function __construct(private readonly RecommendationFeedResolver $feedResolver) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'user_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'algo_version' => ['nullable', 'string', 'max:64'],
|
||||
]);
|
||||
|
||||
$userId = (int) $validated['user_id'];
|
||||
$algoVersion = isset($validated['algo_version']) ? (string) $validated['algo_version'] : null;
|
||||
|
||||
return response()->json([
|
||||
'meta' => [
|
||||
'generated_at' => now()->toISOString(),
|
||||
],
|
||||
'decision' => $this->feedResolver->inspectDecision($userId, $algoVersion),
|
||||
], Response::HTTP_OK);
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,231 @@
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\Report;
|
||||
use App\Models\ReportHistory;
|
||||
use App\Services\NovaCards\NovaCardPublishModerationService;
|
||||
use App\Support\Moderation\ReportTargetResolver;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ModerationReportQueueController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ReportTargetResolver $targets,
|
||||
private readonly NovaCardPublishModerationService $moderation,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$status = (string) $request->query('status', 'open');
|
||||
$status = in_array($status, ['open', 'reviewing', 'closed'], true) ? $status : 'open';
|
||||
$group = (string) $request->query('group', '');
|
||||
|
||||
$items = Report::query()
|
||||
->with('reporter:id,username')
|
||||
$query = Report::query()
|
||||
->with(['reporter:id,username', 'lastModeratedBy:id,username', 'historyEntries.actor:id,username'])
|
||||
->where('status', $status)
|
||||
->orderByDesc('id')
|
||||
->paginate(30);
|
||||
->orderByDesc('id');
|
||||
|
||||
return response()->json($items);
|
||||
if ($group === 'nova_cards') {
|
||||
$query->whereIn('target_type', $this->targets->novaCardTargetTypes());
|
||||
}
|
||||
|
||||
$items = $query->paginate(30);
|
||||
|
||||
return response()->json([
|
||||
'data' => collect($items->items())
|
||||
->map(fn (Report $report): array => $this->serializeReport($report))
|
||||
->values()
|
||||
->all(),
|
||||
'meta' => [
|
||||
'current_page' => $items->currentPage(),
|
||||
'last_page' => $items->lastPage(),
|
||||
'per_page' => $items->perPage(),
|
||||
'total' => $items->total(),
|
||||
'from' => $items->firstItem(),
|
||||
'to' => $items->lastItem(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, Report $report): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'status' => 'sometimes|in:open,reviewing,closed',
|
||||
'moderator_note' => 'sometimes|nullable|string|max:2000',
|
||||
]);
|
||||
|
||||
$before = [];
|
||||
$after = [];
|
||||
$user = $request->user();
|
||||
|
||||
DB::transaction(function () use ($data, $report, $user, &$before, &$after): void {
|
||||
if (array_key_exists('status', $data) && $data['status'] !== $report->status) {
|
||||
$before['status'] = (string) $report->status;
|
||||
$after['status'] = (string) $data['status'];
|
||||
$report->status = $data['status'];
|
||||
}
|
||||
|
||||
if (array_key_exists('moderator_note', $data)) {
|
||||
$normalizedNote = is_string($data['moderator_note']) ? trim($data['moderator_note']) : null;
|
||||
$normalizedNote = $normalizedNote !== '' ? $normalizedNote : null;
|
||||
|
||||
if ($normalizedNote !== $report->moderator_note) {
|
||||
$before['moderator_note'] = $report->moderator_note;
|
||||
$after['moderator_note'] = $normalizedNote;
|
||||
$report->moderator_note = $normalizedNote;
|
||||
}
|
||||
}
|
||||
|
||||
if ($before !== [] || $after !== []) {
|
||||
$report->last_moderated_by_id = $user?->id;
|
||||
$report->last_moderated_at = now();
|
||||
$report->save();
|
||||
|
||||
$report->historyEntries()->create([
|
||||
'actor_user_id' => $user?->id,
|
||||
'action_type' => 'report_updated',
|
||||
'summary' => $this->buildUpdateSummary($before, $after),
|
||||
'note' => $report->moderator_note,
|
||||
'before_json' => $before !== [] ? $before : null,
|
||||
'after_json' => $after !== [] ? $after : null,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$report = $report->fresh(['reporter:id,username', 'lastModeratedBy:id,username', 'historyEntries.actor:id,username']);
|
||||
|
||||
return response()->json([
|
||||
'report' => $this->serializeReport($report),
|
||||
]);
|
||||
}
|
||||
|
||||
public function moderateTarget(Request $request, Report $report): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'action' => 'required|in:approve_card,flag_card,reject_card',
|
||||
'disposition' => 'nullable|in:' . implode(',', array_keys(NovaCardPublishModerationService::DISPOSITION_LABELS)),
|
||||
]);
|
||||
|
||||
$card = $this->targets->resolveModerationCard($report);
|
||||
abort_unless($card !== null, 422, 'This report does not have a Nova Card moderation target.');
|
||||
|
||||
DB::transaction(function () use ($card, $data, $report, $request): void {
|
||||
$before = [
|
||||
'card_id' => (int) $card->id,
|
||||
'moderation_status' => (string) $card->moderation_status,
|
||||
];
|
||||
|
||||
$nextStatus = match ($data['action']) {
|
||||
'approve_card' => NovaCard::MOD_APPROVED,
|
||||
'flag_card' => NovaCard::MOD_FLAGGED,
|
||||
'reject_card' => NovaCard::MOD_REJECTED,
|
||||
};
|
||||
$card = $this->moderation->recordStaffOverride(
|
||||
$card,
|
||||
$nextStatus,
|
||||
$request->user(),
|
||||
'report_queue',
|
||||
[
|
||||
'note' => $report->moderator_note,
|
||||
'report_id' => $report->id,
|
||||
'disposition' => $data['disposition'] ?? null,
|
||||
],
|
||||
);
|
||||
|
||||
$report->last_moderated_by_id = $request->user()?->id;
|
||||
$report->last_moderated_at = now();
|
||||
$report->save();
|
||||
|
||||
$report->historyEntries()->create([
|
||||
'actor_user_id' => $request->user()?->id,
|
||||
'action_type' => 'target_moderated',
|
||||
'summary' => $this->buildTargetModerationSummary($data['action'], $card),
|
||||
'note' => $report->moderator_note,
|
||||
'before_json' => $before,
|
||||
'after_json' => [
|
||||
'card_id' => (int) $card->id,
|
||||
'moderation_status' => (string) $card->moderation_status,
|
||||
'action' => (string) $data['action'],
|
||||
],
|
||||
'created_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
$report = $report->fresh(['reporter:id,username', 'lastModeratedBy:id,username', 'historyEntries.actor:id,username']);
|
||||
|
||||
return response()->json([
|
||||
'report' => $this->serializeReport($report),
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildUpdateSummary(array $before, array $after): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (array_key_exists('status', $after)) {
|
||||
$parts[] = sprintf('Status %s -> %s', $before['status'], $after['status']);
|
||||
}
|
||||
|
||||
if (array_key_exists('moderator_note', $after)) {
|
||||
$parts[] = $after['moderator_note'] ? 'Moderator note updated' : 'Moderator note cleared';
|
||||
}
|
||||
|
||||
return $parts !== [] ? implode(' • ', $parts) : 'Report reviewed';
|
||||
}
|
||||
|
||||
private function buildTargetModerationSummary(string $action, NovaCard $card): string
|
||||
{
|
||||
return match ($action) {
|
||||
'approve_card' => sprintf('Approved card #%d', $card->id),
|
||||
'flag_card' => sprintf('Flagged card #%d', $card->id),
|
||||
'reject_card' => sprintf('Rejected card #%d', $card->id),
|
||||
default => sprintf('Updated card #%d', $card->id),
|
||||
};
|
||||
}
|
||||
|
||||
private function serializeReport(Report $report): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $report->id,
|
||||
'status' => (string) $report->status,
|
||||
'target_type' => (string) $report->target_type,
|
||||
'target_id' => (int) $report->target_id,
|
||||
'reason' => (string) $report->reason,
|
||||
'details' => $report->details,
|
||||
'moderator_note' => $report->moderator_note,
|
||||
'created_at' => optional($report->created_at)?->toISOString(),
|
||||
'updated_at' => optional($report->updated_at)?->toISOString(),
|
||||
'last_moderated_at' => optional($report->last_moderated_at)?->toISOString(),
|
||||
'reporter' => $report->reporter ? [
|
||||
'id' => (int) $report->reporter->id,
|
||||
'username' => (string) $report->reporter->username,
|
||||
] : null,
|
||||
'last_moderated_by' => $report->lastModeratedBy ? [
|
||||
'id' => (int) $report->lastModeratedBy->id,
|
||||
'username' => (string) $report->lastModeratedBy->username,
|
||||
] : null,
|
||||
'target' => $this->targets->summarize($report),
|
||||
'history' => $report->historyEntries
|
||||
->take(8)
|
||||
->map(fn (ReportHistory $entry): array => [
|
||||
'id' => (int) $entry->id,
|
||||
'action_type' => (string) $entry->action_type,
|
||||
'summary' => $entry->summary,
|
||||
'note' => $entry->note,
|
||||
'before' => $entry->before_json,
|
||||
'after' => $entry->after_json,
|
||||
'created_at' => optional($entry->created_at)?->toISOString(),
|
||||
'actor' => $entry->actor ? [
|
||||
'id' => (int) $entry->actor->id,
|
||||
'username' => (string) $entry->actor->username,
|
||||
] : null,
|
||||
])
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user