diff --git a/app/Http/Controllers/PublicStudentLookupController.php b/app/Http/Controllers/PublicStudentLookupController.php index 39b88b8..cf08f09 100644 --- a/app/Http/Controllers/PublicStudentLookupController.php +++ b/app/Http/Controllers/PublicStudentLookupController.php @@ -4,7 +4,9 @@ namespace App\Http\Controllers; use App\Models\DocumentType; use App\Models\Student; +use App\Models\StudentDocument; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; use Illuminate\View\View; class PublicStudentLookupController extends Controller @@ -75,23 +77,63 @@ class PublicStudentLookupController extends Controller protected function resolvePhotoUrl(Student $student): ?string { $photoDocument = $student->documents - ->filter(function ($document) { - $code = strtoupper((string) ($document->documentType?->code ?? '')); - $name = strtolower((string) ($document->documentType?->name ?? '')); - - return $code === 'PAS_FOTO' - || $code === 'PASFOTO' - || str_contains($name, 'pas foto') - || str_contains($name, 'pasfoto'); + ->filter(fn ($document) => $this->isPhotoDocument($document)) + ->sortByDesc(function ($document) { + return sprintf( + '%d-%s', + $document->verification_status === 'valid' ? 1 : 0, + optional($document->created_at)?->format('YmdHis') ?? '00000000000000' + ); }) - ->sortByDesc('created_at') ->first(); + if (! $photoDocument && $student->relationLoaded('documents') === false) { + $photoDocument = StudentDocument::query() + ->with('documentType') + ->where('student_id', $student->id) + ->get() + ->filter(fn ($document) => $this->isPhotoDocument($document)) + ->sortByDesc(function ($document) { + return sprintf( + '%d-%s', + $document->verification_status === 'valid' ? 1 : 0, + optional($document->created_at)?->format('YmdHis') ?? '00000000000000' + ); + }) + ->first(); + } + if (! $photoDocument?->file_path) { return null; } - return asset('storage/' . $photoDocument->file_path); + if (! Storage::disk('public')->exists($photoDocument->file_path)) { + return null; + } + + return asset('storage/' . ltrim($photoDocument->file_path, '/')); + } + + protected function isPhotoDocument(StudentDocument $document): bool + { + $code = strtoupper((string) ($document->documentType?->code ?? '')); + $name = strtolower((string) ($document->documentType?->name ?? '')); + $fileName = strtolower((string) ($document->file_name ?? '')); + $mimeType = strtolower((string) ($document->mime_type ?? '')); + + $looksLikePhoto = $code === 'PAS_FOTO' + || $code === 'PASFOTO' + || str_contains($name, 'pas foto') + || str_contains($name, 'pasfoto') + || str_contains($fileName, 'pas foto') + || str_contains($fileName, 'pasfoto'); + + if ($looksLikePhoto) { + return true; + } + + return str_starts_with($mimeType, 'image/') + && (str_contains($name, 'foto') || str_contains($fileName, 'foto')); } protected function buildRequiredDocumentSummary(Student $student): array diff --git a/app/Http/Controllers/StudentSelfServiceController.php b/app/Http/Controllers/StudentSelfServiceController.php index 6e1631b..b90e1d7 100644 --- a/app/Http/Controllers/StudentSelfServiceController.php +++ b/app/Http/Controllers/StudentSelfServiceController.php @@ -5,8 +5,11 @@ namespace App\Http\Controllers; use App\Models\AcademicYear; use App\Models\SchoolClass; use App\Models\Student; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Illuminate\View\View; +use RealRashid\SweetAlert\Facades\Alert; class StudentSelfServiceController extends Controller { @@ -34,6 +37,11 @@ class StudentSelfServiceController extends Controller ->withQueryString(); $summaryBaseQuery = Student::query(); + $statusLabels = [ + 'not_started' => 'Belum Mulai', + 'draft' => 'Draft', + 'submitted' => 'Sudah Dikirim', + ]; return view('student-self-service.index', [ 'students' => $students, @@ -45,17 +53,115 @@ class StudentSelfServiceController extends Controller ], 'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'), 'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'), - 'statuses' => [ - 'not_started' => 'Belum Mulai', - 'draft' => 'Draft', - 'submitted' => 'Sudah Dikirim', - ], + 'statuses' => $statusLabels, 'summary' => [ 'total' => (clone $summaryBaseQuery)->count(), 'not_started' => (clone $summaryBaseQuery)->where('self_service_status', 'not_started')->count(), 'draft' => (clone $summaryBaseQuery)->where('self_service_status', 'draft')->count(), 'submitted' => (clone $summaryBaseQuery)->where('self_service_status', 'submitted')->count(), + 'with_code' => (clone $summaryBaseQuery)->whereNotNull('self_service_access_code')->count(), + 'without_code' => (clone $summaryBaseQuery)->whereNull('self_service_access_code')->count(), ], ]); } + + public function generateMissingCodes(): RedirectResponse + { + $students = Student::query()->whereNull('self_service_access_code')->get(); + + if ($students->isEmpty()) { + Alert::info('Tidak ada perubahan', 'Semua siswa sudah memiliki kode akses.'); + + return redirect()->route('student-self-service.index')->with('success', 'Semua siswa sudah memiliki kode akses.'); + } + + foreach ($students as $student) { + $student->forceFill([ + 'self_service_access_code' => $this->generateUniqueCode(), + 'self_service_code_generated_at' => now(), + ])->save(); + } + + Alert::success('Berhasil', $students->count() . ' kode akses berhasil digenerate.'); + + return redirect()->route('student-self-service.index')->with('success', $students->count() . ' kode akses berhasil digenerate.'); + } + + public function regenerateAllCodes(): RedirectResponse + { + $students = Student::query()->get(); + + foreach ($students as $student) { + $student->forceFill([ + 'self_service_access_code' => $this->generateUniqueCode(), + 'self_service_code_generated_at' => now(), + ])->save(); + } + + Alert::success('Berhasil', 'Semua kode akses siswa berhasil direset dan digenerate ulang.'); + + return redirect()->route('student-self-service.index')->with('success', 'Semua kode akses siswa berhasil direset dan digenerate ulang.'); + } + + public function regenerateStudentCode(Student $student): RedirectResponse + { + $student->forceFill([ + 'self_service_access_code' => $this->generateUniqueCode(), + 'self_service_code_generated_at' => now(), + ])->save(); + + Alert::success('Berhasil', 'Kode akses untuk ' . $student->full_name . ' berhasil digenerate ulang.'); + + return redirect()->route('student-self-service.index')->with('success', 'Kode akses untuk ' . $student->full_name . ' berhasil digenerate ulang.'); + } + + public function exportClassCodes(Request $request): View|RedirectResponse + { + $validated = $request->validate([ + 'class_id' => ['required', 'exists:classes,id'], + ], [ + 'class_id.required' => 'Pilih kelas terlebih dahulu sebelum export kode.', + ]); + + $class = SchoolClass::with(['academicYear', 'department'])->findOrFail($validated['class_id']); + $students = Student::query() + ->with(['classRoom']) + ->where('class_id', $class->id) + ->orderBy('full_name') + ->get(); + + if ($students->isEmpty()) { + return redirect()->route('student-self-service.index', ['class_id' => $class->id]) + ->withErrors(['class_id' => 'Kelas terpilih belum memiliki data siswa.']); + } + + $studentsWithoutCode = $students->filter(fn (Student $student) => blank($student->self_service_access_code)); + + foreach ($studentsWithoutCode as $student) { + $student->forceFill([ + 'self_service_access_code' => $this->generateUniqueCode(), + 'self_service_code_generated_at' => now(), + ])->save(); + } + + $students = Student::query() + ->where('class_id', $class->id) + ->orderBy('full_name') + ->get(); + + return view('student-self-service.export-class-codes', [ + 'class' => $class, + 'students' => $students, + 'generatedCount' => $studentsWithoutCode->count(), + ]); + } + + protected function generateUniqueCode(): string + { + do { + $code = strtoupper(Str::random(8)); + } while (Student::where('self_service_access_code', $code)->exists()); + + return $code; + } } diff --git a/resources/views/public/student-lookup.blade.php b/resources/views/public/student-lookup.blade.php index 0a31f44..3dd3bb3 100644 --- a/resources/views/public/student-lookup.blade.php +++ b/resources/views/public/student-lookup.blade.php @@ -142,42 +142,33 @@
-
+
-
Data ditemukan
-

