feat: update student import and graduation workflows

This commit is contained in:
2026-04-30 00:04:52 +07:00
parent 189b076ab0
commit ac22f8b7c8
51 changed files with 4547 additions and 1287 deletions
@@ -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,
@@ -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,
@@ -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;
@@ -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);
@@ -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,
@@ -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,
]);
}
+531 -19
View File
@@ -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));
+24 -2
View File
@@ -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,
]);
}
@@ -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;
@@ -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,
]);
}
+2 -6
View File
@@ -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.');
}
}
+1
View File
@@ -37,6 +37,7 @@ class Student extends Model
protected $fillable = [
'student_number',
'nisn',
'nik',
'full_name',
'gender',
'place_of_birth',
+12
View File
@@ -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',
];