From e5b47a36266ea38c6302126543a182c2289b4f86 Mon Sep 17 00:00:00 2001 From: novysan87 Date: Mon, 11 May 2026 14:51:30 +0700 Subject: [PATCH] Improve public controls and dashboard student statistics --- .../Controllers/BrandingSettingController.php | 2 + app/Http/Controllers/DashboardController.php | 33 ++++- app/Models/AppSetting.php | 1 + resources/views/dashboard.blade.php | 99 +++++-------- .../views/public/student-lookup.blade.php | 136 ++++++++++-------- resources/views/settings/branding.blade.php | 15 ++ tests/Feature/StudentPromotionTest.php | 118 +++++++++++++++ 7 files changed, 278 insertions(+), 126 deletions(-) diff --git a/app/Http/Controllers/BrandingSettingController.php b/app/Http/Controllers/BrandingSettingController.php index a8f789f..5121a21 100644 --- a/app/Http/Controllers/BrandingSettingController.php +++ b/app/Http/Controllers/BrandingSettingController.php @@ -29,10 +29,12 @@ class BrandingSettingController extends Controller 'remove_app_logo' => ['nullable', 'boolean'], 'remove_app_favicon' => ['nullable', 'boolean'], 'remove_public_portal_logo' => ['nullable', 'boolean'], + 'public_show_student_documents' => ['nullable', 'boolean'], ]); AppSetting::setValue('app_name', $data['app_name']); AppSetting::setValue('app_short_name', $data['app_short_name']); + AppSetting::setValue('public_show_student_documents', $request->boolean('public_show_student_documents') ? '1' : '0'); $this->handleAssetUpdate($request, 'app_logo', 'branding/app-logo', (bool) ($data['remove_app_logo'] ?? false)); $this->handleAssetUpdate($request, 'app_favicon', 'branding/app-favicon', (bool) ($data['remove_app_favicon'] ?? false)); diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 3943f00..e098ecc 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -7,6 +7,7 @@ use App\Models\Department; use App\Models\DocumentType; use App\Models\SchoolClass; use App\Models\Student; +use Illuminate\Database\Eloquent\Builder; use App\Models\StudentAchievement; use App\Models\StudentExit; use App\Models\StudentTransfer; @@ -21,7 +22,9 @@ class DashboardController extends Controller ->where('is_required', true) ->pluck('id'); - $students = Student::with('documents')->get(); + $students = Student::with('documents') + ->where('status', 'active') + ->get(); $completion = $this->calculateCompletion($students, $requiredDocumentTypeIds); @@ -42,24 +45,46 @@ class DashboardController extends Controller ->filter(fn (array $item) => $item['total'] > 0) ->values(); + $activeAcademicYearId = AcademicYear::query()->where('is_active', true)->value('id'); + + $studentStatisticsByClass = SchoolClass::query() + ->with(['academicYear', 'department']) + ->when($activeAcademicYearId, fn (Builder $query) => $query->where('academic_year_id', $activeAcademicYearId)) + ->withCount([ + 'students as active_students_count' => fn (Builder $query) => $query->where('status', 'active'), + 'students as male_students_count' => fn (Builder $query) => $query->where('status', 'active')->where('gender', 'L'), + 'students as female_students_count' => fn (Builder $query) => $query->where('status', 'active')->where('gender', 'P'), + ]) + ->orderBy('level') + ->orderBy('name') + ->get() + ->filter(fn (SchoolClass $classRoom) => $classRoom->active_students_count > 0) + ->values(); + return view('dashboard', [ 'stats' => [ 'academicYears' => AcademicYear::count(), 'departments' => Department::count(), 'classes' => SchoolClass::count(), 'documentTypes' => DocumentType::count(), - 'students' => Student::count(), + 'students' => Student::where('status', 'active')->count(), 'studentsDropped' => StudentExit::count(), 'studentsTransferred' => StudentTransfer::count(), 'studentsGraduated' => Student::where('status', 'graduated')->count(), - 'studentAchievements' => StudentAchievement::count(), - 'studentsWithAchievements' => StudentAchievement::distinct('student_id')->count('student_id'), + 'studentAchievements' => StudentAchievement::query() + ->whereHas('student', fn (Builder $query) => $query->where('status', 'active')) + ->count(), + 'studentsWithAchievements' => StudentAchievement::query() + ->whereHas('student', fn (Builder $query) => $query->where('status', 'active')) + ->distinct('student_id') + ->count('student_id'), 'studentsCompleteDocuments' => $completion['complete'], 'studentsIncompleteDocuments' => $completion['incomplete'], 'studentsVerifiedDocuments' => $completion['verified_complete'], 'studentsUnverifiedDocuments' => $completion['verified_incomplete'], ], 'studentGenderByAcademicYear' => $studentGenderByAcademicYear, + 'studentStatisticsByClass' => $studentStatisticsByClass, ]); } diff --git a/app/Models/AppSetting.php b/app/Models/AppSetting.php index 1f1b6af..2592e5f 100644 --- a/app/Models/AppSetting.php +++ b/app/Models/AppSetting.php @@ -35,6 +35,7 @@ class AppSetting extends Model 'app_logo' => null, 'app_favicon' => null, 'public_portal_logo' => null, + 'public_show_student_documents' => '1', ]; } diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index fb6fbad..a9596e2 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -260,71 +260,44 @@ - -
-
-
- - - - Auth & Login + +
+ @forelse ($studentStatisticsByClass as $classStat) +
+
+
+ + + +
+
{{ $classStat->name }}
+
{{ $classStat->academicYear?->name ?: '-' }} · {{ $classStat->department?->name ?: '-' }}
+
+
+
Total {{ $classStat->active_students_count }}
+
+
+
+
+ + Laki-laki +
+
{{ $classStat->male_students_count }}
+
+
+
+ + Perempuan +
+
{{ $classStat->female_students_count }}
+
+
- Aktif -
-
-
- - - - Master Data + @empty +
+ Belum ada data siswa aktif yang bisa direkap per kelas.
- Aktif -
-
-
- - - - Data Siswa -
- Aktif -
-
-
- - - - Dokumen & Verifikasi -
- Aktif -
-
-
- - - - Histori Akademik -
- Siap -
-
-
- - - - Promosi Siswa -
- Siap -
-
-
- - - - Siswa Berprestasi -
- Siap -
+ @endforelse
diff --git a/resources/views/public/student-lookup.blade.php b/resources/views/public/student-lookup.blade.php index 44d46be..18588c9 100644 --- a/resources/views/public/student-lookup.blade.php +++ b/resources/views/public/student-lookup.blade.php @@ -11,6 +11,15 @@ return sprintf('%04d-%s-%06d', $achievement->achievement_year ?? 0, optional($achievement->achievement_date)->format('Y-m-d') ?? '0000-00-00', $achievement->id); }) : collect(); + $showPublicStudentDocuments = (\App\Models\AppSetting::getValue('public_show_student_documents', '1') === '1'); + $publicStatusLabel = match ($student?->status) { + 'transferred' => 'Pindah Sekolah', + 'dropped' => 'Keluar Sekolah', + 'graduated' => 'Lulus', + 'inactive' => 'Nonaktif', + 'active' => 'Aktif', + default => $student?->status ? ucfirst($student->status) : '-', + }; @endphp @@ -234,7 +243,14 @@
Data ditemukan
-

