User::count(), 'new_users_today' => User::whereDate('created_at', today())->count(), 'staff_count' => User::whereIn('role', ['admin', 'manager', 'editorial'])->count(), 'moderator_count' => User::where('role', 'moderator')->count(), ]; return Inertia::render('Admin/Dashboard', [ 'stats' => $stats, ]); } public function dailyActivity(Request $request, ReportTargetResolver $reportTargets): Response { $selectedDate = $this->resolveActivityDate($request); $periodStart = $selectedDate->copy()->startOfDay(); $periodEnd = $selectedDate->copy()->endOfDay(); $users = User::query() ->select('id', 'name', 'username', 'email', 'role', 'created_at') ->whereBetween('created_at', [$periodStart, $periodEnd]) ->orderByDesc('created_at') ->limit(25) ->get() ->map(fn (User $user): array => [ 'id' => (int) $user->id, 'name' => (string) $user->name, 'username' => $user->username, 'email' => (string) $user->email, 'role' => (string) $user->role, 'created_at' => optional($user->created_at)?->toISOString(), ]) ->values(); $artworks = Artwork::query() ->with('user:id,name,username') ->select('id', 'title', 'artwork_status', 'created_at', 'user_id', 'hash', 'thumb_ext') ->whereBetween('created_at', [$periodStart, $periodEnd]) ->orderByDesc('created_at') ->limit(25) ->get() ->map(fn (Artwork $artwork): array => [ 'id' => (int) $artwork->id, 'title' => (string) ($artwork->title ?? 'Untitled artwork'), 'status' => (string) ($artwork->artwork_status ?? 'unknown'), 'thumb' => $artwork->thumbUrl('sm') ?? null, 'created_at' => optional($artwork->created_at)?->toISOString(), 'user' => $artwork->user ? [ 'id' => (int) $artwork->user->id, 'name' => (string) $artwork->user->name, 'username' => $artwork->user->username, ] : null, ]) ->values(); $stories = Story::query() ->with('creator:id,name,username') ->select('id', 'title', 'status', 'created_at', 'published_at', 'creator_id') ->whereBetween('created_at', [$periodStart, $periodEnd]) ->orderByDesc('created_at') ->limit(25) ->get() ->map(fn (Story $story): array => [ 'id' => (int) $story->id, 'title' => (string) ($story->title ?? 'Untitled story'), 'status' => (string) ($story->status ?? 'draft'), 'created_at' => optional($story->created_at)?->toISOString(), 'published_at' => optional($story->published_at)?->toISOString(), 'creator' => $story->creator ? [ 'id' => (int) $story->creator->id, 'name' => (string) $story->creator->name, 'username' => $story->creator->username, ] : null, ]) ->values(); $uploads = Schema::hasTable('uploads') ? Upload::query() ->select('id', 'user_id', 'type', 'status', 'processing_state', 'title', 'created_at', 'moderation_status', 'moderated_at', 'moderated_by', 'moderation_note') ->where(function ($query) use ($periodStart, $periodEnd): void { $query->whereBetween('created_at', [$periodStart, $periodEnd]) ->orWhereBetween('moderated_at', [$periodStart, $periodEnd]); }) ->orderByDesc('created_at') ->limit(40) ->get() ->map(fn (Upload $upload): array => [ 'id' => (string) $upload->id, 'user_id' => $upload->user_id !== null ? (int) $upload->user_id : null, 'title' => (string) ($upload->title ?? 'Untitled upload'), 'type' => (string) ($upload->type ?? 'unknown'), 'status' => (string) ($upload->status ?? 'unknown'), 'processing_state' => (string) ($upload->processing_state ?? 'unknown'), 'moderation_status' => (string) ($upload->moderation_status ?? 'unknown'), 'created_at' => optional($upload->created_at)?->toISOString(), 'moderated_at' => optional($upload->moderated_at)?->toISOString(), 'moderated_by' => $upload->moderated_by !== null ? (int) $upload->moderated_by : null, 'moderation_note' => $upload->moderation_note, ]) ->values() : collect(); $reports = Schema::hasTable('reports') ? Report::query() ->with(['reporter:id,username', 'lastModeratedBy:id,username']) ->where(function ($query) use ($periodStart, $periodEnd): void { $query->whereBetween('created_at', [$periodStart, $periodEnd]) ->orWhereBetween('last_moderated_at', [$periodStart, $periodEnd]); }) ->orderByDesc('created_at') ->limit(30) ->get() ->map(fn (Report $report): array => [ 'id' => (int) $report->id, 'status' => (string) $report->status, 'reason' => (string) $report->reason, 'target_type' => (string) $report->target_type, 'target_id' => (int) $report->target_id, 'created_at' => optional($report->created_at)?->toISOString(), 'last_moderated_at' => optional($report->last_moderated_at)?->toISOString(), 'moderator_note' => $report->moderator_note, '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' => $reportTargets->summarize($report), ]) ->values() : collect(); $usernameRequests = Schema::hasTable('username_approval_requests') ? (function () use ($periodStart, $periodEnd) { $requestColumns = Schema::getColumnListing('username_approval_requests'); $selects = [ 'requests.id', 'requests.user_id', 'requests.requested_username', 'requests.status', 'requests.created_at', 'users.username as current_username', 'users.name as current_name', ]; if (in_array('context', $requestColumns, true)) { $selects[] = 'requests.context'; } if (in_array('similar_to', $requestColumns, true)) { $selects[] = 'requests.similar_to'; } if (in_array('review_note', $requestColumns, true)) { $selects[] = 'requests.review_note'; } if (in_array('reviewed_at', $requestColumns, true)) { $selects[] = 'requests.reviewed_at'; } $query = DB::table('username_approval_requests as requests') ->leftJoin('users', 'users.id', '=', 'requests.user_id') ->select($selects) ->where(function (Builder $query) use ($periodStart, $periodEnd, $requestColumns): void { $query->whereBetween('requests.created_at', [$periodStart, $periodEnd]); if (in_array('reviewed_at', $requestColumns, true)) { $query->orWhereBetween('requests.reviewed_at', [$periodStart, $periodEnd]); } }) ->orderByDesc('requests.created_at') ->limit(30); return $query ->get() ->map(fn ($row): array => [ 'id' => (int) $row->id, 'user_id' => $row->user_id !== null ? (int) $row->user_id : null, 'requested_username' => (string) $row->requested_username, 'status' => (string) ($row->status ?? 'pending'), 'context' => $row->context ?? null, 'similar_to' => $row->similar_to ?? null, 'reason' => $row->review_note ?? null, 'created_at' => $this->serializeDatabaseTimestamp($row->created_at), 'reviewed_at' => $this->serializeDatabaseTimestamp($row->reviewed_at ?? null), 'current_username' => $row->current_username, 'current_name' => $row->current_name, ]) ->values(); })() : collect(); $authEvents = Schema::hasTable('auth_audit_logs') ? AuthAuditLog::query() ->with('user:id,name,username,email,role') ->select('id', 'user_id', 'event_type', 'identifier', 'status', 'reason', 'ip', 'created_at') ->whereBetween('created_at', [$periodStart, $periodEnd]) ->orderByDesc('created_at') ->limit(30) ->get() ->map(fn (AuthAuditLog $log): array => [ 'id' => (int) $log->id, 'event_type' => (string) $log->event_type, 'identifier' => $log->identifier, 'status' => (string) $log->status, 'reason' => $log->reason, 'ip' => $log->ip, 'created_at' => optional($log->created_at)?->toISOString(), 'user' => $log->user ? [ 'id' => (int) $log->user->id, 'name' => (string) $log->user->name, 'username' => $log->user->username, 'email' => (string) $log->user->email, 'role' => (string) $log->user->role, ] : null, ]) ->values() : collect(); return Inertia::render('Admin/DailyActivity', [ 'selectedDate' => $selectedDate->toDateString(), 'summary' => [ 'new_users' => $users->count(), 'new_artworks' => $artworks->count(), 'new_stories' => $stories->count(), 'upload_events' => $uploads->count(), 'report_events' => $reports->count(), 'username_events' => $usernameRequests->count(), 'auth_events' => $authEvents->count(), 'moderated_uploads' => $uploads->filter(fn (array $upload): bool => ! empty($upload['moderated_at']))->count(), 'moderated_reports' => $reports->filter(fn (array $report): bool => ! empty($report['last_moderated_at']))->count(), ], 'queues' => [ 'pending_uploads' => Schema::hasTable('uploads') ? Upload::query()->where('status', 'draft')->where('moderation_status', 'pending')->count() : 0, 'open_reports' => Schema::hasTable('reports') ? Report::query()->where('status', 'open')->count() : 0, 'pending_username_requests' => Schema::hasTable('username_approval_requests') ? DB::table('username_approval_requests')->where('status', 'pending')->count() : 0, ], 'sections' => [ 'users' => $users, 'artworks' => $artworks, 'stories' => $stories, 'uploads' => $uploads, 'reports' => $reports, 'username_requests' => $usernameRequests, 'auth_events' => $authEvents, ], ]); } // ── Users ───────────────────────────────────────────────────────────────── public function users(Request $request): Response { $search = $request->string('search')->trim()->toString(); $roleFilter = $request->string('role')->trim()->toString(); $query = User::select('id', 'name', 'username', 'email', 'role', 'created_at', 'is_active') ->orderByDesc('created_at'); if ($search !== '') { $query->where(function ($q) use ($search): void { $q->where('name', 'like', "%{$search}%") ->orWhere('username', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%"); }); } if ($roleFilter !== '' && $roleFilter !== 'all') { $query->where('role', $roleFilter); } $users = $query->paginate(50)->withQueryString(); return Inertia::render('Admin/Users/Index', [ 'users' => $users, 'filters' => ['search' => $search, 'role' => $roleFilter], 'roles' => collect(UserRole::cases())->map(fn ($r) => [ 'value' => $r->value, 'label' => $r->label(), 'badge' => $r->badgeClass(), ]), ]); } // ── Promote / Demote ────────────────────────────────────────────────────── public function updateRole(Request $request, User $user): RedirectResponse { $request->validate([ 'role' => ['required', 'string', 'in:' . implode(',', array_column(UserRole::cases(), 'value'))], ]); /** @var \App\Models\User $actor */ $actor = $request->user(); // Only admins can set the 'admin' role. if ($request->input('role') === UserRole::Admin->value && ! $actor->isAdmin()) { abort(403, 'Only admins can grant the Admin role.'); } // Prevent self-demotion. if ($actor->id === $user->id) { return back()->with('error', 'You cannot change your own role.'); } $user->update(['role' => $request->input('role')]); return back()->with('success', "Role updated to \"{$request->input('role')}\" for {$user->name}."); } // ── Stories ─────────────────────────────────────────────────────────────── public function stories(Request $request): Response { $stories = Story::with('creator:id,name,username') ->select('id', 'title', 'status', 'published_at', 'creator_id') ->orderByDesc('created_at') ->paginate(50) ->withQueryString(); return Inertia::render('Admin/Stories', [ 'stories' => $stories, ]); } // ── Artworks ────────────────────────────────────────────────────────────── public function artworks(Request $request): Response { $artworks = Artwork::with('user:id,name,username') ->select('id', 'title', 'artwork_status', 'created_at', 'user_id', 'hash', 'thumb_ext') ->orderByDesc('created_at') ->paginate(50) ->withQueryString(); // Normalise status field and add thumb URL $artworks->getCollection()->transform(function ($artwork) { return [ 'id' => $artwork->id, 'title' => $artwork->title, 'status' => $artwork->artwork_status, 'thumb' => $artwork->thumbUrl('sm') ?? null, 'created_at' => $artwork->created_at, 'user' => $artwork->user, ]; }); return Inertia::render('Admin/Artworks', [ 'artworks' => $artworks, ]); } // ── Username Queue ──────────────────────────────────────────────────────── public function usernameQueue(): Response { return Inertia::render('Admin/UsernameQueue'); } // ── Upload Queue ────────────────────────────────────────────────────────── public function uploadQueue(): Response { return Inertia::render('Admin/UploadQueue'); } // ── Settings ────────────────────────────────────────────────────────────── public function settings(): Response { return Inertia::render('Admin/Settings', [ 'settings' => [], ]); } public function authAudit(Request $request): Response { abort_unless($request->user()?->isAdmin(), 403, 'Only admins can access this area.'); $search = $request->string('search')->trim()->toString(); $eventType = $request->string('event')->trim()->toString(); $status = $request->string('status')->trim()->toString(); $query = AuthAuditLog::query() ->with('user:id,name,username,email,role') ->latest('created_at') ->latest('id'); if ($search !== '') { $query->where(function ($builder) use ($search): void { $builder ->where('identifier', 'like', "%{$search}%") ->orWhere('ip', 'like', "%{$search}%") ->orWhere('reason', 'like', "%{$search}%") ->orWhereHas('user', function ($userQuery) use ($search): void { $userQuery ->where('name', 'like', "%{$search}%") ->orWhere('username', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%"); }); }); } if ($eventType !== '' && $eventType !== 'all') { $query->where('event_type', $eventType); } if ($status !== '' && $status !== 'all') { $query->where('status', $status); } $logs = $query->paginate(50)->withQueryString()->through(function (AuthAuditLog $log): array { return [ 'id' => $log->id, 'event_type' => $log->event_type, 'identifier' => $log->identifier, 'status' => $log->status, 'reason' => $log->reason, 'ip' => $log->ip, 'user_agent' => $log->user_agent, 'metadata' => $log->metadata ?? [], 'created_at' => $log->created_at, 'user' => $log->user ? [ 'id' => $log->user->id, 'name' => $log->user->name, 'username' => $log->user->username, 'email' => $log->user->email, 'role' => $log->user->role, ] : null, ]; }); return Inertia::render('Admin/AuthAudit', [ 'logs' => $logs, 'filters' => [ 'search' => $search, 'event' => $eventType, 'status' => $status, ], 'eventOptions' => [ ['value' => 'all', 'label' => 'All events'], ['value' => 'login', 'label' => 'Login'], ['value' => 'register', 'label' => 'Register'], ['value' => 'forgot_password', 'label' => 'Forgot password'], ['value' => 'reset_password', 'label' => 'Reset password'], ], 'statusOptions' => [ ['value' => 'all', 'label' => 'All statuses'], ['value' => 'success', 'label' => 'Success'], ['value' => 'failed', 'label' => 'Failed'], ], ]); } private function resolveActivityDate(Request $request): Carbon { $date = $request->string('date')->trim()->toString(); if ($date === '') { return today(); } try { return Carbon::createFromFormat('Y-m-d', $date)->startOfDay(); } catch (\Throwable) { return today(); } } private function serializeDatabaseTimestamp(mixed $value): ?string { if ($value === null || $value === '') { return null; } if ($value instanceof Carbon) { return $value->toISOString(); } try { return Carbon::parse((string) $value)->toISOString(); } catch (\Throwable) { return null; } } }