{{ $student->full_name }}

-

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

-
-
-
-
{{ $student->classRoom?->name ?: '-' }}
-
Kelas saat ini
+
+ Data ditemukan
-
-
{{ $studentAchievements->count() }}
-
- {{ $studentAchievements->count() > 0 ? 'Prestasi tercatat' : 'Belum ada prestasi' }} +

{{ $student->full_name }}

+

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

+
+ +
+
+
Kelas Saat Ini
+
{{ $student->classRoom?->name ?: '-' }}
+
+
+
+ {{ $studentAchievements->count() > 0 ? 'Prestasi Tercatat' : 'Status Prestasi' }}
+
{{ $studentAchievements->count() }}
-
-
- @if ($student->public_photo_url) - {{ $student->full_name }} - @else -
- {{ strtoupper(mb_substr($student->full_name, 0, 1)) }} -
- @endif -
-
{{ $student->full_name }}
-
NIS: {{ $student->student_number ?: '-' }} · NISN: {{ $student->nisn ?: '-' }}
-
-
- +
NIS
@@ -199,38 +190,91 @@
-
-
-

Biodata Siswa

-
-
Nama: {{ $student->full_name }}
-
NIS: {{ $student->student_number ?: '-' }}
-
NISN: {{ $student->nisn ?: '-' }}
-
Jenis Kelamin: {{ $student->gender === 'L' ? 'Laki-laki' : 'Perempuan' }}
-
Tempat, Tanggal Lahir: {{ $student->place_of_birth ?: '-' }}, {{ $student->date_of_birth?->format('d-m-Y') ?: '-' }}
+
+
+
+ @if ($student->public_photo_url) + {{ $student->full_name }} + @else +
+
Pas Foto
+
Belum ada foto
+
+ @endif +
Pas Foto
-
-
-