{{ $student->full_name }}

+
+

{{ $student->full_name }}

+
+ Wali Kelas + • + {{ $student->classRoom?->homeroom_teacher ?: '-' }} +
+

Pencarian cocok berdasarkan {{ strtolower($matchedBy ?? 'data siswa') }} dan menampilkan ringkasan informasi siswa yang tersedia untuk publik.

@@ -271,7 +287,7 @@
Status
-
{{ ucfirst($student->status) }}
+
{{ $publicStatusLabel }}
@@ -332,7 +348,7 @@
{{ $student->classRoom?->name ?: '-' }}
Status
-
{{ ucfirst($student->status) }}
+
{{ $publicStatusLabel }}
Masuk
{{ $student->entry_date?->format('d-m-Y') ?: '-' }}
@@ -416,69 +432,71 @@ @endif -
-
-
-

Dokumen Kelengkapan Siswa

-

Informasi kelengkapan ditampilkan berdasarkan dokumen yang sudah diunggah siswa.

+ @if ($showPublicStudentDocuments) +
+
+
+

Dokumen Kelengkapan Siswa

+

Informasi kelengkapan ditampilkan berdasarkan dokumen yang sudah diunggah siswa.

+
+
+ Informasi publik terbatas +
-
- Informasi publik terbatas -
-
- @if ($requiredDocumentSummary['missing_items']->isNotEmpty() || $requiredDocumentSummary['unverified_items']->isNotEmpty()) -
- @foreach ($requiredDocumentSummary['missing_items'] as $missingDocument) - Belum upload: {{ $missingDocument }} - @endforeach - @foreach ($requiredDocumentSummary['unverified_items'] as $unverifiedDocument) - Belum valid: {{ $unverifiedDocument }} - @endforeach -
- @else -
- Seluruh dokumen wajib siswa ini sudah lengkap dan tervalidasi penuh. -
- @endif + @if ($requiredDocumentSummary['missing_items']->isNotEmpty() || $requiredDocumentSummary['unverified_items']->isNotEmpty()) +
+ @foreach ($requiredDocumentSummary['missing_items'] as $missingDocument) + Belum upload: {{ $missingDocument }} + @endforeach + @foreach ($requiredDocumentSummary['unverified_items'] as $unverifiedDocument) + Belum valid: {{ $unverifiedDocument }} + @endforeach +
+ @else +
+ Seluruh dokumen wajib siswa ini sudah lengkap dan tervalidasi penuh. +
+ @endif -
- @forelse ($student->documents->sortBy('documentType.name') as $document) - @php - $verificationMap = [ - 'pending' => 'bg-slate-100 text-slate-600', - 'valid' => 'bg-emerald-100 text-emerald-700', - 'invalid' => 'bg-rose-100 text-rose-700', - 'revision' => 'bg-amber-100 text-amber-700', - ]; - @endphp -
-
-
-
{{ $document->documentType?->name ?: '-' }}
-
{{ $document->file_name }}
-
- - {{ ucfirst($document->verification_status) }} - - - {{ strtoupper($document->file_extension ?: '-') }} - +
+ @forelse ($student->documents->sortBy('documentType.name') as $document) + @php + $verificationMap = [ + 'pending' => 'bg-slate-100 text-slate-600', + 'valid' => 'bg-emerald-100 text-emerald-700', + 'invalid' => 'bg-rose-100 text-rose-700', + 'revision' => 'bg-amber-100 text-amber-700', + ]; + @endphp +
+
+
+
{{ $document->documentType?->name ?: '-' }}
+
{{ $document->file_name }}
+
+ + {{ ucfirst($document->verification_status) }} + + + {{ strtoupper($document->file_extension ?: '-') }} + +
+
+
-
-
- @empty -
- Belum ada dokumen siswa yang diunggah. -
- @endforelse + @empty +
+ Belum ada dokumen siswa yang diunggah. +
+ @endforelse +
-
+ @endif @endif
@else diff --git a/resources/views/settings/branding.blade.php b/resources/views/settings/branding.blade.php index a541f5a..cf3e9c2 100644 --- a/resources/views/settings/branding.blade.php +++ b/resources/views/settings/branding.blade.php @@ -18,6 +18,21 @@
+
+
+
+

