From ac22f8b7c84501f288d3e16d76f0c8d2b226e82b Mon Sep 17 00:00:00 2001 From: novysan87 Date: Thu, 30 Apr 2026 00:04:52 +0700 Subject: [PATCH] feat: update student import and graduation workflows --- .../Api/Integration/StudentSyncController.php | 70 ++- .../GraduatedStudentController.php | 115 +++- .../GraduationBulkUploadController.php | 41 +- .../PublicStudentLookupController.php | 14 +- .../PublicStudentSelfServiceController.php | 2 + .../StudentAchievementController.php | 46 +- app/Http/Controllers/StudentController.php | 550 +++++++++++++++++- .../Controllers/StudentExitController.php | 26 +- .../StudentSelfServiceController.php | 6 +- .../Controllers/StudentTransferController.php | 26 +- app/Http/Controllers/SyncTokenController.php | 8 +- app/Models/Student.php | 1 + app/Models/StudentGraduation.php | 12 + ...and_skhun_to_student_graduations_table.php | 38 ++ ...tificates_to_student_graduations_table.php | 38 ++ ...columns_from_student_graduations_table.php | 30 + ...04_29_234500_add_nik_to_students_table.php | 22 + package-lock.json | 10 + package.json | 1 + resources/css/app.css | 2 + resources/js/app.js | 76 +++ resources/views/dashboard.blade.php | 269 +++++---- .../graduated-students/bulk-upload.blade.php | 329 ++++++++--- .../views/graduated-students/index.blade.php | 217 ++++--- .../preview-document.blade.php | 194 +++++- .../views/graduated-students/show.blade.php | 313 +++++++--- resources/views/layouts/app.blade.php | 1 + resources/views/layouts/navigation.blade.php | 134 ++--- .../student-self-service/edit.blade.php | 28 +- .../student-self-service/verify.blade.php | 46 +- .../settings/sync-tokens/index.blade.php | 26 +- .../student-achievements/_form.blade.php | 203 ++++--- .../student-achievements/create.blade.php | 94 ++- .../views/student-achievements/edit.blade.php | 99 +++- .../student-achievements/index.blade.php | 345 ++++++++--- resources/views/student-exits/index.blade.php | 199 ++++++- .../student-exits/preview-letter.blade.php | 157 ++++- resources/views/student-exits/show.blade.php | 151 +++-- .../views/student-promotions/index.blade.php | 89 +-- .../export-class-codes.blade.php | 20 +- .../student-self-service/index.blade.php | 162 ++++-- .../views/student-transfers/index.blade.php | 199 ++++++- .../preview-letter.blade.php | 157 ++++- .../views/student-transfers/show.blade.php | 151 +++-- resources/views/students/_form.blade.php | 281 +++++---- resources/views/students/create.blade.php | 22 +- resources/views/students/edit.blade.php | 22 +- resources/views/students/import.blade.php | 40 +- resources/views/students/index.blade.php | 270 ++++++--- resources/views/students/show.blade.php | 481 ++++++++++++--- routes/web.php | 1 + 51 files changed, 4547 insertions(+), 1287 deletions(-) create mode 100644 database/migrations/2026_04_29_202100_add_transcript_and_skhun_to_student_graduations_table.php create mode 100644 database/migrations/2026_04_29_202600_add_ukk_and_lsp_certificates_to_student_graduations_table.php create mode 100644 database/migrations/2026_04_29_223900_drop_skhun_columns_from_student_graduations_table.php create mode 100644 database/migrations/2026_04_29_234500_add_nik_to_students_table.php diff --git a/app/Http/Controllers/Api/Integration/StudentSyncController.php b/app/Http/Controllers/Api/Integration/StudentSyncController.php index b370867..798a9a6 100644 --- a/app/Http/Controllers/Api/Integration/StudentSyncController.php +++ b/app/Http/Controllers/Api/Integration/StudentSyncController.php @@ -16,7 +16,17 @@ class StudentSyncController extends Controller abort_unless(collect($token->abilities ?? [])->contains('students'), 403, 'Token tidak memiliki akses ke data siswa.'); + $classLevel = strtoupper(trim((string) $request->query('class_level', ''))); + $items = Student::query() + ->when(in_array($classLevel, ['X', 'XI', 'XII'], true), function ($query) use ($classLevel) { + $query->whereHas('classRoom', fn ($classQuery) => $classQuery->where('level', $classLevel)); + }) + ->with([ + 'academicYear:id,name,start_year,end_year,is_active', + 'department:id,code,name,description,is_active', + 'classRoom:id,academic_year_id,department_id,name,level,is_active', + ]) ->orderBy('full_name') ->get([ 'id', @@ -24,16 +34,74 @@ class StudentSyncController extends Controller 'nisn', 'full_name', 'gender', + 'place_of_birth', + 'date_of_birth', + 'nationality', + 'address', + 'postal_code', 'academic_year_id', 'department_id', 'class_id', + 'entry_date', 'status', 'phone_number', 'email', 'photo_path', 'notes', 'updated_at', - ]); + ]) + ->map(function (Student $student) { + return [ + 'id' => $student->id, + 'student_number' => $student->student_number, + 'nisn' => $student->nisn, + 'full_name' => $student->full_name, + 'gender' => $student->gender, + 'place_of_birth' => $student->place_of_birth, + 'date_of_birth' => $student->date_of_birth?->toDateString(), + 'nationality' => $student->nationality, + 'address' => $student->address, + 'postal_code' => $student->postal_code, + 'academic_year_id' => $student->academic_year_id, + 'academic_year_name' => $student->academicYear?->name, + 'academic_year' => $student->academicYear ? [ + 'id' => $student->academicYear->id, + 'name' => $student->academicYear->name, + 'start_year' => $student->academicYear->start_year, + 'end_year' => $student->academicYear->end_year, + 'is_active' => (bool) $student->academicYear->is_active, + ] : null, + 'department_id' => $student->department_id, + 'department_code' => $student->department?->code, + 'department_name' => $student->department?->name, + 'department' => $student->department ? [ + 'id' => $student->department->id, + 'code' => $student->department->code, + 'name' => $student->department->name, + 'description' => $student->department->description, + 'is_active' => (bool) $student->department->is_active, + ] : null, + 'class_id' => $student->class_id, + 'class_name' => $student->classRoom?->name, + 'class_level' => $student->classRoom?->level, + 'classroom' => $student->classRoom ? [ + 'id' => $student->classRoom->id, + 'name' => $student->classRoom->name, + 'level' => $student->classRoom->level, + 'academic_year_id' => $student->classRoom->academic_year_id, + 'department_id' => $student->classRoom->department_id, + 'is_active' => (bool) $student->classRoom->is_active, + ] : null, + 'entry_date' => $student->entry_date?->toDateString(), + 'status' => $student->status, + 'phone_number' => $student->phone_number, + 'email' => $student->email, + 'photo_path' => $student->photo_path, + 'notes' => $student->notes, + 'updated_at' => $student->updated_at?->toISOString(), + ]; + }) + ->values(); return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/GraduatedStudentController.php b/app/Http/Controllers/GraduatedStudentController.php index c2eb1c4..316f708 100644 --- a/app/Http/Controllers/GraduatedStudentController.php +++ b/app/Http/Controllers/GraduatedStudentController.php @@ -21,6 +21,7 @@ class GraduatedStudentController extends Controller $academicYearId = $request->get('academic_year_id'); $departmentId = $request->get('department_id'); $classId = $request->get('class_id'); + $documentStatus = (string) $request->get('document_status', 'all'); $sort = (string) $request->get('sort', 'full_name'); $direction = strtolower((string) $request->get('direction', 'asc')) === 'desc' ? 'desc' : 'asc'; @@ -38,6 +39,14 @@ class GraduatedStudentController extends Controller $sort = 'full_name'; } + $documentFields = [ + 'skl_file_path', + 'ijazah_file_path', + 'transcript_file_path', + 'ukk_certificate_file_path', + 'lsp_certificate_file_path', + ]; + $graduations = StudentGraduation::query() ->with(['academicYear', 'department', 'classRoom']) ->when($search !== '', function ($query) use ($search) { @@ -50,17 +59,46 @@ class GraduatedStudentController extends Controller ->when($academicYearId, fn ($query) => $query->where('academic_year_id', $academicYearId)) ->when($departmentId, fn ($query) => $query->where('department_id', $departmentId)) ->when($classId, fn ($query) => $query->where('class_id', $classId)) + ->when($documentStatus === 'incomplete', function ($query) use ($documentFields) { + foreach ($documentFields as $field) { + $query->orWhereNull($field); + } + }) + ->when($documentStatus === 'complete', function ($query) use ($documentFields) { + foreach ($documentFields as $field) { + $query->whereNotNull($field); + } + }) ->orderBy($allowedSorts[$sort], $direction) ->orderBy('id', $direction) ->paginate(10) ->withQueryString(); + $totalGraduations = StudentGraduation::count(); $summary = [ - 'total' => $graduations->total(), + 'total' => $totalGraduations, + 'filtered' => $graduations->total(), 'with_skl' => StudentGraduation::query()->whereNotNull('skl_file_path')->count(), 'with_ijazah' => StudentGraduation::query()->whereNotNull('ijazah_file_path')->count(), + 'with_transcript' => StudentGraduation::query()->whereNotNull('transcript_file_path')->count(), + 'with_ukk_certificate' => StudentGraduation::query()->whereNotNull('ukk_certificate_file_path')->count(), + 'with_lsp_certificate' => StudentGraduation::query()->whereNotNull('lsp_certificate_file_path')->count(), + 'this_year' => StudentGraduation::query()->whereYear('graduation_date', now()->year)->count(), + 'latest_graduation_date' => StudentGraduation::query()->max('graduation_date'), ]; + $graduations->getCollection()->transform(function (StudentGraduation $graduation) use ($documentFields) { + $availableDocuments = collect($documentFields)->filter(fn ($field) => filled($graduation->{$field}))->count(); + $graduation->documents_total = count($documentFields); + $graduation->documents_available = $availableDocuments; + $graduation->documents_missing = max($graduation->documents_total - $availableDocuments, 0); + $graduation->documents_progress_percentage = $graduation->documents_total > 0 + ? (int) round(($availableDocuments / $graduation->documents_total) * 100) + : 100; + + return $graduation; + }); + return view('graduated-students.index', [ 'graduations' => $graduations, 'search' => $search, @@ -68,10 +106,16 @@ class GraduatedStudentController extends Controller 'academic_year_id' => $academicYearId, 'department_id' => $departmentId, 'class_id' => $classId, + 'document_status' => $documentStatus, ], 'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'), 'departments' => Department::orderBy('name')->pluck('name', 'id'), 'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'), + 'documentStatusOptions' => [ + 'all' => 'Semua Dokumen', + 'complete' => 'Dokumen Lengkap', + 'incomplete' => 'Dokumen Belum Lengkap', + ], 'sort' => $sort, 'direction' => $direction, 'summary' => $summary, @@ -94,11 +138,20 @@ class GraduatedStudentController extends Controller 'graduation_note' => ['nullable', 'string'], 'skl_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'], 'ijazah_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'], + 'transcript_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'], + 'ukk_certificate_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'], + 'lsp_certificate_file' => ['nullable', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'], ], [ 'skl_file.mimes' => 'File SKL harus berupa PDF, JPG, JPEG, atau PNG.', 'skl_file.max' => 'Ukuran file SKL maksimal 4 MB.', 'ijazah_file.mimes' => 'File ijazah harus berupa PDF, JPG, JPEG, atau PNG.', 'ijazah_file.max' => 'Ukuran file ijazah maksimal 4 MB.', + 'transcript_file.mimes' => 'File transkrip nilai harus berupa PDF, JPG, JPEG, atau PNG.', + 'transcript_file.max' => 'Ukuran file transkrip nilai maksimal 4 MB.', + 'ukk_certificate_file.mimes' => 'File sertifikat UKK harus berupa PDF, JPG, JPEG, atau PNG.', + 'ukk_certificate_file.max' => 'Ukuran file sertifikat UKK maksimal 4 MB.', + 'lsp_certificate_file.mimes' => 'File sertifikat LSP harus berupa PDF, JPG, JPEG, atau PNG.', + 'lsp_certificate_file.max' => 'Ukuran file sertifikat LSP maksimal 4 MB.', ]); $graduatedStudent->graduation_date = $validated['graduation_date'] ?? $graduatedStudent->graduation_date; @@ -132,6 +185,48 @@ class GraduatedStudentController extends Controller $graduatedStudent->ijazah_file_size = $file->getSize(); } + if ($request->hasFile('transcript_file')) { + if ($graduatedStudent->transcript_file_path && Storage::disk('public')->exists($graduatedStudent->transcript_file_path)) { + Storage::disk('public')->delete($graduatedStudent->transcript_file_path); + } + + $file = $request->file('transcript_file'); + $path = $file->store("student-graduation-documents/{$graduatedStudent->student_id}/transcript", 'public'); + + $graduatedStudent->transcript_file_name = $file->getClientOriginalName(); + $graduatedStudent->transcript_file_path = $path; + $graduatedStudent->transcript_mime_type = $file->getMimeType(); + $graduatedStudent->transcript_file_size = $file->getSize(); + } + + if ($request->hasFile('ukk_certificate_file')) { + if ($graduatedStudent->ukk_certificate_file_path && Storage::disk('public')->exists($graduatedStudent->ukk_certificate_file_path)) { + Storage::disk('public')->delete($graduatedStudent->ukk_certificate_file_path); + } + + $file = $request->file('ukk_certificate_file'); + $path = $file->store("student-graduation-documents/{$graduatedStudent->student_id}/ukk-certificate", 'public'); + + $graduatedStudent->ukk_certificate_file_name = $file->getClientOriginalName(); + $graduatedStudent->ukk_certificate_file_path = $path; + $graduatedStudent->ukk_certificate_mime_type = $file->getMimeType(); + $graduatedStudent->ukk_certificate_file_size = $file->getSize(); + } + + if ($request->hasFile('lsp_certificate_file')) { + if ($graduatedStudent->lsp_certificate_file_path && Storage::disk('public')->exists($graduatedStudent->lsp_certificate_file_path)) { + Storage::disk('public')->delete($graduatedStudent->lsp_certificate_file_path); + } + + $file = $request->file('lsp_certificate_file'); + $path = $file->store("student-graduation-documents/{$graduatedStudent->student_id}/lsp-certificate", 'public'); + + $graduatedStudent->lsp_certificate_file_name = $file->getClientOriginalName(); + $graduatedStudent->lsp_certificate_file_path = $path; + $graduatedStudent->lsp_certificate_mime_type = $file->getMimeType(); + $graduatedStudent->lsp_certificate_file_size = $file->getSize(); + } + $graduatedStudent->save(); Alert::success('Berhasil', 'Arsip siswa lulus berhasil diperbarui.'); @@ -141,11 +236,19 @@ class GraduatedStudentController extends Controller public function previewDocument(StudentGraduation $graduatedStudent, string $document): View { - abort_unless(in_array($document, ['skl', 'ijazah'], true), 404); + abort_unless(in_array($document, ['skl', 'ijazah', 'transcript', 'ukk-certificate', 'lsp-certificate'], true), 404); - $filePathField = $document === 'skl' ? 'skl_file_path' : 'ijazah_file_path'; - $fileNameField = $document === 'skl' ? 'skl_file_name' : 'ijazah_file_name'; - $mimeTypeField = $document === 'skl' ? 'skl_mime_type' : 'ijazah_mime_type'; + $documentConfig = [ + 'skl' => ['path' => 'skl_file_path', 'name' => 'skl_file_name', 'mime' => 'skl_mime_type', 'label' => 'SKL'], + 'ijazah' => ['path' => 'ijazah_file_path', 'name' => 'ijazah_file_name', 'mime' => 'ijazah_mime_type', 'label' => 'Ijazah'], + 'transcript' => ['path' => 'transcript_file_path', 'name' => 'transcript_file_name', 'mime' => 'transcript_mime_type', 'label' => 'Transkrip Nilai'], + 'ukk-certificate' => ['path' => 'ukk_certificate_file_path', 'name' => 'ukk_certificate_file_name', 'mime' => 'ukk_certificate_mime_type', 'label' => 'Sertifikat UKK'], + 'lsp-certificate' => ['path' => 'lsp_certificate_file_path', 'name' => 'lsp_certificate_file_name', 'mime' => 'lsp_certificate_mime_type', 'label' => 'Sertifikat LSP'], + ][$document]; + + $filePathField = $documentConfig['path']; + $fileNameField = $documentConfig['name']; + $mimeTypeField = $documentConfig['mime']; abort_unless($graduatedStudent->{$filePathField} && Storage::disk('public')->exists($graduatedStudent->{$filePathField}), 404); @@ -157,7 +260,7 @@ class GraduatedStudentController extends Controller return view('graduated-students.preview-document', [ 'graduation' => $graduatedStudent, 'documentType' => $document, - 'documentLabel' => $document === 'skl' ? 'SKL' : 'Ijazah', + 'documentLabel' => $documentConfig['label'], 'fileName' => $graduatedStudent->{$fileNameField}, 'fileUrl' => $fileUrl, 'mimeType' => $mimeType, diff --git a/app/Http/Controllers/GraduationBulkUploadController.php b/app/Http/Controllers/GraduationBulkUploadController.php index 99bf578..6eeff86 100644 --- a/app/Http/Controllers/GraduationBulkUploadController.php +++ b/app/Http/Controllers/GraduationBulkUploadController.php @@ -20,6 +20,9 @@ class GraduationBulkUploadController extends Controller 'documentTypes' => [ 'skl' => 'SKL', 'ijazah' => 'Ijazah', + 'transcript' => 'Transkrip Nilai', + 'ukk-certificate' => 'Sertifikat UKK', + 'lsp-certificate' => 'Sertifikat LSP', ], 'matchByOptions' => [ 'student_number' => 'NIS', @@ -31,15 +34,15 @@ class GraduationBulkUploadController extends Controller public function preview(Request $request): RedirectResponse { $validated = $request->validate([ - 'document_type' => ['required', 'in:skl,ijazah'], + 'document_type' => ['required', 'in:skl,ijazah,transcript,ukk-certificate,lsp-certificate'], 'match_by' => ['required', 'in:student_number,nisn'], 'documents' => ['required', 'array', 'min:1', 'max:10'], - 'documents.*' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:2048'], + 'documents.*' => ['required', 'file', 'mimes:pdf,jpg,jpeg,png', 'max:4096'], ], [ 'documents.required' => 'Pilih minimal satu file dokumen untuk dipreview.', 'documents.max' => 'Maksimal 10 file per batch upload.', 'documents.*.mimes' => 'Dokumen hanya boleh berupa PDF, JPG, JPEG, atau PNG.', - 'documents.*.max' => 'Ukuran tiap dokumen maksimal 2 MB.', + 'documents.*.max' => 'Ukuran tiap dokumen maksimal 4 MB.', ]); $graduations = StudentGraduation::query()->get()->keyBy($validated['match_by']); @@ -59,7 +62,13 @@ class GraduationBulkUploadController extends Controller $baseName = pathinfo($originalName, PATHINFO_FILENAME); $normalizedKey = trim($baseName); $graduation = $graduations->get($normalizedKey); - $existingPathField = $validated['document_type'] === 'skl' ? 'skl_file_path' : 'ijazah_file_path'; + $existingPathField = [ + 'skl' => 'skl_file_path', + 'ijazah' => 'ijazah_file_path', + 'transcript' => 'transcript_file_path', + 'ukk-certificate' => 'ukk_certificate_file_path', + 'lsp-certificate' => 'lsp_certificate_file_path', + ][$validated['document_type']]; $storedTempPath = $file->store('temp/graduation-bulk-upload', 'public'); @@ -93,7 +102,13 @@ class GraduationBulkUploadController extends Controller $preview = [ 'token' => (string) Str::uuid(), 'document_type' => $validated['document_type'], - 'document_label' => $validated['document_type'] === 'skl' ? 'SKL' : 'Ijazah', + 'document_label' => [ + 'skl' => 'SKL', + 'ijazah' => 'Ijazah', + 'transcript' => 'Transkrip Nilai', + 'ukk-certificate' => 'Sertifikat UKK', + 'lsp-certificate' => 'Sertifikat LSP', + ][$validated['document_type']], 'match_by' => $validated['match_by'], 'match_by_label' => $validated['match_by'] === 'student_number' ? 'NIS' : 'NISN', 'rows' => $rows, @@ -120,10 +135,18 @@ class GraduationBulkUploadController extends Controller } $documentType = $preview['document_type']; - $fileNameField = $documentType === 'skl' ? 'skl_file_name' : 'ijazah_file_name'; - $filePathField = $documentType === 'skl' ? 'skl_file_path' : 'ijazah_file_path'; - $mimeTypeField = $documentType === 'skl' ? 'skl_mime_type' : 'ijazah_mime_type'; - $fileSizeField = $documentType === 'skl' ? 'skl_file_size' : 'ijazah_file_size'; + $documentConfig = [ + 'skl' => ['name' => 'skl_file_name', 'path' => 'skl_file_path', 'mime' => 'skl_mime_type', 'size' => 'skl_file_size'], + 'ijazah' => ['name' => 'ijazah_file_name', 'path' => 'ijazah_file_path', 'mime' => 'ijazah_mime_type', 'size' => 'ijazah_file_size'], + 'transcript' => ['name' => 'transcript_file_name', 'path' => 'transcript_file_path', 'mime' => 'transcript_mime_type', 'size' => 'transcript_file_size'], + 'ukk-certificate' => ['name' => 'ukk_certificate_file_name', 'path' => 'ukk_certificate_file_path', 'mime' => 'ukk_certificate_mime_type', 'size' => 'ukk_certificate_file_size'], + 'lsp-certificate' => ['name' => 'lsp_certificate_file_name', 'path' => 'lsp_certificate_file_path', 'mime' => 'lsp_certificate_mime_type', 'size' => 'lsp_certificate_file_size'], + ][$documentType]; + + $fileNameField = $documentConfig['name']; + $filePathField = $documentConfig['path']; + $mimeTypeField = $documentConfig['mime']; + $fileSizeField = $documentConfig['size']; $processed = 0; $skipped = 0; diff --git a/app/Http/Controllers/PublicStudentLookupController.php b/app/Http/Controllers/PublicStudentLookupController.php index 9aa1222..ae30a46 100644 --- a/app/Http/Controllers/PublicStudentLookupController.php +++ b/app/Http/Controllers/PublicStudentLookupController.php @@ -144,12 +144,20 @@ class PublicStudentLookupController extends Controller public function downloadGraduationDocument(Student $student, string $document): StreamedResponse { abort_unless($student->status === 'graduated', 404); - abort_unless(in_array($document, ['skl', 'ijazah'], true), 404); + abort_unless(in_array($document, ['skl', 'ijazah', 'transcript', 'ukk-certificate', 'lsp-certificate'], true), 404); $graduation = StudentGraduation::query()->where('student_id', $student->id)->firstOrFail(); - $filePathField = $document === 'skl' ? 'skl_file_path' : 'ijazah_file_path'; - $fileNameField = $document === 'skl' ? 'skl_file_name' : 'ijazah_file_name'; + $documentConfig = [ + 'skl' => ['path' => 'skl_file_path', 'name' => 'skl_file_name'], + 'ijazah' => ['path' => 'ijazah_file_path', 'name' => 'ijazah_file_name'], + 'transcript' => ['path' => 'transcript_file_path', 'name' => 'transcript_file_name'], + 'ukk-certificate' => ['path' => 'ukk_certificate_file_path', 'name' => 'ukk_certificate_file_name'], + 'lsp-certificate' => ['path' => 'lsp_certificate_file_path', 'name' => 'lsp_certificate_file_name'], + ][$document]; + + $filePathField = $documentConfig['path']; + $fileNameField = $documentConfig['name']; abort_unless($graduation->{$filePathField} && Storage::disk('public')->exists($graduation->{$filePathField}), 404); diff --git a/app/Http/Controllers/PublicStudentSelfServiceController.php b/app/Http/Controllers/PublicStudentSelfServiceController.php index 5dbe1c7..902502b 100644 --- a/app/Http/Controllers/PublicStudentSelfServiceController.php +++ b/app/Http/Controllers/PublicStudentSelfServiceController.php @@ -125,6 +125,7 @@ class PublicStudentSelfServiceController extends Controller 'date_of_birth' => ['nullable', 'date'], 'religion' => ['nullable', 'string', 'max:50'], 'nationality' => ['nullable', 'string', 'max:50'], + 'nik' => ['nullable', 'string', 'max:30', Rule::unique('students', 'nik')->ignore($student->id)], 'family_card_number' => ['nullable', 'string', 'max:30'], 'child_order' => ['nullable', 'integer', 'min:1', 'max:50'], 'number_of_siblings' => ['nullable', 'integer', 'min:0', 'max:50'], @@ -171,6 +172,7 @@ class PublicStudentSelfServiceController extends Controller 'date_of_birth' => $data['date_of_birth'] ?? null, 'religion' => $data['religion'] ?? null, 'nationality' => $data['nationality'] ?? 'Indonesia', + 'nik' => $data['nik'] ?? null, 'family_card_number' => $data['family_card_number'] ?? null, 'child_order' => $data['child_order'] ?? null, 'number_of_siblings' => $data['number_of_siblings'] ?? null, diff --git a/app/Http/Controllers/StudentAchievementController.php b/app/Http/Controllers/StudentAchievementController.php index e93c591..8b152b2 100644 --- a/app/Http/Controllers/StudentAchievementController.php +++ b/app/Http/Controllers/StudentAchievementController.php @@ -18,8 +18,10 @@ class StudentAchievementController extends Controller $search = trim((string) $request->get('search')); $academicYearId = $request->get('academic_year_id'); $classId = $request->get('class_id'); + $level = trim((string) $request->get('level')); + $category = trim((string) $request->get('category')); - $achievements = StudentAchievement::query() + $achievementsQuery = StudentAchievement::query() ->with(['student.academicYear', 'student.department', 'student.classRoom']) ->when($search !== '', function ($query) use ($search) { $query->where(function ($subQuery) use ($search) { @@ -35,25 +37,67 @@ class StudentAchievementController extends Controller }) ->when($academicYearId, fn ($query) => $query->whereHas('student', fn ($studentQuery) => $studentQuery->where('academic_year_id', $academicYearId))) ->when($classId, fn ($query) => $query->whereHas('student', fn ($studentQuery) => $studentQuery->where('class_id', $classId))) + ->when($level !== '', fn ($query) => $query->where('level', $level)) + ->when($category !== '', fn ($query) => $query->where('category', $category)); + + $achievements = (clone $achievementsQuery) ->latest('achievement_year') ->latest('achievement_date') ->latest('id') ->paginate(15) ->withQueryString(); + $levelCounts = StudentAchievement::query() + ->selectRaw('COALESCE(level, ?) as level_label, COUNT(*) as total', ['Tanpa tingkat']) + ->groupBy('level') + ->orderByDesc('total') + ->pluck('total', 'level_label'); + + $withCertificateCount = StudentAchievement::query()->whereNotNull('certificate_file')->count(); + $withPhotoCount = StudentAchievement::query()->whereNotNull('activity_photo')->count(); + $thisYearCount = StudentAchievement::query()->where('achievement_year', now()->year)->count(); + + $recentAchievements = StudentAchievement::query() + ->with(['student:id,full_name']) + ->latest('achievement_date') + ->latest('achievement_year') + ->latest('id') + ->limit(3) + ->get(); + + $categoryOptions = StudentAchievement::query() + ->whereNotNull('category') + ->where('category', '!=', '') + ->distinct() + ->orderBy('category') + ->pluck('category', 'category'); + return view('student-achievements.index', [ 'achievements' => $achievements, 'search' => $search, 'filters' => [ 'academic_year_id' => $academicYearId, 'class_id' => $classId, + 'level' => $level, + 'category' => $category, ], 'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'), 'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'), + 'categoryOptions' => $categoryOptions, + 'levelOptions' => $this->levelOptions(), 'summary' => [ 'total' => StudentAchievement::count(), 'filtered' => $achievements->total(), + 'students' => StudentAchievement::distinct('student_id')->count('student_id'), + 'latest_year' => StudentAchievement::max('achievement_year'), + 'top_level' => $levelCounts->keys()->first(), + 'top_level_total' => $levelCounts->first(), + 'with_certificate' => $withCertificateCount, + 'without_certificate' => max(StudentAchievement::count() - $withCertificateCount, 0), + 'with_photo' => $withPhotoCount, + 'this_year' => $thisYearCount, ], + 'recentAchievements' => $recentAchievements, ]); } diff --git a/app/Http/Controllers/StudentController.php b/app/Http/Controllers/StudentController.php index 64cf6f0..b4a04d9 100644 --- a/app/Http/Controllers/StudentController.php +++ b/app/Http/Controllers/StudentController.php @@ -75,6 +75,7 @@ class StudentController extends Controller $subQuery->where('students.full_name', 'like', "%{$search}%") ->orWhere('students.student_number', 'like', "%{$search}%") ->orWhere('students.nisn', 'like', "%{$search}%") + ->orWhere('students.nik', 'like', "%{$search}%") ->orWhere('students.phone_number', 'like', "%{$search}%"); }); }) @@ -161,6 +162,7 @@ class StudentController extends Controller $subQuery->where('students.full_name', 'like', "%{$search}%") ->orWhere('students.student_number', 'like', "%{$search}%") ->orWhere('students.nisn', 'like', "%{$search}%") + ->orWhere('students.nik', 'like', "%{$search}%") ->orWhere('students.phone_number', 'like', "%{$search}%"); }); }) @@ -276,9 +278,14 @@ class StudentController extends Controller public function edit(Student $student): View { - return view('students.edit', array_merge($this->formData(), [ - 'student' => $student, - ])); + $student->loadMissing('classRoom'); + + return view('students.edit', array_merge( + $this->formData($student), + [ + 'student' => $student, + ] + )); } public function update(Request $request, Student $student): RedirectResponse @@ -395,14 +402,34 @@ class StudentController extends Controller $payload = Arr::only($row['normalized'], [ 'student_number', 'nisn', + 'nik', 'full_name', 'gender', + 'place_of_birth', + 'date_of_birth', + 'religion', + 'nationality', + 'family_card_number', + 'child_order', + 'number_of_siblings', + 'phone_number', + 'email', + 'address', + 'province_id', + 'regency_id', + 'district_id', + 'village_id', + 'province', + 'city', + 'district', + 'village', + 'postal_code', 'academic_year_id', 'department_id', 'class_id', 'entry_date', 'status', - 'phone_number', + 'notes', ]); $lookup = null; @@ -422,12 +449,14 @@ class StudentController extends Controller $lookup->update($payload); $this->syncAcademicHistory($lookup, $payload); + $this->syncImportedParents($lookup, $row['normalized']['parents'] ?? []); $updated++; continue; } $student = Student::create($payload); $this->syncAcademicHistory($student, $payload); + $this->syncImportedParents($student, $row['normalized']['parents'] ?? []); $inserted++; } }); @@ -441,7 +470,7 @@ class StudentController extends Controller ->with('success', "Import data siswa selesai. {$inserted} data baru ditambahkan, {$updated} data diperbarui."); } - public function downloadImportTemplate(): Response + public function downloadImportTemplate() { $this->ensureAdmin(); @@ -452,32 +481,112 @@ class StudentController extends Controller $headers = [ 'NIS', 'NISN', + 'NIK', 'Nama Lengkap', 'Jenis Kelamin (L/P)', + 'Tempat Lahir', + 'Tanggal Lahir (YYYY-MM-DD)', + 'Agama', + 'Kewarganegaraan', + 'No. Kartu Keluarga', + 'Anak Ke', + 'Jumlah Saudara', + 'Nomor HP', + 'Email', + 'Alamat', + 'Provinsi', + 'Kabupaten/Kota', + 'Kecamatan', + 'Kelurahan/Desa', + 'Kode Pos', 'Tahun Ajaran', 'Program Keahlian', 'Kelas', 'Tanggal Masuk (YYYY-MM-DD)', 'Status', - 'Nomor HP', + 'Catatan', + 'Nama Ayah', + 'NIK Ayah', + 'Tahun Lahir Ayah', + 'Pendidikan Ayah', + 'Pekerjaan Ayah', + 'No. HP Ayah', + 'Alamat Ayah', + 'Status Hidup Ayah', + 'Nama Ibu', + 'NIK Ibu', + 'Tahun Lahir Ibu', + 'Pendidikan Ibu', + 'Pekerjaan Ibu', + 'No. HP Ibu', + 'Alamat Ibu', + 'Status Hidup Ibu', + 'Nama Wali', + 'NIK Wali', + 'Tahun Lahir Wali', + 'Pendidikan Wali', + 'Pekerjaan Wali', + 'No. HP Wali', + 'Alamat Wali', + 'Status Hidup Wali', ]; $sheet->fromArray($headers, null, 'A1'); $sheet->fromArray([ '2026-001', '9988776655', + '3512345678901234', 'Contoh Nama Siswa', 'L', + 'Malang', + '2010-01-15', + 'Islam', + 'Indonesia', + '3512345678901235', + '1', + '2', + '081234567890', + 'contoh@siswa.local', + 'Jl. Contoh No. 1', + 'Jawa Timur', + 'Kota Malang', + 'Klojen', + 'Kauman', + '65119', AcademicYear::orderBy('name')->value('name') ?? '2026/2027', Department::orderBy('name')->value('name') ?? 'DKV', SchoolClass::orderBy('name')->value('name') ?? 'X DKV 1', '2026-07-15', 'active', - '081234567890', + 'Catatan opsional', + 'Budi Santoso', + '3512345678901111', + '1980', + 'SMA', + 'Wiraswasta', + '081200000001', + 'Jl. Ayah No. 1', + 'Ya', + 'Siti Aminah', + '3512345678902222', + '1982', + 'SMA', + 'Ibu Rumah Tangga', + '081200000002', + 'Jl. Ibu No. 1', + 'Ya', + 'Ahmad Wali', + '3512345678903333', + '1975', + 'S1', + 'Pegawai Swasta', + '081200000003', + 'Jl. Wali No. 1', + 'Ya', ], null, 'A2'); - foreach (range('A', 'J') as $column) { - $sheet->getColumnDimension($column)->setAutoSize(true); + for ($columnIndex = 1; $columnIndex <= count($headers); $columnIndex++) { + $sheet->getColumnDimensionByColumn($columnIndex)->setAutoSize(true); } $referenceSheet = $spreadsheet->createSheet(); @@ -502,14 +611,54 @@ class StudentController extends Controller ['Kolom', 'Keterangan'], ['NIS', 'Boleh kosong, tapi idealnya diisi untuk identitas lokal sekolah.'], ['NISN', 'Boleh kosong jika belum ada.'], + ['NIK', 'Opsional, tetapi jika diisi harus unik.'], ['Nama Lengkap', 'Wajib diisi.'], ['Jenis Kelamin (L/P)', 'Isi L atau P.'], + ['Tempat Lahir', 'Opsional.'], + ['Tanggal Lahir (YYYY-MM-DD)', 'Format contoh: 2010-01-15.'], + ['Agama', 'Opsional, contoh: Islam, Kristen, Katolik, Hindu, Buddha, Konghucu.'], + ['Kewarganegaraan', 'Opsional, default aman: Indonesia.'], + ['No. Kartu Keluarga', 'Opsional.'], + ['Anak Ke', 'Opsional, angka bulat minimal 1.'], + ['Jumlah Saudara', 'Opsional, angka bulat minimal 0.'], + ['Nomor HP', 'Opsional.'], + ['Email', 'Opsional, harus format email valid bila diisi.'], + ['Alamat', 'Opsional.'], + ['Provinsi', 'Opsional, isi nama persis seperti data master wilayah.'], + ['Kabupaten/Kota', 'Opsional, isi nama persis seperti data master wilayah.'], + ['Kecamatan', 'Opsional, isi nama persis seperti data master wilayah.'], + ['Kelurahan/Desa', 'Opsional, isi nama persis seperti data master wilayah.'], + ['Kode Pos', 'Opsional.'], ['Tahun Ajaran', 'Isi dengan nama tahun ajaran persis seperti sheet Referensi.'], ['Program Keahlian', 'Isi dengan nama jurusan persis seperti sheet Referensi.'], ['Kelas', 'Isi dengan nama kelas persis seperti sheet Referensi.'], ['Tanggal Masuk (YYYY-MM-DD)', 'Format contoh: 2026-07-15.'], ['Status', 'Gunakan: active, inactive, transferred, graduated, dropped.'], - ['Nomor HP', 'Opsional.'], + ['Catatan', 'Opsional.'], + ['Nama Ayah', 'Opsional.'], + ['NIK Ayah', 'Opsional.'], + ['Tahun Lahir Ayah', 'Opsional, angka 4 digit.'], + ['Pendidikan Ayah', 'Opsional.'], + ['Pekerjaan Ayah', 'Opsional.'], + ['No. HP Ayah', 'Opsional.'], + ['Alamat Ayah', 'Opsional.'], + ['Status Hidup Ayah', 'Opsional, isi Ya/Tidak atau Hidup/Meninggal.'], + ['Nama Ibu', 'Opsional.'], + ['NIK Ibu', 'Opsional.'], + ['Tahun Lahir Ibu', 'Opsional, angka 4 digit.'], + ['Pendidikan Ibu', 'Opsional.'], + ['Pekerjaan Ibu', 'Opsional.'], + ['No. HP Ibu', 'Opsional.'], + ['Alamat Ibu', 'Opsional.'], + ['Status Hidup Ibu', 'Opsional, isi Ya/Tidak atau Hidup/Meninggal.'], + ['Nama Wali', 'Opsional.'], + ['NIK Wali', 'Opsional.'], + ['Tahun Lahir Wali', 'Opsional, angka 4 digit.'], + ['Pendidikan Wali', 'Opsional.'], + ['Pekerjaan Wali', 'Opsional.'], + ['No. HP Wali', 'Opsional.'], + ['Alamat Wali', 'Opsional.'], + ['Status Hidup Wali', 'Opsional, isi Ya/Tidak atau Hidup/Meninggal.'], ], null, 'A1'); foreach (range('A', 'B') as $column) { $guideSheet->getColumnDimension($column)->setAutoSize(true); @@ -526,11 +675,26 @@ class StudentController extends Controller ]); } + public function downloadImportExample() + { + $this->ensureAdmin(); + + $path = storage_path('app/private/student-import-example.xlsx'); + + abort_unless(file_exists($path), 404, 'File contoh import siswa belum tersedia.'); + + return response()->file($path, [ + 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition' => 'attachment; filename="contoh-import-siswa.xlsx"', + ]); + } + protected function validateStudent(Request $request, ?Student $student = null): array { $data = $request->validate([ 'student_number' => ['nullable', 'string', 'max:30', Rule::unique('students', 'student_number')->ignore($student?->id)], 'nisn' => ['nullable', 'string', 'max:30', Rule::unique('students', 'nisn')->ignore($student?->id)], + 'nik' => ['nullable', 'string', 'max:30', Rule::unique('students', 'nik')->ignore($student?->id)], 'full_name' => ['required', 'string', 'max:150'], 'gender' => ['required', Rule::in(['L', 'P'])], 'place_of_birth' => ['nullable', 'string', 'max:100'], @@ -598,16 +762,22 @@ class StudentController extends Controller return $data; } - protected function formData(): array + protected function formData(?Student $student = null): array { + $classOptionsQuery = SchoolClass::query() + ->when( + $student?->class_id, + fn ($query) => $query->where('is_active', true)->orWhere('id', $student->class_id), + fn ($query) => $query->where('is_active', true) + ) + ->orderBy('name'); + return [ 'academicYears' => AcademicYear::where('is_active', true)->orderBy('name')->pluck('name', 'id'), 'departments' => Department::where('is_active', true)->orderBy('name')->pluck('name', 'id'), 'classes' => SchoolClass::where('is_active', true)->orderBy('name')->pluck('name', 'id'), 'provinces' => Province::orderBy('name')->pluck('name', 'id'), - 'classOptions' => SchoolClass::query() - ->where('is_active', true) - ->orderBy('name') + 'classOptions' => $classOptionsQuery ->get(['id', 'name', 'department_id', 'academic_year_id']) ->map(fn ($classRoom) => [ 'id' => $classRoom->id, @@ -825,6 +995,48 @@ class StudentController extends Controller $graduationRecord->ijazah_file_size = $file->getSize(); } + if (request()->hasFile('transcript_file')) { + if ($graduationRecord->transcript_file_path && Storage::disk('public')->exists($graduationRecord->transcript_file_path)) { + Storage::disk('public')->delete($graduationRecord->transcript_file_path); + } + + $file = request()->file('transcript_file'); + $path = $file->store("student-graduation-documents/{$student->id}/transcript", 'public'); + + $graduationRecord->transcript_file_name = $file->getClientOriginalName(); + $graduationRecord->transcript_file_path = $path; + $graduationRecord->transcript_mime_type = $file->getMimeType(); + $graduationRecord->transcript_file_size = $file->getSize(); + } + + if (request()->hasFile('ukk_certificate_file')) { + if ($graduationRecord->ukk_certificate_file_path && Storage::disk('public')->exists($graduationRecord->ukk_certificate_file_path)) { + Storage::disk('public')->delete($graduationRecord->ukk_certificate_file_path); + } + + $file = request()->file('ukk_certificate_file'); + $path = $file->store("student-graduation-documents/{$student->id}/ukk-certificate", 'public'); + + $graduationRecord->ukk_certificate_file_name = $file->getClientOriginalName(); + $graduationRecord->ukk_certificate_file_path = $path; + $graduationRecord->ukk_certificate_mime_type = $file->getMimeType(); + $graduationRecord->ukk_certificate_file_size = $file->getSize(); + } + + if (request()->hasFile('lsp_certificate_file')) { + if ($graduationRecord->lsp_certificate_file_path && Storage::disk('public')->exists($graduationRecord->lsp_certificate_file_path)) { + Storage::disk('public')->delete($graduationRecord->lsp_certificate_file_path); + } + + $file = request()->file('lsp_certificate_file'); + $path = $file->store("student-graduation-documents/{$student->id}/lsp-certificate", 'public'); + + $graduationRecord->lsp_certificate_file_name = $file->getClientOriginalName(); + $graduationRecord->lsp_certificate_file_path = $path; + $graduationRecord->lsp_certificate_mime_type = $file->getMimeType(); + $graduationRecord->lsp_certificate_file_size = $file->getSize(); + } + $graduationRecord->save(); return; @@ -838,14 +1050,54 @@ class StudentController extends Controller return [ 'student_number' => ['nullable', 'string', 'max:30'], 'nisn' => ['nullable', 'string', 'max:30'], + 'nik' => ['nullable', 'string', 'max:30'], 'full_name' => ['required', 'string', 'max:150'], 'gender' => ['required', Rule::in(['L', 'P'])], + 'place_of_birth' => ['nullable', 'string', 'max:100'], + 'date_of_birth' => ['nullable', 'date'], + 'religion' => ['nullable', 'string', 'max:50'], + 'nationality' => ['nullable', 'string', 'max:50'], + 'family_card_number' => ['nullable', 'string', 'max:30'], + 'child_order' => ['nullable', 'integer', 'min:1'], + 'number_of_siblings' => ['nullable', 'integer', 'min:0'], + 'phone_number' => ['nullable', 'string', 'max:30'], + 'email' => ['nullable', 'email', 'max:100'], + 'address' => ['nullable', 'string'], + 'province_id' => ['nullable', 'string', 'exists:provinces,id'], + 'regency_id' => ['nullable', 'string', 'exists:regencies,id'], + 'district_id' => ['nullable', 'string', 'exists:districts,id'], + 'village_id' => ['nullable', 'string', 'exists:villages,id'], + 'postal_code' => ['nullable', 'string', 'max:10'], 'academic_year_id' => ['required', 'integer', Rule::exists('academic_years', 'id')->where(fn ($query) => $query->where('is_active', true))], 'department_id' => ['required', 'integer', Rule::exists('departments', 'id')->where(fn ($query) => $query->where('is_active', true))], 'class_id' => ['required', 'integer', Rule::exists('classes', 'id')->where(fn ($query) => $query->where('is_active', true))], 'entry_date' => ['nullable', 'date'], 'status' => ['required', Rule::in(array_keys($this->statusOptions()))], - 'phone_number' => ['nullable', 'string', 'max:30'], + 'notes' => ['nullable', 'string'], + 'parents.father.name' => ['nullable', 'string', 'max:150'], + 'parents.father.nik' => ['nullable', 'string', 'max:30'], + 'parents.father.birth_year' => ['nullable', 'integer', 'min:1900', 'max:' . now()->year], + 'parents.father.education' => ['nullable', 'string', 'max:100'], + 'parents.father.occupation' => ['nullable', 'string', 'max:100'], + 'parents.father.phone_number' => ['nullable', 'string', 'max:30'], + 'parents.father.address' => ['nullable', 'string'], + 'parents.father.is_alive' => ['nullable', 'boolean'], + 'parents.mother.name' => ['nullable', 'string', 'max:150'], + 'parents.mother.nik' => ['nullable', 'string', 'max:30'], + 'parents.mother.birth_year' => ['nullable', 'integer', 'min:1900', 'max:' . now()->year], + 'parents.mother.education' => ['nullable', 'string', 'max:100'], + 'parents.mother.occupation' => ['nullable', 'string', 'max:100'], + 'parents.mother.phone_number' => ['nullable', 'string', 'max:30'], + 'parents.mother.address' => ['nullable', 'string'], + 'parents.mother.is_alive' => ['nullable', 'boolean'], + 'parents.guardian.name' => ['nullable', 'string', 'max:150'], + 'parents.guardian.nik' => ['nullable', 'string', 'max:30'], + 'parents.guardian.birth_year' => ['nullable', 'integer', 'min:1900', 'max:' . now()->year], + 'parents.guardian.education' => ['nullable', 'string', 'max:100'], + 'parents.guardian.occupation' => ['nullable', 'string', 'max:100'], + 'parents.guardian.phone_number' => ['nullable', 'string', 'max:30'], + 'parents.guardian.address' => ['nullable', 'string'], + 'parents.guardian.is_alive' => ['nullable', 'boolean'], ]; } @@ -854,14 +1106,54 @@ class StudentController extends Controller return [ 'student_number' => 'NIS', 'nisn' => 'NISN', + 'nik' => 'NIK', 'full_name' => 'nama lengkap', 'gender' => 'jenis kelamin', + 'place_of_birth' => 'tempat lahir', + 'date_of_birth' => 'tanggal lahir', + 'religion' => 'agama', + 'nationality' => 'kewarganegaraan', + 'family_card_number' => 'nomor kartu keluarga', + 'child_order' => 'anak ke', + 'number_of_siblings' => 'jumlah saudara', + 'phone_number' => 'nomor HP', + 'email' => 'email', + 'address' => 'alamat', + 'province_id' => 'provinsi', + 'regency_id' => 'kabupaten/kota', + 'district_id' => 'kecamatan', + 'village_id' => 'kelurahan/desa', + 'postal_code' => 'kode pos', 'academic_year_id' => 'tahun ajaran', 'department_id' => 'program keahlian', 'class_id' => 'kelas', 'entry_date' => 'tanggal masuk', 'status' => 'status', - 'phone_number' => 'nomor HP', + 'notes' => 'catatan', + 'parents.father.name' => 'nama ayah', + 'parents.father.nik' => 'NIK ayah', + 'parents.father.birth_year' => 'tahun lahir ayah', + 'parents.father.education' => 'pendidikan ayah', + 'parents.father.occupation' => 'pekerjaan ayah', + 'parents.father.phone_number' => 'nomor HP ayah', + 'parents.father.address' => 'alamat ayah', + 'parents.father.is_alive' => 'status hidup ayah', + 'parents.mother.name' => 'nama ibu', + 'parents.mother.nik' => 'NIK ibu', + 'parents.mother.birth_year' => 'tahun lahir ibu', + 'parents.mother.education' => 'pendidikan ibu', + 'parents.mother.occupation' => 'pekerjaan ibu', + 'parents.mother.phone_number' => 'nomor HP ibu', + 'parents.mother.address' => 'alamat ibu', + 'parents.mother.is_alive' => 'status hidup ibu', + 'parents.guardian.name' => 'nama wali', + 'parents.guardian.nik' => 'NIK wali', + 'parents.guardian.birth_year' => 'tahun lahir wali', + 'parents.guardian.education' => 'pendidikan wali', + 'parents.guardian.occupation' => 'pekerjaan wali', + 'parents.guardian.phone_number' => 'nomor HP wali', + 'parents.guardian.address' => 'alamat wali', + 'parents.guardian.is_alive' => 'status hidup wali', ]; } @@ -968,30 +1260,75 @@ class StudentController extends Controller $studentNumber = $this->nullIfBlank($normalized['student_number'] ?? ($normalized['nis'] ?? null)); $nisn = $this->nullIfBlank($normalized['nisn'] ?? null); + $nik = $this->nullIfBlank($normalized['nik'] ?? null); $fullName = $this->nullIfBlank($normalized['full_name'] ?? ($normalized['nama_lengkap'] ?? null)); $gender = strtoupper((string) $this->nullIfBlank($normalized['gender'] ?? ($normalized['jenis_kelamin_l_p'] ?? null))); - $status = strtolower((string) ($this->nullIfBlank($normalized['status'] ?? null) ?? 'active')); - $entryDate = $this->nullIfBlank($normalized['entry_date'] ?? ($normalized['tanggal_masuk_yyyy_mm_dd'] ?? null)); + $placeOfBirth = $this->nullIfBlank($normalized['place_of_birth'] ?? ($normalized['tempat_lahir'] ?? null)); + $dateOfBirth = $this->normalizeImportDate($normalized['date_of_birth'] ?? ($normalized['tanggal_lahir_yyyy_mm_dd'] ?? null)); + $religion = $this->nullIfBlank($normalized['religion'] ?? ($normalized['agama'] ?? null)); + $nationality = $this->nullIfBlank($normalized['nationality'] ?? ($normalized['kewarganegaraan'] ?? null)); + $familyCardNumber = $this->nullIfBlank($normalized['family_card_number'] ?? ($normalized['no_kartu_keluarga'] ?? null)); + $childOrder = $this->normalizeImportInteger($normalized['child_order'] ?? ($normalized['anak_ke'] ?? null)); + $numberOfSiblings = $this->normalizeImportInteger($normalized['number_of_siblings'] ?? ($normalized['jumlah_saudara'] ?? null)); $phoneNumber = $this->nullIfBlank($normalized['phone_number'] ?? ($normalized['nomor_hp'] ?? null)); + $email = $this->nullIfBlank($normalized['email'] ?? null); + $address = $this->nullIfBlank($normalized['address'] ?? ($normalized['alamat'] ?? null)); + $postalCode = $this->nullIfBlank($normalized['postal_code'] ?? ($normalized['kode_pos'] ?? null)); + $status = strtolower((string) ($this->nullIfBlank($normalized['status'] ?? null) ?? 'active')); + $entryDate = $this->normalizeImportDate($normalized['entry_date'] ?? ($normalized['tanggal_masuk_yyyy_mm_dd'] ?? null)); + $notes = $this->nullIfBlank($normalized['notes'] ?? ($normalized['catatan'] ?? null)); $academicYearSource = $this->nullIfBlank($normalized['academic_year_id'] ?? ($normalized['tahun_ajaran'] ?? null)); $departmentSource = $this->nullIfBlank($normalized['department_id'] ?? ($normalized['program_keahlian'] ?? null)); $classSource = $this->nullIfBlank($normalized['class_id'] ?? ($normalized['kelas'] ?? null)); + $provinceSource = $this->nullIfBlank($normalized['province_id'] ?? ($normalized['provinsi'] ?? null)); + $regencySource = $this->nullIfBlank($normalized['regency_id'] ?? ($normalized['kabupaten_kota'] ?? null)); + $districtSource = $this->nullIfBlank($normalized['district_id'] ?? ($normalized['kecamatan'] ?? null)); + $villageSource = $this->nullIfBlank($normalized['village_id'] ?? ($normalized['kelurahan_desa'] ?? null)); $academicYearId = $this->resolveAcademicYearId($academicYearSource); $departmentId = $this->resolveDepartmentId($departmentSource); $classId = $this->resolveClassId($classSource, $academicYearId, $departmentId); + $provinceId = $this->resolveProvinceId($provinceSource); + $regencyId = $this->resolveRegencyId($regencySource, $provinceId); + $districtId = $this->resolveDistrictId($districtSource, $regencyId); + $villageId = $this->resolveVillageId($villageSource, $districtId); return [ 'student_number' => $studentNumber, 'nisn' => $nisn, + 'nik' => $nik, 'full_name' => $fullName, 'gender' => $gender, + 'place_of_birth' => $placeOfBirth, + 'date_of_birth' => $dateOfBirth, + 'religion' => $religion, + 'nationality' => $nationality, + 'family_card_number' => $familyCardNumber, + 'child_order' => $childOrder, + 'number_of_siblings' => $numberOfSiblings, + 'phone_number' => $phoneNumber, + 'email' => $email, + 'address' => $address, + 'province_id' => $provinceId, + 'regency_id' => $regencyId, + 'district_id' => $districtId, + 'village_id' => $villageId, + 'province' => $provinceId ? Province::where('id', $provinceId)->value('name') : null, + 'city' => $regencyId ? Regency::where('id', $regencyId)->value('name') : null, + 'district' => $districtId ? District::where('id', $districtId)->value('name') : null, + 'village' => $villageId ? Village::where('id', $villageId)->value('name') : null, + 'postal_code' => $postalCode, 'academic_year_id' => $academicYearId, 'department_id' => $departmentId, 'class_id' => $classId, 'entry_date' => $entryDate, 'status' => $status, - 'phone_number' => $phoneNumber, + 'notes' => $notes, + 'parents' => [ + 'father' => $this->normalizeImportParentData($normalized, 'father', 'ayah'), + 'mother' => $this->normalizeImportParentData($normalized, 'mother', 'ibu'), + 'guardian' => $this->normalizeImportParentData($normalized, 'guardian', 'wali'), + ], ]; } @@ -1038,6 +1375,181 @@ class StudentController extends Controller ->value('id'); } + protected function resolveProvinceId(mixed $value): mixed + { + if ($value === null || $value === '') { + return null; + } + + if (is_numeric($value)) { + return (string) $value; + } + + return Province::where('name', trim((string) $value))->value('id'); + } + + protected function resolveRegencyId(mixed $value, mixed $provinceId = null): mixed + { + if ($value === null || $value === '') { + return null; + } + + if (is_numeric($value)) { + return (string) $value; + } + + return Regency::query() + ->when($provinceId, fn ($query) => $query->where('province_id', $provinceId)) + ->where('name', trim((string) $value)) + ->value('id'); + } + + protected function resolveDistrictId(mixed $value, mixed $regencyId = null): mixed + { + if ($value === null || $value === '') { + return null; + } + + if (is_numeric($value)) { + return (string) $value; + } + + return District::query() + ->when($regencyId, fn ($query) => $query->where('regency_id', $regencyId)) + ->where('name', trim((string) $value)) + ->value('id'); + } + + protected function resolveVillageId(mixed $value, mixed $districtId = null): mixed + { + if ($value === null || $value === '') { + return null; + } + + if (is_numeric($value)) { + return (string) $value; + } + + return Village::query() + ->when($districtId, fn ($query) => $query->where('district_id', $districtId)) + ->where('name', trim((string) $value)) + ->value('id'); + } + + protected function normalizeImportDate(mixed $value): mixed + { + $value = $this->nullIfBlank($value); + + if ($value === null) { + return null; + } + + if (is_numeric($value)) { + try { + return \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject((float) $value)->format('Y-m-d'); + } catch (\Throwable) { + return $value; + } + } + + return $value; + } + + protected function normalizeImportInteger(mixed $value): mixed + { + $value = $this->nullIfBlank($value); + + if ($value === null) { + return null; + } + + if (is_numeric($value)) { + return (int) $value; + } + + return $value; + } + + protected function normalizeImportBoolean(mixed $value): ?bool + { + $value = $this->nullIfBlank($value); + + if ($value === null) { + return null; + } + + if (is_bool($value)) { + return $value; + } + + $normalized = strtolower(trim((string) $value)); + + return match ($normalized) { + '1', 'true', 'ya', 'y', 'yes', 'hidup', 'masih hidup' => true, + '0', 'false', 'tidak', 't', 'no', 'meninggal', 'wafat' => false, + default => null, + }; + } + + protected function normalizeImportParentData(array $normalized, string $relationKey, string $legacyPrefix): array + { + return [ + 'name' => $this->nullIfBlank($normalized["parent_{$relationKey}_name"] ?? ($normalized["nama_{$legacyPrefix}"] ?? null)), + 'nik' => $this->nullIfBlank($normalized["parent_{$relationKey}_nik"] ?? ($normalized["nik_{$legacyPrefix}"] ?? null)), + 'birth_year' => $this->normalizeImportInteger($normalized["parent_{$relationKey}_birth_year"] ?? ($normalized["tahun_lahir_{$legacyPrefix}"] ?? null)), + 'education' => $this->nullIfBlank($normalized["parent_{$relationKey}_education"] ?? ($normalized["pendidikan_{$legacyPrefix}"] ?? null)), + 'occupation' => $this->nullIfBlank($normalized["parent_{$relationKey}_occupation"] ?? ($normalized["pekerjaan_{$legacyPrefix}"] ?? null)), + 'phone_number' => $this->nullIfBlank($normalized["parent_{$relationKey}_phone_number"] ?? ($normalized["no_hp_{$legacyPrefix}"] ?? null)), + 'address' => $this->nullIfBlank($normalized["parent_{$relationKey}_address"] ?? ($normalized["alamat_{$legacyPrefix}"] ?? null)), + 'is_alive' => $this->normalizeImportBoolean($normalized["parent_{$relationKey}_is_alive"] ?? ($normalized["status_hidup_{$legacyPrefix}"] ?? null)), + ]; + } + + protected function syncImportedParents(Student $student, array $parents): void + { + foreach (['father', 'mother', 'guardian'] as $relationType) { + $parentData = $parents[$relationType] ?? []; + $hasMeaningfulValue = collect($parentData) + ->except('is_alive') + ->filter(fn ($value) => filled($value)) + ->isNotEmpty(); + + if (! $hasMeaningfulValue && ! array_key_exists('is_alive', $parentData)) { + continue; + } + + $existingParent = $student->parents()->where('relation_type', $relationType)->first(); + + if (! $hasMeaningfulValue) { + if ($existingParent) { + $existingParent->delete(); + } + + continue; + } + + $payload = [ + 'name' => $parentData['name'] ?? '-', + 'nik' => $parentData['nik'] ?? null, + 'birth_year' => $parentData['birth_year'] ?? null, + 'education' => $parentData['education'] ?? null, + 'occupation' => $parentData['occupation'] ?? null, + 'phone_number' => $parentData['phone_number'] ?? null, + 'address' => $parentData['address'] ?? null, + 'is_alive' => $parentData['is_alive'] ?? false, + ]; + + if ($existingParent) { + $existingParent->update($payload); + } else { + $student->parents()->create([ + 'relation_type' => $relationType, + ...$payload, + ]); + } + } + } + protected function parseImportFile(string $path, string $originalName): array { $extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); diff --git a/app/Http/Controllers/StudentExitController.php b/app/Http/Controllers/StudentExitController.php index 086069d..d73e078 100644 --- a/app/Http/Controllers/StudentExitController.php +++ b/app/Http/Controllers/StudentExitController.php @@ -21,7 +21,7 @@ class StudentExitController extends Controller $departmentId = $request->get('department_id'); $classId = $request->get('class_id'); - $studentExits = StudentExit::query() + $studentExitsQuery = StudentExit::query() ->with(['academicYear', 'department', 'classRoom']) ->when($search !== '', function ($query) use ($search) { $query->where(function ($subQuery) use ($search) { @@ -32,12 +32,25 @@ class StudentExitController extends Controller }) ->when($academicYearId, fn ($query) => $query->where('academic_year_id', $academicYearId)) ->when($departmentId, fn ($query) => $query->where('department_id', $departmentId)) - ->when($classId, fn ($query) => $query->where('class_id', $classId)) + ->when($classId, fn ($query) => $query->where('class_id', $classId)); + + $studentExits = (clone $studentExitsQuery) ->latest('exit_date') ->latest('id') ->paginate(10) ->withQueryString(); + $totalExits = StudentExit::count(); + $withLetterCount = StudentExit::query()->whereNotNull('exit_letter_file_path')->count(); + $withoutLetterCount = max($totalExits - $withLetterCount, 0); + $thisYearCount = StudentExit::query()->whereYear('exit_date', now()->year)->count(); + $latestExitDate = StudentExit::query()->max('exit_date'); + $recentExits = StudentExit::query() + ->latest('exit_date') + ->latest('id') + ->limit(3) + ->get(['id', 'full_name', 'exit_date', 'department_id', 'class_id']); + return view('student-exits.index', [ 'studentExits' => $studentExits, 'search' => $search, @@ -49,6 +62,15 @@ class StudentExitController extends Controller 'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'), 'departments' => Department::orderBy('name')->pluck('name', 'id'), 'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'), + 'summary' => [ + 'total' => $totalExits, + 'with_letter' => $withLetterCount, + 'without_letter' => $withoutLetterCount, + 'this_year' => $thisYearCount, + 'latest_exit_date' => $latestExitDate, + 'filtered' => $studentExits->total(), + ], + 'recentExits' => $recentExits, ]); } diff --git a/app/Http/Controllers/StudentSelfServiceController.php b/app/Http/Controllers/StudentSelfServiceController.php index b90e1d7..10557fd 100644 --- a/app/Http/Controllers/StudentSelfServiceController.php +++ b/app/Http/Controllers/StudentSelfServiceController.php @@ -158,8 +158,12 @@ class StudentSelfServiceController extends Controller protected function generateUniqueCode(): string { + $characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + do { - $code = strtoupper(Str::random(8)); + $code = collect(range(1, 4)) + ->map(fn () => $characters[random_int(0, strlen($characters) - 1)]) + ->implode(''); } while (Student::where('self_service_access_code', $code)->exists()); return $code; diff --git a/app/Http/Controllers/StudentTransferController.php b/app/Http/Controllers/StudentTransferController.php index 7482fbc..23632a5 100644 --- a/app/Http/Controllers/StudentTransferController.php +++ b/app/Http/Controllers/StudentTransferController.php @@ -21,7 +21,7 @@ class StudentTransferController extends Controller $departmentId = $request->get('department_id'); $classId = $request->get('class_id'); - $studentTransfers = StudentTransfer::query() + $studentTransfersQuery = StudentTransfer::query() ->with(['academicYear', 'department', 'classRoom']) ->when($search !== '', function ($query) use ($search) { $query->where(function ($subQuery) use ($search) { @@ -32,12 +32,25 @@ class StudentTransferController extends Controller }) ->when($academicYearId, fn ($query) => $query->where('academic_year_id', $academicYearId)) ->when($departmentId, fn ($query) => $query->where('department_id', $departmentId)) - ->when($classId, fn ($query) => $query->where('class_id', $classId)) + ->when($classId, fn ($query) => $query->where('class_id', $classId)); + + $studentTransfers = (clone $studentTransfersQuery) ->latest('transfer_date') ->latest('id') ->paginate(10) ->withQueryString(); + $totalTransfers = StudentTransfer::count(); + $withLetterCount = StudentTransfer::query()->whereNotNull('transfer_letter_file_path')->count(); + $withoutLetterCount = max($totalTransfers - $withLetterCount, 0); + $thisYearCount = StudentTransfer::query()->whereYear('transfer_date', now()->year)->count(); + $latestTransferDate = StudentTransfer::query()->max('transfer_date'); + $recentTransfers = StudentTransfer::query() + ->latest('transfer_date') + ->latest('id') + ->limit(3) + ->get(['id', 'full_name', 'transfer_date', 'department_id', 'class_id']); + return view('student-transfers.index', [ 'studentTransfers' => $studentTransfers, 'search' => $search, @@ -49,6 +62,15 @@ class StudentTransferController extends Controller 'academicYears' => AcademicYear::orderBy('name')->pluck('name', 'id'), 'departments' => Department::orderBy('name')->pluck('name', 'id'), 'classes' => SchoolClass::orderBy('name')->pluck('name', 'id'), + 'summary' => [ + 'total' => $totalTransfers, + 'with_letter' => $withLetterCount, + 'without_letter' => $withoutLetterCount, + 'this_year' => $thisYearCount, + 'latest_transfer_date' => $latestTransferDate, + 'filtered' => $studentTransfers->total(), + ], + 'recentTransfers' => $recentTransfers, ]); } diff --git a/app/Http/Controllers/SyncTokenController.php b/app/Http/Controllers/SyncTokenController.php index 7b00717..0a4c379 100644 --- a/app/Http/Controllers/SyncTokenController.php +++ b/app/Http/Controllers/SyncTokenController.php @@ -44,14 +44,10 @@ class SyncTokenController extends Controller public function revoke(SyncToken $syncToken): RedirectResponse { - if (! $syncToken->revoked_at) { - $syncToken->update([ - 'revoked_at' => now(), - ]); - } + $syncToken->delete(); return redirect() ->route('settings.sync-tokens.index') - ->with('status', 'Token sinkronisasi berhasil dinonaktifkan.'); + ->with('status', 'Token sinkronisasi berhasil dihapus.'); } } diff --git a/app/Models/Student.php b/app/Models/Student.php index 2e253ef..479acce 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -37,6 +37,7 @@ class Student extends Model protected $fillable = [ 'student_number', 'nisn', + 'nik', 'full_name', 'gender', 'place_of_birth', diff --git a/app/Models/StudentGraduation.php b/app/Models/StudentGraduation.php index d69045c..9a30c78 100644 --- a/app/Models/StudentGraduation.php +++ b/app/Models/StudentGraduation.php @@ -29,6 +29,18 @@ class StudentGraduation extends Model 'ijazah_file_path', 'ijazah_mime_type', 'ijazah_file_size', + 'transcript_file_name', + 'transcript_file_path', + 'transcript_mime_type', + 'transcript_file_size', + 'ukk_certificate_file_name', + 'ukk_certificate_file_path', + 'ukk_certificate_mime_type', + 'ukk_certificate_file_size', + 'lsp_certificate_file_name', + 'lsp_certificate_file_path', + 'lsp_certificate_mime_type', + 'lsp_certificate_file_size', 'status', ]; diff --git a/database/migrations/2026_04_29_202100_add_transcript_and_skhun_to_student_graduations_table.php b/database/migrations/2026_04_29_202100_add_transcript_and_skhun_to_student_graduations_table.php new file mode 100644 index 0000000..b2bf17d --- /dev/null +++ b/database/migrations/2026_04_29_202100_add_transcript_and_skhun_to_student_graduations_table.php @@ -0,0 +1,38 @@ +string('transcript_file_name')->nullable()->after('ijazah_file_size'); + $table->string('transcript_file_path')->nullable()->after('transcript_file_name'); + $table->string('transcript_mime_type')->nullable()->after('transcript_file_path'); + $table->unsignedBigInteger('transcript_file_size')->nullable()->after('transcript_mime_type'); + $table->string('skhun_file_name')->nullable()->after('transcript_file_size'); + $table->string('skhun_file_path')->nullable()->after('skhun_file_name'); + $table->string('skhun_mime_type')->nullable()->after('skhun_file_path'); + $table->unsignedBigInteger('skhun_file_size')->nullable()->after('skhun_mime_type'); + }); + } + + public function down(): void + { + Schema::table('student_graduations', function (Blueprint $table) { + $table->dropColumn([ + 'transcript_file_name', + 'transcript_file_path', + 'transcript_mime_type', + 'transcript_file_size', + 'skhun_file_name', + 'skhun_file_path', + 'skhun_mime_type', + 'skhun_file_size', + ]); + }); + } +}; diff --git a/database/migrations/2026_04_29_202600_add_ukk_and_lsp_certificates_to_student_graduations_table.php b/database/migrations/2026_04_29_202600_add_ukk_and_lsp_certificates_to_student_graduations_table.php new file mode 100644 index 0000000..75ab79f --- /dev/null +++ b/database/migrations/2026_04_29_202600_add_ukk_and_lsp_certificates_to_student_graduations_table.php @@ -0,0 +1,38 @@ +string('ukk_certificate_file_name')->nullable()->after('skhun_file_size'); + $table->string('ukk_certificate_file_path')->nullable()->after('ukk_certificate_file_name'); + $table->string('ukk_certificate_mime_type')->nullable()->after('ukk_certificate_file_path'); + $table->unsignedBigInteger('ukk_certificate_file_size')->nullable()->after('ukk_certificate_mime_type'); + $table->string('lsp_certificate_file_name')->nullable()->after('ukk_certificate_file_size'); + $table->string('lsp_certificate_file_path')->nullable()->after('lsp_certificate_file_name'); + $table->string('lsp_certificate_mime_type')->nullable()->after('lsp_certificate_file_path'); + $table->unsignedBigInteger('lsp_certificate_file_size')->nullable()->after('lsp_certificate_mime_type'); + }); + } + + public function down(): void + { + Schema::table('student_graduations', function (Blueprint $table) { + $table->dropColumn([ + 'ukk_certificate_file_name', + 'ukk_certificate_file_path', + 'ukk_certificate_mime_type', + 'ukk_certificate_file_size', + 'lsp_certificate_file_name', + 'lsp_certificate_file_path', + 'lsp_certificate_mime_type', + 'lsp_certificate_file_size', + ]); + }); + } +}; diff --git a/database/migrations/2026_04_29_223900_drop_skhun_columns_from_student_graduations_table.php b/database/migrations/2026_04_29_223900_drop_skhun_columns_from_student_graduations_table.php new file mode 100644 index 0000000..d0af313 --- /dev/null +++ b/database/migrations/2026_04_29_223900_drop_skhun_columns_from_student_graduations_table.php @@ -0,0 +1,30 @@ +dropColumn([ + 'skhun_file_name', + 'skhun_file_path', + 'skhun_mime_type', + 'skhun_file_size', + ]); + }); + } + + public function down(): void + { + Schema::table('student_graduations', function (Blueprint $table) { + $table->string('skhun_file_name')->nullable()->after('transcript_file_size'); + $table->string('skhun_file_path')->nullable()->after('skhun_file_name'); + $table->string('skhun_mime_type')->nullable()->after('skhun_file_path'); + $table->unsignedBigInteger('skhun_file_size')->nullable()->after('skhun_mime_type'); + }); + } +}; diff --git a/database/migrations/2026_04_29_234500_add_nik_to_students_table.php b/database/migrations/2026_04_29_234500_add_nik_to_students_table.php new file mode 100644 index 0000000..4ac56f6 --- /dev/null +++ b/database/migrations/2026_04_29_234500_add_nik_to_students_table.php @@ -0,0 +1,22 @@ +string('nik', 30)->nullable()->after('nisn'); + }); + } + + public function down(): void + { + Schema::table('students', function (Blueprint $table) { + $table->dropColumn('nik'); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index bffac96..543e6d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@fortawesome/fontawesome-free": "^7.2.0", "sweetalert2": "^11.26.24" }, "devDependencies": { @@ -67,6 +68,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", + "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", diff --git a/package.json b/package.json index 4759f08..f67cfe1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "vite": "^8.0.0" }, "dependencies": { + "@fortawesome/fontawesome-free": "^7.2.0", "sweetalert2": "^11.26.24" } } diff --git a/resources/css/app.css b/resources/css/app.css index b5c61c9..90cc5b8 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,3 +1,5 @@ +@import '@fortawesome/fontawesome-free/css/all.min.css'; + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/resources/js/app.js b/resources/js/app.js index 92b0f75..e0911aa 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -365,6 +365,82 @@ document.addEventListener('DOMContentLoaded', () => { confirmText: 'Ya, hapus semua', }); + const bulkUploadInput = document.querySelector('[data-bulk-upload-input]'); + const bulkUploadDropzone = document.querySelector('[data-bulk-upload-dropzone]'); + const bulkUploadDropzoneInner = document.querySelector('[data-bulk-upload-dropzone-inner]'); + const bulkUploadSummary = document.querySelector('[data-bulk-upload-summary]'); + const bulkUploadFileList = document.querySelector('[data-bulk-upload-file-list]'); + + if (bulkUploadInput && bulkUploadDropzone && bulkUploadDropzoneInner) { + const renderBulkUploadFiles = () => { + const files = Array.from(bulkUploadInput.files || []); + + if (!bulkUploadSummary || !bulkUploadFileList) { + return; + } + + if (files.length === 0) { + bulkUploadSummary.classList.add('hidden'); + bulkUploadFileList.classList.add('hidden'); + bulkUploadFileList.innerHTML = ''; + return; + } + + const totalBytes = files.reduce((sum, file) => sum + (file.size || 0), 0); + const totalMb = (totalBytes / (1024 * 1024)).toFixed(2); + + bulkUploadSummary.classList.remove('hidden'); + bulkUploadSummary.innerHTML = `
${files.length} file siap dipreview
Total ukuran sementara ${totalMb} MB. Batas upload bulk saat ini maksimal 4 MB per file.
`; + + bulkUploadFileList.classList.remove('hidden'); + bulkUploadFileList.innerHTML = ` +
+
File terpilih
+
Menampilkan ${Math.min(files.length, 8)} dari ${files.length} file
+
+
+ ${files.slice(0, 8).map((file) => ` +
+
+
${file.name}
+
${(file.size / 1024 / 1024).toFixed(2)} MB
+
+ ${(file.name.split('.').pop() || 'FILE').toUpperCase()} +
+ `).join('')} + ${files.length > 8 ? `
+${files.length - 8} file lainnya
` : ''} +
+ `; + }; + + ['dragenter', 'dragover'].forEach((eventName) => { + bulkUploadDropzone.addEventListener(eventName, (event) => { + event.preventDefault(); + bulkUploadDropzone.classList.add('border-indigo-400', 'bg-indigo-50/40'); + bulkUploadDropzoneInner.classList.add('border-indigo-300', 'bg-indigo-50/50'); + }); + }); + + ['dragleave', 'drop'].forEach((eventName) => { + bulkUploadDropzone.addEventListener(eventName, (event) => { + event.preventDefault(); + bulkUploadDropzone.classList.remove('border-indigo-400', 'bg-indigo-50/40'); + bulkUploadDropzoneInner.classList.remove('border-indigo-300', 'bg-indigo-50/50'); + }); + }); + + bulkUploadDropzone.addEventListener('drop', (event) => { + const files = event.dataTransfer?.files; + if (files && files.length > 0) { + bulkUploadInput.files = files; + renderBulkUploadFiles(); + } + }); + + bulkUploadInput.addEventListener('change', renderBulkUploadFiles); + renderBulkUploadFiles(); + } + const promotionForm = document.querySelector('[data-student-promotion-form]'); if (promotionForm) { diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index b46aeb5..fb6fbad 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -49,65 +49,53 @@
-
+

Total Siswa

{{ $stats['students'] }}
-
- - - - +
+

Jumlah seluruh siswa yang tercatat di sistem saat ini.

-
+

Siswa Keluar

{{ $stats['studentsDropped'] }}
-
- - - - +
+

Jumlah siswa pada arsip siswa keluar.

-
+

Siswa Mutasi

{{ $stats['studentsTransferred'] }}
-
- - - - +
+

Jumlah siswa pada arsip siswa mutasi.

-
+

Siswa Lulus

{{ $stats['studentsGraduated'] }}
-
- - - - +
+

Jumlah siswa yang sudah berstatus lulus.

@@ -118,24 +106,49 @@
-
- Tahun Angkatan +
+
+ + + + Tahun Angkatan +
{{ $stats['academicYears'] }}
-
- Program Keahlian +
+
+ + + + Program Keahlian +
{{ $stats['departments'] }}
-
- Kelas +
+
+ + + + Kelas +
{{ $stats['classes'] }}
-
- Jenis Dokumen +
+
+ + + + Jenis Dokumen +
{{ $stats['documentTypes'] }}
-
- Data Prestasi +
+
+ + + + Data Prestasi +
{{ $stats['studentAchievements'] }}
@@ -143,76 +156,66 @@
-
+

Data Siswa

{{ $stats['students'] }}

-
- - - +
+

Total siswa aktif yang tersimpan di sistem.

-
+

Dokumen Wajib Lengkap

{{ $stats['studentsCompleteDocuments'] }}

-
- - - +
+

Siswa yang sudah upload seluruh dokumen wajib.

-
+

Belum Lengkap

{{ $stats['studentsIncompleteDocuments'] }}

-
- - - +
+

Siswa yang masih kurang upload dokumen wajib.

-
+

Belum Tervalidasi Penuh

{{ $stats['studentsUnverifiedDocuments'] }}

-
- - - +
+

Siswa yang dokumen wajibnya belum valid semua.

-
+

Siswa Berprestasi

{{ $stats['studentsWithAchievements'] }}

-
- - - +
+

Jumlah siswa yang sudah memiliki minimal satu data prestasi.

@@ -222,55 +225,105 @@
-
- Auth & Login - Aktif +
+
+ + + + Auth & Login +
+ Aktif
-
- Master Data - Aktif +
+
+ + + + Master Data +
+ Aktif
-
- Data Siswa - Aktif +
+
+ + + + Data Siswa +
+ Aktif
-
- Dokumen & Verifikasi - Aktif +
+
+ + + + Dokumen & Verifikasi +
+ Aktif
-
- Histori Akademik - Siap +
+
+ + + + Histori Akademik +
+ Siap
-
- Promosi Siswa - Siap +
+
+ + + + Promosi Siswa +
+ Siap
-
- Siswa Berprestasi - Siap +
+
+ + + + Siswa Berprestasi +
+ Siap
@@ -278,19 +331,33 @@
@forelse ($studentGenderByAcademicYear as $item) -
+
-
{{ $item['name'] }}
-
Total {{ $item['total'] }}
-
-
-
-
Laki-laki
-
{{ $item['male'] }}
+
+ + + +
+
{{ $item['name'] }}
+
Distribusi siswa per gender
+
-
-
Perempuan
-
{{ $item['female'] }}
+
Total {{ $item['total'] }}
+
+
+
+
+ + Laki-laki +
+
{{ $item['male'] }}
+
+
+
+ + Perempuan +
+
{{ $item['female'] }}
diff --git a/resources/views/graduated-students/bulk-upload.blade.php b/resources/views/graduated-students/bulk-upload.blade.php index 62e34be..ae9a573 100644 --- a/resources/views/graduated-students/bulk-upload.blade.php +++ b/resources/views/graduated-students/bulk-upload.blade.php @@ -1,132 +1,273 @@ - +
- -
-

Aturan nama file yang disarankan

-
    -
  • Jika memilih pencocokan NIS, nama file harus berupa NIS siswa. Contoh: 2024001.pdf
  • -
  • Jika memilih pencocokan NISN, nama file harus berupa NISN siswa. Contoh: 9988776655.pdf
  • -
  • Upload dipisah per jenis dokumen: jalankan batch SKL dan Ijazah secara terpisah agar lebih aman.
  • -
  • Untuk meminimalkan error, gunakan maksimal 10 file per batch dengan ukuran maksimal 2 MB per file.
  • -
+
+
+
+
+
+ + Arsip Kelulusan +
+

Bulk Upload Dokumen Kelulusan

+

+ Upload banyak dokumen sekaligus untuk siswa lulus dengan pencocokan otomatis berbasis NIS atau NISN, supaya proses arsip jauh lebih cepat dan minim input manual. +

+
+
+
Batas Batch
+
10
+
file per upload
+
+
-
- @csrf - -
-
- - - -
- -
- - - +
+
+
+
+
Maksimal File
+
10
+
+
+ +
- -
- - -

Format yang didukung: PDF, JPG, JPEG, PNG. Maksimal 2 MB per file dan 10 file per batch upload.

- - +
+
+
+
Ukuran per File
+
4 MB
+
+
+ +
+
- -
- - - Kembali ke Data Siswa Lulus - +
+
+
+
Format
+
PDF, JPG, PNG
+
+
+ +
+
- - +
+
+ +
+ +
+
+
+
+ +
+
+

Aturan nama file yang disarankan

+
    +
  • Jika memilih pencocokan NIS, nama file harus berupa NIS siswa. Contoh: 2024001.pdf
  • +
  • Jika memilih pencocokan NISN, nama file harus berupa NISN siswa. Contoh: 9988776655.pdf
  • +
  • Upload dipisah per jenis dokumen, misalnya batch khusus SKL, Ijazah, Transkrip Nilai, Sertifikat UKK, atau Sertifikat LSP.
  • +
  • Gunakan maksimal 10 file per batch dengan ukuran maksimal 4 MB per file agar konsisten dengan upload manual.
  • +
+
+
+
+ +
+ @csrf + +
+
+ + +

Tiap batch sebaiknya hanya berisi satu jenis dokumen yang sama.

+ +
+ +
+ + +

Pastikan nama file konsisten dengan identitas yang dipilih.

+ +
+
+ +
+ +
+
+
+ +
+
Tarik file ke area ini atau pilih manual
+
Bisa pilih banyak file sekaligus untuk satu jenis dokumen yang sama, dengan batas maksimal 4 MB per file.
+
+ +
+ Maks. 10 file + Maks. 4 MB / file + PDF, JPG, JPEG, PNG +
+ + +
+ + +
+ +
+ + + + Kembali ke Data Siswa Lulus + +
+
+
+
+ + +
+
+
+
+
+
Samakan nama file dengan identitas siswa
+
Gunakan NIS atau NISN sesuai pilihan pencocokan. Hindari tambahan teks di nama file.
+
+
+
+
+
+
+
+
Pisahkan per jenis dokumen
+
Jangan campur SKL, ijazah, dan transkrip dalam satu batch agar mapping tetap bersih.
+
+
+
+
+
+
+
+
Perhatikan file yang akan replace
+
Preview akan memberi tanda jika dokumen lama akan diganti, jadi operator masih bisa cek sebelum proses permanen.
+
+
+
+
+
+
@if ($preview)
-
+
Jenis Dokumen
{{ $preview['document_label'] }}
-
+
Pencocokan
{{ $preview['match_by_label'] }}
-
+
Total File
{{ $preview['summary']['total_files'] }}
-
+
Cocok
{{ $preview['summary']['matched'] }}
-
+
Akan Replace
{{ $preview['summary']['will_replace'] }}
-
- - - - - - - - - - - @foreach ($preview['rows'] as $row) +
+
+
+
+
Detail Hasil Mapping
+
Review file yang cocok, file baru, dan file yang akan menggantikan dokumen lama.
+
+
+ + {{ count($preview['rows']) }} baris preview +
+
+
+
+
Nama FileKey TerdeteksiSiswa LulusStatus
+ - - - - + + + + - @endforeach - -
{{ $row['original_name'] }}{{ $row['match_key'] ?: '-' }} - @if ($row['graduation_id']) -
{{ $row['student_name'] }}
-
{{ $preview['match_by_label'] }}: {{ $row['identifier_value'] ?: '-' }}
- @else - Tidak ditemukan - @endif -
- @if ($row['status'] === 'matched') -
- Siap Diproses - @if ($row['will_replace']) - Replace Dokumen Lama - @endif -
- @else - Tidak Cocok - @endif -
Nama FileKey TerdeteksiSiswa LulusStatus
+ + + @foreach ($preview['rows'] as $row) + + +
{{ $row['original_name'] }}
+ + + {{ $row['match_key'] ?: '-' }} + + + @if ($row['graduation_id']) +
{{ $row['student_name'] }}
+
{{ $preview['match_by_label'] }}: {{ $row['identifier_value'] ?: '-' }}
+ @else + Tidak ditemukan + @endif + + + @if ($row['status'] === 'matched') +
+ Siap Diproses + @if ($row['will_replace']) + Replace Dokumen Lama + @else + Dokumen Baru + @endif +
+ @else + Tidak Cocok + @endif + + + @endforeach + + +
@csrf -
diff --git a/resources/views/graduated-students/index.blade.php b/resources/views/graduated-students/index.blade.php index 3ea64fc..acafc3e 100644 --- a/resources/views/graduated-students/index.blade.php +++ b/resources/views/graduated-students/index.blade.php @@ -16,68 +16,108 @@ }; @endphp -
-
-
+
+
+
-
- Arsip Kelulusan -
-

Data Siswa Lulus

-

- Modul arsip khusus lulusan untuk menelusuri identitas akhir siswa, akademik terakhir, tanggal kelulusan, serta kelengkapan dokumen SKL dan ijazah. -

+
Total Arsip Lulus
+
{{ $summary['total'] }}
-
-
Total Lulusan
-
{{ $summary['total'] ?? $graduations->total() }}
-
Arsip siswa lulus
+
+
-
-
Kelengkapan Dokumen
-
-
-
SKL tersedia
-
{{ $summary['with_skl'] ?? 0 }} arsip
+
+
+
+
SKL Tersedia
+
{{ $summary['with_skl'] }}
-
-
Ijazah tersedia
-
{{ $summary['with_ijazah'] ?? 0 }} arsip
+
+ +
+
+
+ +
+
+
+
Ijazah Tersedia
+
{{ $summary['with_ijazah'] }}
+
+
+ +
+
+
+ +
+
+
+
Lulus Tahun Ini
+
{{ $summary['this_year'] }}
+
+
+ +
+
+
+ +
+
+
+
Hasil Filter
+
{{ $summary['filtered'] }}
+
{{ $summary['latest_graduation_date'] ? 'Update terakhir ' . \Illuminate\Support\Carbon::parse($summary['latest_graduation_date'])->format('d M Y') : 'Belum ada tanggal lulus' }}
+
+
+
- -
- - - - + +
+ + + + + + - - + + -
- - Reset -
- +
+ + Reset + Bulk Upload +
+ +
-
-
-
-
Daftar Arsip Siswa Lulus
-
Fokus pada identitas lulusan, akademik terakhir, tanggal lulus, dan dokumen kelulusan.
-
-
- Bulk Upload SKL / Ijazah -
- Menampilkan {{ $graduations->firstItem() ?? 0 }} sampai {{ $graduations->lastItem() ?? 0 }} dari {{ $graduations->total() }} arsip lulusan. +
+
+
+
+
Daftar Arsip Siswa Lulus
+
Fokus pada identitas lulusan, akademik terakhir, tanggal lulus, dan dokumen kelulusan.
+
+
+
+ + {{ $graduations->total() }} data terfilter +
+ @if ($search || collect($filters)->filter()->isNotEmpty()) +
+ + Filter aktif +
+ @endif
@@ -101,7 +141,7 @@ @forelse ($graduations as $graduation) - +
@@ -121,31 +161,65 @@
Lulus
Tanggal lulus: {{ optional($graduation->graduation_date)->format('d-m-Y') ?: '-' }}
+
+
+
Progress Dokumen
+
{{ $graduation->documents_available }}/{{ $graduation->documents_total }}
+
+
+
+
+
{{ $graduation->documents_progress_percentage }}% lengkap, {{ $graduation->documents_missing }} dokumen belum ada.
+
-
-
- SKL - @unless ($graduation->skl_file_path) - Belum ada - @endunless - @if ($graduation->skl_file_path) - Preview - @endif -
-
- Ijazah - @unless ($graduation->ijazah_file_path) - Belum ada - @endunless - @if ($graduation->ijazah_file_path) - Preview - @endif -
+ @php + $documentGroups = [ + [ + 'title' => 'Akademik', + 'badgeClass' => 'bg-sky-100 text-sky-700', + 'items' => [ + ['label' => 'SKL', 'path' => 'skl_file_path', 'route' => 'skl', 'icon' => 'fa-solid fa-file-lines'], + ['label' => 'Ijazah', 'path' => 'ijazah_file_path', 'route' => 'ijazah', 'icon' => 'fa-solid fa-graduation-cap'], + ['label' => 'Transkrip', 'path' => 'transcript_file_path', 'route' => 'transcript', 'icon' => 'fa-solid fa-table-list'], + ], + ], + [ + 'title' => 'Sertifikasi', + 'badgeClass' => 'bg-violet-100 text-violet-700', + 'items' => [ + ['label' => 'Sertifikat UKK', 'path' => 'ukk_certificate_file_path', 'route' => 'ukk-certificate', 'icon' => 'fa-solid fa-award'], + ['label' => 'Sertifikat LSP', 'path' => 'lsp_certificate_file_path', 'route' => 'lsp-certificate', 'icon' => 'fa-solid fa-certificate'], + ], + ], + ]; + @endphp +
+ @foreach ($documentGroups as $group) +
+
+
{{ $group['title'] }}
+ {{ $group['title'] }} +
+
+ @foreach ($group['items'] as $item) + @php $hasFile = filled($graduation->{$item['path']}); @endphp +
+ {{ $item['label'] }} + @if ($hasFile) + Preview + @else + Belum ada + @endif +
+ @endforeach +
+
+ @endforeach
- Detail Arsip + Detail Arsip @empty @@ -158,12 +232,7 @@
-
-
- Arsip lulusan dipisahkan agar pengelolaan dokumen kelulusan lebih rapi dan tidak bercampur dengan data siswa aktif. -
-
- {{ $graduations->onEachSide(1)->links() }} -
+
+ {{ $graduations->onEachSide(1)->links() }}
diff --git a/resources/views/graduated-students/preview-document.blade.php b/resources/views/graduated-students/preview-document.blade.php index 3f0692c..9e26f9e 100644 --- a/resources/views/graduated-students/preview-document.blade.php +++ b/resources/views/graduated-students/preview-document.blade.php @@ -1,43 +1,173 @@ -
- -
-
Nama Siswa: {{ $graduation->full_name }}
-
Jenis Dokumen: {{ $documentLabel }}
-
Nama File: {{ $fileName ?: '-' }}
-
Tipe MIME: {{ $mimeType ?: '-' }}
-
Ukuran: {{ ($documentType === 'skl' ? $graduation->skl_file_size : $graduation->ijazah_file_size) ? number_format((($documentType === 'skl' ? $graduation->skl_file_size : $graduation->ijazah_file_size) / 1024), 1) . ' KB' : '-' }}
-
Tanggal Lulus: {{ $graduation->graduation_date?->format('d-m-Y') ?: '-' }}
- @if ($graduation->graduation_note) -
- Catatan Kelulusan: {{ $graduation->graduation_note }} -
- @endif +
+
+
+
+
Nama Siswa
+
{{ $graduation->full_name }}
+
+
+ +
+
+
+ +
+
+
+
Format File
+
{{ strtoupper(pathinfo($fileName ?: '', PATHINFO_EXTENSION) ?: 'FILE') }}
+
+
+ +
+
+
+ +
+
+
+
Ukuran File
+
{{ match($documentType) { + 'skl' => $graduation->skl_file_size, + 'ijazah' => $graduation->ijazah_file_size, + 'transcript' => $graduation->transcript_file_size, + 'ukk-certificate' => $graduation->ukk_certificate_file_size, + 'lsp-certificate' => $graduation->lsp_certificate_file_size, + default => null, + } ? number_format((match($documentType) { + 'skl' => $graduation->skl_file_size, + 'ijazah' => $graduation->ijazah_file_size, + 'transcript' => $graduation->transcript_file_size, + 'ukk-certificate' => $graduation->ukk_certificate_file_size, + 'lsp-certificate' => $graduation->lsp_certificate_file_size, + default => null, + } / 1024), 1) . ' KB' : '-' }}
+
+
+ +
+
+
+ +
+
+
+
Tanggal Lulus
+
{{ $graduation->graduation_date?->format('d M Y') ?: 'Belum diisi' }}
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
Metadata Dokumen
+
Informasi cepat sebelum file dibuka atau diunduh
+
+
+ +
+
+
Nama Siswa
+
{{ $graduation->full_name }}
+
+
+
Jenis Dokumen
+
{{ $documentLabel }}
+
+
+
Nama File
+
{{ $fileName ?: '-' }}
+
+
+
Tipe MIME
+
{{ $mimeType ?: '-' }}
+
+
+
Ukuran File
+
{{ match($documentType) { + 'skl' => $graduation->skl_file_size, + 'ijazah' => $graduation->ijazah_file_size, + 'transcript' => $graduation->transcript_file_size, + 'ukk-certificate' => $graduation->ukk_certificate_file_size, + 'lsp-certificate' => $graduation->lsp_certificate_file_size, + default => null, + } ? number_format((match($documentType) { + 'skl' => $graduation->skl_file_size, + 'ijazah' => $graduation->ijazah_file_size, + 'transcript' => $graduation->transcript_file_size, + 'ukk-certificate' => $graduation->ukk_certificate_file_size, + 'lsp-certificate' => $graduation->lsp_certificate_file_size, + default => null, + } / 1024), 1) . ' KB' : '-' }}
+
+
+
Tanggal Lulus
+
{{ $graduation->graduation_date?->format('d-m-Y') ?: '-' }}
+
+
+
Catatan Kelulusan
+
{{ $graduation->graduation_note ?: '-' }}
+
-
- - @if ($isImage) -
- {{ $fileName ?: $documentLabel }} + +
+
+
Mode Preview
+
+ @if ($isImage) + Gambar langsung di browser + @elseif ($isPdf) + PDF embed di browser + @else + Preview tidak didukung + @endif
- @elseif ($isPdf) -
- +
+
+ +
+
+ + @if ($isImage) +
+ {{ $fileName ?: $documentLabel }} +
+ @elseif ($isPdf) +
+ +
+ @else +
+
+
- @else -
-
Preview tidak tersedia
-

Format file ini belum didukung untuk preview langsung di browser. Silakan gunakan tombol buka file asli.

-
- @endif - -
+
Preview tidak tersedia
+

Format file ini belum didukung untuk preview langsung di browser. Gunakan tombol buka file asli untuk melihat dokumen.

+
+ @endif +
diff --git a/resources/views/graduated-students/show.blade.php b/resources/views/graduated-students/show.blade.php index 7607795..5d94998 100644 --- a/resources/views/graduated-students/show.blade.php +++ b/resources/views/graduated-students/show.blade.php @@ -1,97 +1,274 @@ - -
-
+ + @php + $documentGroups = [ + [ + 'title' => 'Dokumen Akademik Utama', + 'badge' => 'Akademik', + 'badgeClass' => 'bg-sky-100 text-sky-700', + 'items' => [ + ['label' => 'SKL', 'path' => 'skl_file_path', 'name' => 'skl_file_name', 'route' => 'skl', 'icon' => 'fa-solid fa-file-lines'], + ['label' => 'Ijazah', 'path' => 'ijazah_file_path', 'name' => 'ijazah_file_name', 'route' => 'ijazah', 'icon' => 'fa-solid fa-graduation-cap'], + ['label' => 'Transkrip Nilai', 'path' => 'transcript_file_path', 'name' => 'transcript_file_name', 'route' => 'transcript', 'icon' => 'fa-solid fa-table-list'], + ], + ], + [ + 'title' => 'Dokumen Sertifikasi', + 'badge' => 'Sertifikasi', + 'badgeClass' => 'bg-violet-100 text-violet-700', + 'items' => [ + ['label' => 'Sertifikat UKK', 'path' => 'ukk_certificate_file_path', 'name' => 'ukk_certificate_file_name', 'route' => 'ukk-certificate', 'icon' => 'fa-solid fa-award'], + ['label' => 'Sertifikat LSP', 'path' => 'lsp_certificate_file_path', 'name' => 'lsp_certificate_file_name', 'route' => 'lsp-certificate', 'icon' => 'fa-solid fa-certificate'], + ], + ], + ]; + + $allDocuments = collect($documentGroups)->flatMap(fn ($group) => $group['items']); + $availableDocuments = $allDocuments->filter(fn ($item) => filled($graduation->{$item['path']}))->count(); + $totalDocuments = $allDocuments->count(); + $missingDocuments = max($totalDocuments - $availableDocuments, 0); + @endphp + +
+
+
+
+
Status Arsip
+
Siswa Lulus
+
+
+ +
+
+
+
+
+
+
Tanggal Lulus
+
{{ $graduation->graduation_date?->format('d M Y') ?: 'Belum diisi' }}
+
+
+ +
+
+
+
+
+
+
Kelengkapan Dokumen
+
{{ $availableDocuments }}/{{ $totalDocuments }} tersedia
+
+
+ +
+
+
+
+
+
+
Kelas Terakhir
+
{{ $graduation->classRoom?->name ?: '-' }}
+
+
+ +
+
+
+
+ +
+
-
-
-
Nama Lengkap
-
{{ $graduation->full_name }}
+
+
+
-
Status Arsip
-
Siswa Lulus
+
Snapshot Identitas
+
Identitas dasar siswa saat masuk arsip lulusan
-
-
NIS
-
{{ $graduation->student_number ?: '-' }}
+
+
+
+
Nama Lengkap
+
{{ $graduation->full_name }}
-
-
NISN
-
{{ $graduation->nisn ?: '-' }}
+
+
Status Arsip
+
Siswa Lulus
+
+
+
NIS
+
{{ $graduation->student_number ?: '-' }}
+
+
+
NISN
+
{{ $graduation->nisn ?: '-' }}
-
-
-
Tahun Angkatan
-
{{ $graduation->academicYear?->name ?: '-' }}
+
+
+
-
Program Keahlian
-
{{ $graduation->department?->name ?: '-' }}
+
Posisi Akademik Terakhir
+
Rombel dan relasi akademik sebelum siswa lulus dari sekolah
-
-
Kelas Terakhir
-
{{ $graduation->classRoom?->name ?: '-' }}
+
+
+
+
Tahun Angkatan
+
{{ $graduation->academicYear?->name ?: '-' }}
-
-
Tanggal Masuk
-
{{ $graduation->entry_date?->format('d-m-Y') ?: '-' }}
+
+
Program Keahlian
+
{{ $graduation->department?->name ?: '-' }}
+
+
+
Kelas Terakhir
+
{{ $graduation->classRoom?->name ?: '-' }}
+
+
+
Tanggal Masuk
+
{{ $graduation->entry_date?->format('d-m-Y') ?: '-' }}
- -
- - Bulk Upload SKL / Ijazah - + +
+
+
+ +
+
+
+ {{ $missingDocuments === 0 && $totalDocuments > 0 ? 'Dokumen kelulusan sudah lengkap' : 'Masih ada dokumen yang perlu dilengkapi' }} +
+
+ {{ $availableDocuments }} dari {{ $totalDocuments }} dokumen tersedia{{ $missingDocuments > 0 ? ', sisa ' . $missingDocuments . ' dokumen belum ada.' : '.' }} +
+
+
-
-
-
SKL {{ $graduation->skl_file_path ? 'sudah tersedia' : 'belum diupload' }}
- @if ($graduation->skl_file_name) -
{{ $graduation->skl_file_name }}
- @endif + +
+ @foreach ($documentGroups as $group) +
+
+
+
{{ $group['title'] }}
+
Status arsip dokumen pada kategori ini.
+
+ {{ $group['badge'] }} +
+
+ @foreach ($group['items'] as $item) + @php + $hasFile = filled($graduation->{$item['path']}); + @endphp +
+
+
+
{{ $item['label'] }} {{ $hasFile ? 'sudah tersedia' : 'belum diupload' }}
+ @if ($graduation->{$item['name']}) +
{{ $graduation->{$item['name']} }}
+ @endif +
+ @if ($hasFile) + + + Preview + + @endif +
+
+ @endforeach +
+
+ @endforeach +
+ + + +
+ @csrf + @method('PATCH') + +
+
+ + +
-
-
Ijazah {{ $graduation->ijazah_file_path ? 'sudah tersedia' : 'belum diupload' }}
- @if ($graduation->ijazah_file_name) -
{{ $graduation->ijazah_file_name }}
- @endif +
+
+
+
+
Upload Dokumen Akademik
+
Untuk arsip kelulusan inti dan dokumen nilai.
+
+ Akademik +
+
+
+ + +

Format: PDF, JPG, JPEG, PNG. Maksimal 4 MB.

+ +
+
+ + +

Format: PDF, JPG, JPEG, PNG. Maksimal 4 MB.

+ +
+
+ + +

Format: PDF, JPG, JPEG, PNG. Maksimal 4 MB.

+ +
+
+
+ +
+
+
+
Upload Dokumen Sertifikasi
+
Untuk bukti kompetensi dan sertifikasi lulusan.
+
+ Sertifikasi +
+
+
+ + +

Format: PDF, JPG, JPEG, PNG. Maksimal 4 MB.

+ +
+
+ + +

Format: PDF, JPG, JPEG, PNG. Maksimal 4 MB.

+ +
+
+
- - @csrf - @method('PATCH') - - - - -
- - -

Format: PDF, JPG, JPEG, PNG. Maksimal 4 MB.

- -
- -
- - -

Format: PDF, JPG, JPEG, PNG. Maksimal 4 MB.

- -
- - - -
+
+
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 8a8e58b..c816993 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -24,6 +24,7 @@ @vite(['resources/css/app.css', 'resources/js/app.js']) + @stack('styles') @include('sweetalert::alert') diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 15535c9..c2b1e2e 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -64,11 +64,9 @@