Informasi Akademik

-
-
Tahun Angkatan: {{ $student->academicYear?->name ?: '-' }}
-
Program Keahlian: {{ $student->department?->name ?: '-' }}
-
Kelas: {{ $student->classRoom?->name ?: '-' }}
-
Status: {{ ucfirst($student->status) }}
-
Tanggal Masuk: {{ $student->entry_date?->format('d-m-Y') ?: '-' }}
-
-
+
+
+
Profil Siswa
+
Data Identitas dan Akademik Siswa
+
Ringkasan profil siswa yang tersedia untuk tampilan publik terbatas.
+
-
-

Alamat Siswa

-
-
Alamat Lengkap: {{ $student->address ?: '-' }}
-
Desa/Kelurahan: {{ $student->village ?: '-' }}
-
Kecamatan: {{ $student->district ?: '-' }}
-
Kabupaten/Kota: {{ $student->city ?: '-' }}
-
Provinsi: {{ $student->province ?: '-' }}
-
Kode Pos: {{ $student->postal_code ?: '-' }}
+
+
+

Biodata Siswa

+
+
Nama
+
{{ $student->full_name }}
+ +
NIS
+
{{ $student->student_number ?: '-' }}
+ +
NISN
+
{{ $student->nisn ?: '-' }}
+ +
Gender
+
{{ $student->gender === 'L' ? 'Laki-laki' : 'Perempuan' }}
+ +
Lahir
+
{{ $student->place_of_birth ?: '-' }}, {{ $student->date_of_birth?->format('d-m-Y') ?: '-' }}
+
+
+ +
+

Informasi Akademik

+
+
Angkatan
+
{{ $student->academicYear?->name ?: '-' }}
+ +
Program
+
{{ $student->department?->name ?: '-' }}
+ +
Kelas
+
{{ $student->classRoom?->name ?: '-' }}
+ +
Status
+
{{ ucfirst($student->status) }}
+ +
Masuk
+
{{ $student->entry_date?->format('d-m-Y') ?: '-' }}
+
+
+ +
+

Alamat Siswa

+
+
Alamat
+
{{ $student->address ?: '-' }}
+ +
Desa
+
{{ $student->village ?: '-' }}
+ +
Kecamatan
+
{{ $student->district ?: '-' }}
+ +
Kota
+
{{ $student->city ?: '-' }}
+ +
Provinsi
+
{{ $student->province ?: '-' }}
+ +
Kode Pos
+
{{ $student->postal_code ?: '-' }}
+
+
+
diff --git a/resources/views/student-self-service/export-class-codes.blade.php b/resources/views/student-self-service/export-class-codes.blade.php new file mode 100644 index 0000000..df7bfaa --- /dev/null +++ b/resources/views/student-self-service/export-class-codes.blade.php @@ -0,0 +1,441 @@ +@php + $portalUrl = route('public.student-self-service.index'); + $qrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&margin=8&data=' . urlencode($portalUrl); +@endphp + + + + + + Daftar Kode Akses Self Servis - {{ $class->name }} + + + +
+
+ +
+ +
+
Dokumen Internal Sekolah
+
Database Kesiswaan
+
Lembar Distribusi Kode Akses Self Servis Siswa
+
+ +
+

Daftar Kode Akses Self Servis

+