Kontrol Portal Publik

+

Atur apakah card Dokumen Kelengkapan Siswa boleh tampil di halaman publik.

+
+ +
+

Jika dimatikan, pengunjung publik tetap bisa mencari data siswa, tetapi card dokumen tidak akan ditampilkan.

+
+
diff --git a/tests/Feature/StudentPromotionTest.php b/tests/Feature/StudentPromotionTest.php index 0ea9db1..4f7122b 100644 --- a/tests/Feature/StudentPromotionTest.php +++ b/tests/Feature/StudentPromotionTest.php @@ -262,6 +262,124 @@ class StudentPromotionTest extends TestCase $this->assertDatabaseCount('student_academic_histories', 2); } + public function test_non_active_students_are_hidden_from_promotion_index_and_rejected_if_posted(): void + { + $admin = User::factory()->create(['role' => 'admin']); + + [$fromYear, $toYear, $department, $fromClass, $targetClass] = $this->createPromotionContext(); + + $droppedStudent = Student::create([ + 'student_number' => 'S-004', + 'full_name' => 'Eko Keluar', + 'gender' => 'L', + 'academic_year_id' => $fromYear->id, + 'department_id' => $department->id, + 'class_id' => $fromClass->id, + 'status' => 'dropped', + ]); + + $transferredStudent = Student::create([ + 'student_number' => 'S-005', + 'full_name' => 'Fani Pindah', + 'gender' => 'P', + 'academic_year_id' => $fromYear->id, + 'department_id' => $department->id, + 'class_id' => $fromClass->id, + 'status' => 'transferred', + ]); + + $activeStudent = Student::create([ + 'student_number' => 'S-006', + 'full_name' => 'Gilang Aktif', + 'gender' => 'L', + 'academic_year_id' => $fromYear->id, + 'department_id' => $department->id, + 'class_id' => $fromClass->id, + 'status' => 'active', + ]); + + $response = $this->actingAs($admin)->get(route('student-promotions.index', [ + 'from_academic_year_id' => $fromYear->id, + 'to_academic_year_id' => $toYear->id, + 'class_id' => $fromClass->id, + ])); + + $response->assertOk(); + $response->assertSee('Gilang Aktif'); + $response->assertDontSee('Eko Keluar'); + $response->assertDontSee('Fani Pindah'); + + $droppedPost = $this->from(route('student-promotions.index', [ + 'from_academic_year_id' => $fromYear->id, + 'to_academic_year_id' => $toYear->id, + 'class_id' => $fromClass->id, + ]))->actingAs($admin)->post(route('student-promotions.store'), [ + 'from_academic_year_id' => $fromYear->id, + 'to_academic_year_id' => $toYear->id, + 'class_id' => $fromClass->id, + 'promotions' => [ + [ + 'student_id' => $droppedStudent->id, + 'target_class_id' => $targetClass->id, + 'action' => 'promote', + 'notes' => 'Tidak boleh ikut promosi.', + ], + ], + ]); + + $droppedPost->assertRedirect(route('student-promotions.index', [ + 'from_academic_year_id' => $fromYear->id, + 'to_academic_year_id' => $toYear->id, + 'class_id' => $fromClass->id, + ])); + $droppedPost->assertSessionHasErrors([ + 'promotions.0.student_id' => 'Siswa Eko Keluar tidak berstatus aktif sehingga tidak bisa diproses promosi.', + ]); + + $transferredPost = $this->from(route('student-promotions.index', [ + 'from_academic_year_id' => $fromYear->id, + 'to_academic_year_id' => $toYear->id, + 'class_id' => $fromClass->id, + ]))->actingAs($admin)->post(route('student-promotions.store'), [ + 'from_academic_year_id' => $fromYear->id, + 'to_academic_year_id' => $toYear->id, + 'class_id' => $fromClass->id, + 'promotions' => [ + [ + 'student_id' => $transferredStudent->id, + 'target_class_id' => $targetClass->id, + 'action' => 'promote', + 'notes' => 'Tidak boleh ikut promosi.', + ], + ], + ]); + + $transferredPost->assertRedirect(route('student-promotions.index', [ + 'from_academic_year_id' => $fromYear->id, + 'to_academic_year_id' => $toYear->id, + 'class_id' => $fromClass->id, + ])); + $transferredPost->assertSessionHasErrors([ + 'promotions.0.student_id' => 'Siswa Fani Pindah tidak berstatus aktif sehingga tidak bisa diproses promosi.', + ]); + + $droppedStudent->refresh(); + $transferredStudent->refresh(); + $activeStudent->refresh(); + + $this->assertSame('dropped', $droppedStudent->status); + $this->assertSame('transferred', $transferredStudent->status); + $this->assertSame('active', $activeStudent->status); + $this->assertDatabaseMissing('student_academic_histories', [ + 'student_id' => $droppedStudent->id, + 'academic_year_id' => $toYear->id, + ]); + $this->assertDatabaseMissing('student_academic_histories', [ + 'student_id' => $transferredStudent->id, + 'academic_year_id' => $toYear->id, + ]); + } + protected function createPromotionContext(array $overrides = []): array { $fromYear = AcademicYear::create([