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
+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));