query('status', 'open'); $status = in_array($status, ['open', 'reviewing', 'closed'], true) ? $status : 'open'; $group = (string) $request->query('group', ''); $query = Report::query() ->with(['reporter:id,username', 'lastModeratedBy:id,username', 'historyEntries.actor:id,username']) ->where('status', $status) ->orderByDesc('id'); 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(), ]; } }