@extends('layouts.app') @section('title', 'Admin Dashboard') @section('breadcrumb') Home Dashboard @endsection @section('content') @php $u = auth()->user(); $activePct = ($stats['total_candidates'] ?? 0) > 0 ? round(($stats['active_candidates'] / $stats['total_candidates']) * 100) : 0; $completionPct = ($stats['total_exams'] ?? 0) > 0 ? round(($stats['completed_exams'] / $stats['total_exams']) * 100) : 0; @endphp
{{ now()->format('l, F j, Y') }} {{ $stats['pending_profiles'] }} pending reviews

Welcome back, {{ $u->name }}

Management overview of candidates, applications, and examinations.

{{ $stats['total_candidates'] }}
Candidates
{{ $stats['total_applications'] }}
Applications
{{ $stats['active_posts'] }} / {{ $stats['total_posts'] }}
Open posts
{{ $completionPct }}%
Exam completion
{{-- Candidates → candidates listing --}}
View all candidates
{{ $activePct }}% active
Candidates
{{ $stats['total_candidates'] }}
{{-- Pending profiles → candidate profiles review queue --}}
Review pending profiles
REVIEW
Pending profiles
{{ $stats['pending_profiles'] }}
{{-- Applications → applications listing --}}
View all applications
{{ $stats['approved_applications'] }}
Applications
{{ $stats['total_applications'] }}
{{-- Security events → exam-security-logs index --}}
View security events
{{ $stats['force_submitted_exams'] }}
Security events
{{ $stats['security_events'] }}
{{-- Conversion funnel: registered → … → exam completed. Each stage uses DISTINCT user_id off the same candidate population so the waterfall reads correctly (a candidate counted at stage N is by definition counted at all earlier stages). --}}
Conversion funnel
Candidates progressing through each stage of the recruitment pipeline
@php $funnelTop = max(1, max($funnel ?? [0])); @endphp @if(($funnel['Registered'] ?? 0) === 0)
No candidates yet — funnel will populate as registrations come in.
@else
@php $funnelStart = $funnel['Registered'] ?? 1; @endphp @foreach($funnel as $stage => $count) @php $widthPct = $funnelTop > 0 ? round(($count / $funnelTop) * 100) : 0; // Conversion vs. the previous stage. Skips the first stage because // there's no prior to compare against. Capped at 100% (the data is // monotonically decreasing, but we belt-and-brace against bad rows). $prevCount = $loop->first ? null : $previousCount; $stagePct = $prevCount && $prevCount > 0 ? min(100, round(($count / $prevCount) * 100)) : null; $previousCount = $count; @endphp
{{ $stage }} @if($stagePct !== null) {{ $stagePct }}% from prev @endif
{{ number_format($count) }}
@endforeach
Top of funnel: {{ number_format($funnelStart) }} registered · Bottom of funnel: @php $last = end($funnel); $endToEndPct = $funnelStart > 0 ? round(($last / $funnelStart) * 100, 1) : 0; @endphp {{ number_format($last) }} ({{ $endToEndPct }}% end-to-end)
@endif
Applications by post
Top {{ min(8, $applicationsByPost->count()) }} posts by volume
@if($applicationsByPost->isEmpty())
No application data yet.
@else {{-- Skeleton sits behind the canvas until Chart.js paints; the init script below adds .is-loaded to fade it out. --}}
@endif
Exam status
Current distribution
@if($examStatusCounts->isEmpty())
No exam attempts yet.
@else
@endif
Latest applications
Most recent submissions
View all →
@forelse($latestApplications as $app) @php $status = strtolower($app->status_label ?? $app->status ?? ''); $pillTone = str_contains($status, 'approv') ? 's-success' : (str_contains($status, 'reject') ? 's-danger' : 's-warning'); @endphp
{{ $app->post?->code }} {{ $app->user?->name }}
{{ Str::limit($app->post?->title, 40) }} · {{ $app->created_at->diffForHumans() }}
{{ $app->status_label ?? ucfirst($app->status) }}
@empty
No applications yet.
@endforelse
Latest exam attempts
Most recent sessions
View all →
@forelse($latestExamAttempts as $attempt) @php $status = strtolower($attempt->status_label ?? $attempt->status ?? ''); $pillTone = str_contains($status, 'complet') ? 's-success' : (str_contains($status, 'progress') ? 's-info' : (str_contains($status, 'review') ? 's-warning' : 's-neutral')); @endphp
{{ $attempt->post?->code }} {{ $attempt->user?->name }}
@if($attempt->status === 'completed') Score {{ $attempt->score }}/{{ $attempt->total_questions }} @else {{ $attempt->started_at?->diffForHumans() ?? '—' }} @endif
{{ $attempt->status_label ?? ucfirst(str_replace('_', ' ', $attempt->status)) }}
@empty
No exam attempts yet.
@endforelse
Quick actions
Jump to common tasks
@php $quickActions = [ ['route' => 'admin.candidates.index', 'label' => 'Candidate profiles', 'icon' => '', 'badge' => $stats['pending_profiles']], ['route' => 'admin.posts.index', 'label' => 'Posts', 'icon' => '', 'badge' => null], ['route' => 'admin.applications.index', 'label' => 'Applications', 'icon' => '', 'badge' => $stats['pending_applications']], ['route' => 'admin.questions.index', 'label' => 'Question bank', 'icon' => '', 'badge' => $stats['active_questions'] < 100 ? $stats['active_questions'] : null], ['route' => 'admin.merit.index', 'label' => 'Merit list', 'icon' => '', 'badge' => null], ['route' => 'admin.exam-security-logs.index', 'label' => 'Security logs', 'icon' => '', 'badge' => $stats['security_events'] > 0 ? $stats['security_events'] : null], ]; @endphp @foreach($quickActions as $a) {!! $a['icon'] !!} {{ $a['label'] }} @if($a['badge'] !== null && $a['badge'] > 0) {{ $a['badge'] }} @endif @endforeach
@endsection @push('scripts') @endpush