Digunakan untuk proses pelengkapan data siswa secara mandiri melalui portal sekolah.

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Kelas: {{ $class->name }}
Tahun Angkatan: {{ $class->academicYear?->name ?: '-' }}
Program Keahlian: {{ $class->department?->name ?: '-' }}
Tanggal Cetak: {{ now()->format('d-m-Y H:i') }}
Jumlah Siswa: {{ $students->count() }} siswa
Kode Baru Tergenerate: {{ $generatedCount }}
+
+ +
+
+
+
Akses Digital Siswa
+
Portal Self Servis Siswa
+
+ Gunakan lembar ini sebagai panduan akses awal ke portal self servis siswa. Siswa atau orang tua/wali dapat membuka portal menggunakan tautan di bawah atau scan QR di samping, lalu masuk dengan NIS/NISN dan kode akses masing-masing. +
+ + +
+
+
Scan QR Portal
+
+ QR Portal Self Servis +
+
Arahkan kamera ponsel untuk membuka portal self servis siswa.
+
+
+
+ + + + + + + + + + + + + + @foreach ($students as $index => $student) + + + + + + + + + @endforeach + +
NoNama SiswaNISNISNKode AksesTanda Terima
{{ $index + 1 }}{{ $student->full_name }}{{ $student->student_number ?: '-' }}{{ $student->nisn ?: '-' }}{{ $student->self_service_access_code ?: '-' }}
+ +
+
Catatan Penggunaan:
+
    +
  1. Siswa masuk ke portal self servis menggunakan NIS/NISN dan kode akses pada lembar ini.
  2. +
  3. Siswa juga dapat membuka portal dengan scan QR atau mengetik link portal yang tercantum di dokumen ini.
  4. +
  5. Jika kode akses hilang atau perlu diganti, admin sekolah dapat melakukan generate ulang dari panel admin.
  6. +
  7. Disarankan wali kelas memastikan setiap siswa menerima kode akses yang benar.
  8. +
+
+ + + + + + +
+
+ Mengetahui,
+ Admin Kesiswaan +
+ (........................................) +
+
+
+ Diterima oleh,
+ Wali Kelas / Petugas +
+ (........................................) +
+
+ + +
+ + diff --git a/resources/views/student-self-service/index.blade.php b/resources/views/student-self-service/index.blade.php index 580683c..255c6cd 100644 --- a/resources/views/student-self-service/index.blade.php +++ b/resources/views/student-self-service/index.blade.php @@ -1,5 +1,5 @@ -
+
Total Siswa
{{ $summary['total'] }}
@@ -16,8 +16,76 @@
Sudah Dikirim
{{ $summary['submitted'] }}
+
+
Belum Punya Kode
+
{{ $summary['without_code'] }}
+
+ +
+
+
+
+
+ {{ $summary['without_code'] > 0 ? 'Masih ada siswa yang belum punya kode akses' : 'Semua siswa sudah memiliki kode akses' }} +
+

+ {{ $summary['without_code'] > 0 + ? $summary['without_code'] . ' siswa belum memiliki kode. Gunakan generate massal agar portal self servis bisa langsung dipakai.' + : 'Kode akses sudah lengkap. Anda bisa reset bila sekolah ingin mengganti seluruh batch kode.' }} +

+
+
+ {{ $summary['with_code'] }} siswa sudah punya kode +
+
+
+ +
+
+
+

Aksi Massal Kode Akses

+

Bootstrap awal atau ganti seluruh kode akses.

+
+ +
+
+ @csrf + +
+ +
+ @csrf + +
+
+
+ +
+
+

Export Kode per Kelas

+

Siapkan lembar siap cetak / PDF per kelas.

+
+ +
+ + + + +
+
+
+
+
@@ -43,6 +111,7 @@ Status Kode Akses Aktivitas + Aksi @@ -77,10 +146,25 @@
Terakhir akses: {{ $student->self_service_last_access_at?->format('d-m-Y H:i') ?: '-' }}
Submitted: {{ $student->self_service_submitted_at?->format('d-m-Y H:i') ?: '-' }}
+ +
+ @if ($student->self_service_access_code) + + @endif + + @csrf + + +
+ @empty - Belum ada data siswa untuk self servis. + Belum ada data siswa untuk self servis. @endforelse diff --git a/routes/web.php b/routes/web.php index dab0930..5d683ba 100644 --- a/routes/web.php +++ b/routes/web.php @@ -74,6 +74,10 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::post('student-promotions', [StudentPromotionController::class, 'store'])->name('student-promotions.store'); Route::resource('student-achievements', StudentAchievementController::class)->except(['show']); Route::get('student-self-service', [StudentSelfServiceController::class, 'index'])->name('student-self-service.index'); + Route::post('student-self-service/generate-missing-codes', [StudentSelfServiceController::class, 'generateMissingCodes'])->name('student-self-service.generate-missing-codes'); + Route::post('student-self-service/regenerate-all-codes', [StudentSelfServiceController::class, 'regenerateAllCodes'])->name('student-self-service.regenerate-all-codes'); + Route::post('student-self-service/{student}/regenerate-code', [StudentSelfServiceController::class, 'regenerateStudentCode'])->name('student-self-service.regenerate-student-code'); + Route::get('student-self-service/export-class-codes', [StudentSelfServiceController::class, 'exportClassCodes'])->name('student-self-service.export-class-codes'); Route::resource('students.parents', StudentParentController::class)->except(['index', 'show']); Route::resource('students.documents', StudentDocumentController::class)->except(['index', 'show']); Route::post('students/{student}/documents/{document}/quick-verify', [StudentDocumentController::class, 'quickVerify'])->name('students.documents.quick-verify');