Improve document type cleanup and student import reliability
This commit is contained in:
@@ -3,9 +3,11 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\DocumentType;
|
use App\Models\DocumentType;
|
||||||
|
use App\Models\StudentDocument;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
use RealRashid\SweetAlert\Facades\Alert;
|
use RealRashid\SweetAlert\Facades\Alert;
|
||||||
@@ -17,6 +19,9 @@ class DocumentTypeController extends Controller
|
|||||||
$search = trim((string) $request->get('search'));
|
$search = trim((string) $request->get('search'));
|
||||||
|
|
||||||
$documentTypes = DocumentType::withCount('studentDocuments')
|
$documentTypes = DocumentType::withCount('studentDocuments')
|
||||||
|
->withCount([
|
||||||
|
'studentDocuments as student_documents_with_trashed_count' => fn ($query) => $query->withTrashed(),
|
||||||
|
])
|
||||||
->when($search !== '', function ($query) use ($search) {
|
->when($search !== '', function ($query) use ($search) {
|
||||||
$query->where('code', 'like', "%{$search}%")
|
$query->where('code', 'like', "%{$search}%")
|
||||||
->orWhere('name', 'like', "%{$search}%")
|
->orWhere('name', 'like', "%{$search}%")
|
||||||
@@ -93,23 +98,67 @@ class DocumentTypeController extends Controller
|
|||||||
|
|
||||||
public function destroy(DocumentType $documentType): RedirectResponse
|
public function destroy(DocumentType $documentType): RedirectResponse
|
||||||
{
|
{
|
||||||
$documentType->loadCount('studentDocuments');
|
$documentType->loadCount([
|
||||||
|
'studentDocuments',
|
||||||
|
'studentDocuments as student_documents_with_trashed_count' => fn ($query) => $query->withTrashed(),
|
||||||
|
]);
|
||||||
|
|
||||||
if ($documentType->student_documents_count > 0) {
|
if ($documentType->student_documents_count > 0) {
|
||||||
Alert::error('Tidak bisa dihapus', 'Jenis dokumen ini masih digunakan oleh data dokumen siswa.');
|
Alert::error('Tidak bisa dihapus', 'Jenis dokumen ini masih digunakan oleh data dokumen siswa aktif.');
|
||||||
|
|
||||||
return redirect()->route('document-types.index')->withErrors([
|
return redirect()->route('document-types.index')->withErrors([
|
||||||
'delete' => "Jenis dokumen '{$documentType->name}' tidak bisa dihapus karena masih digunakan oleh {$documentType->student_documents_count} dokumen siswa.",
|
'delete' => "Jenis dokumen '{$documentType->name}' tidak bisa dihapus karena masih digunakan oleh {$documentType->student_documents_count} dokumen siswa aktif.",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$trashedDocumentsCount = $documentType->student_documents_with_trashed_count - $documentType->student_documents_count;
|
||||||
|
|
||||||
|
if ($trashedDocumentsCount > 0) {
|
||||||
|
Alert::error('Tidak bisa dihapus', 'Jenis dokumen ini masih dipakai oleh dokumen siswa yang sudah diarsipkan.');
|
||||||
|
|
||||||
|
return redirect()->route('document-types.index')->withErrors([
|
||||||
|
'delete' => "Jenis dokumen '{$documentType->name}' tidak bisa dihapus karena masih direferensikan oleh {$trashedDocumentsCount} dokumen siswa yang sudah diarsipkan. Bersihkan arsip dokumen terkait terlebih dahulu atau gunakan aksi bersihkan arsip dari modul ini.",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->deleteDocumentType($documentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purgeArchivedDocuments(DocumentType $documentType): RedirectResponse
|
||||||
|
{
|
||||||
|
$trashedDocuments = StudentDocument::withTrashed()
|
||||||
|
->where('document_type_id', $documentType->id)
|
||||||
|
->whereNotNull('deleted_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($trashedDocuments->isEmpty()) {
|
||||||
|
Alert::info('Tidak ada arsip', 'Tidak ditemukan dokumen siswa arsip untuk jenis dokumen ini.');
|
||||||
|
|
||||||
|
return $this->deleteDocumentType($documentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($trashedDocuments as $document) {
|
||||||
|
if ($document->file_path && Storage::disk('public')->exists($document->file_path)) {
|
||||||
|
Storage::disk('public')->delete($document->file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$document->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
Alert::success('Arsip dibersihkan', 'Arsip dokumen siswa yang terkait berhasil dibersihkan.');
|
||||||
|
|
||||||
|
return $this->deleteDocumentType($documentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function deleteDocumentType(DocumentType $documentType): RedirectResponse
|
||||||
|
{
|
||||||
try {
|
try {
|
||||||
$documentType->delete();
|
$documentType->delete();
|
||||||
} catch (QueryException $exception) {
|
} catch (QueryException $exception) {
|
||||||
Alert::error('Tidak bisa dihapus', 'Jenis dokumen ini masih direferensikan oleh data lain di sistem.');
|
Alert::error('Tidak bisa dihapus', 'Jenis dokumen ini masih direferensikan oleh data lain di sistem.');
|
||||||
|
|
||||||
return redirect()->route('document-types.index')->withErrors([
|
return redirect()->route('document-types.index')->withErrors([
|
||||||
'delete' => "Jenis dokumen '{$documentType->name}' tidak bisa dihapus karena masih direferensikan oleh data lain di sistem. Untuk kasus ini, cek dokumen siswa yang masih memakai jenis dokumen tersebut terlebih dahulu.",
|
'delete' => "Jenis dokumen '{$documentType->name}' tidak bisa dihapus karena masih direferensikan oleh data lain di sistem. Cek relasi data lain yang masih memakai jenis dokumen ini terlebih dahulu.",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,8 +342,15 @@ class StudentController extends Controller
|
|||||||
{
|
{
|
||||||
$this->ensureAdmin();
|
$this->ensureAdmin();
|
||||||
|
|
||||||
return view('students.import', array_merge($this->importReferenceData(), [
|
$referenceData = $this->importReferenceData();
|
||||||
'preview' => $request->session()->get('student_import_preview'),
|
$preview = $request->session()->get('student_import_preview');
|
||||||
|
|
||||||
|
if ($preview) {
|
||||||
|
$preview = $this->hydrateImportPreviewDisplay($preview, $referenceData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('students.import', array_merge($referenceData, [
|
||||||
|
'preview' => $preview,
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,6 +439,8 @@ class StudentController extends Controller
|
|||||||
'notes',
|
'notes',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$payload = $this->hydrateRegionNamesFromIds($payload);
|
||||||
|
|
||||||
$lookup = null;
|
$lookup = null;
|
||||||
|
|
||||||
if (! empty($payload['student_number'])) {
|
if (! empty($payload['student_number'])) {
|
||||||
@@ -447,8 +456,14 @@ class StudentController extends Controller
|
|||||||
$lookup->restore();
|
$lookup->restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
$lookup->update($payload);
|
$updatePayload = $this->buildStudentImportUpdatePayload($lookup, $payload);
|
||||||
$this->syncAcademicHistory($lookup, $payload);
|
$historyPayload = $this->buildStudentImportHistoryPayload($lookup, $payload);
|
||||||
|
|
||||||
|
$lookup->update($updatePayload);
|
||||||
|
$this->syncAcademicHistory($lookup, $historyPayload);
|
||||||
|
$this->syncExitRecord($lookup, $historyPayload);
|
||||||
|
$this->syncTransferRecord($lookup, $historyPayload);
|
||||||
|
$this->syncGraduationRecord($lookup, $historyPayload);
|
||||||
$this->syncImportedParents($lookup, $row['normalized']['parents'] ?? []);
|
$this->syncImportedParents($lookup, $row['normalized']['parents'] ?? []);
|
||||||
$updated++;
|
$updated++;
|
||||||
continue;
|
continue;
|
||||||
@@ -456,6 +471,9 @@ class StudentController extends Controller
|
|||||||
|
|
||||||
$student = Student::create($payload);
|
$student = Student::create($payload);
|
||||||
$this->syncAcademicHistory($student, $payload);
|
$this->syncAcademicHistory($student, $payload);
|
||||||
|
$this->syncExitRecord($student, $payload);
|
||||||
|
$this->syncTransferRecord($student, $payload);
|
||||||
|
$this->syncGraduationRecord($student, $payload);
|
||||||
$this->syncImportedParents($student, $row['normalized']['parents'] ?? []);
|
$this->syncImportedParents($student, $row['normalized']['parents'] ?? []);
|
||||||
$inserted++;
|
$inserted++;
|
||||||
}
|
}
|
||||||
@@ -804,14 +822,92 @@ class StudentController extends Controller
|
|||||||
|
|
||||||
protected function importReferenceData(): array
|
protected function importReferenceData(): array
|
||||||
{
|
{
|
||||||
|
$academicYears = AcademicYear::where('is_active', true)->orderBy('name')->pluck('name', 'id');
|
||||||
|
$departments = Department::where('is_active', true)->orderBy('name')->pluck('name', 'id');
|
||||||
|
$classes = SchoolClass::with(['academicYear', 'department'])->where('is_active', true)->orderBy('name')->get();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'academicYears' => AcademicYear::where('is_active', true)->orderBy('name')->pluck('name', 'id'),
|
'academicYears' => $academicYears,
|
||||||
'departments' => Department::where('is_active', true)->orderBy('name')->pluck('name', 'id'),
|
'departments' => $departments,
|
||||||
'classes' => SchoolClass::with(['academicYear', 'department'])->where('is_active', true)->orderBy('name')->get(),
|
'classes' => $classes,
|
||||||
'statuses' => $this->statusOptions(),
|
'statuses' => $this->statusOptions(),
|
||||||
|
'academicYearsById' => $academicYears->all(),
|
||||||
|
'departmentsById' => $departments->all(),
|
||||||
|
'classesById' => $classes->keyBy('id')->map(fn ($classRoom) => [
|
||||||
|
'name' => $classRoom->name,
|
||||||
|
'academic_year' => $classRoom->academicYear?->name,
|
||||||
|
'department' => $classRoom->department?->name,
|
||||||
|
])->all(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function hydrateRegionNamesFromIds(array $payload): array
|
||||||
|
{
|
||||||
|
$payload['province'] = ! empty($payload['province_id']) ? Province::where('id', $payload['province_id'])->value('name') : null;
|
||||||
|
$payload['city'] = ! empty($payload['regency_id']) ? Regency::where('id', $payload['regency_id'])->value('name') : null;
|
||||||
|
$payload['district'] = ! empty($payload['district_id']) ? District::where('id', $payload['district_id'])->value('name') : null;
|
||||||
|
$payload['village'] = ! empty($payload['village_id']) ? Village::where('id', $payload['village_id'])->value('name') : null;
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildStudentImportUpdatePayload(Student $student, array $payload): array
|
||||||
|
{
|
||||||
|
$preserveWhenBlank = [
|
||||||
|
'student_number',
|
||||||
|
'nisn',
|
||||||
|
'nik',
|
||||||
|
'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',
|
||||||
|
'entry_date',
|
||||||
|
'notes',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($preserveWhenBlank as $field) {
|
||||||
|
if (array_key_exists($field, $payload) && blank($payload[$field])) {
|
||||||
|
$payload[$field] = $student->{$field};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildStudentImportHistoryPayload(Student $student, array $payload): array
|
||||||
|
{
|
||||||
|
$historyPayload = Arr::only($payload, [
|
||||||
|
'academic_year_id',
|
||||||
|
'department_id',
|
||||||
|
'class_id',
|
||||||
|
'status',
|
||||||
|
'entry_date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (['academic_year_id', 'department_id', 'class_id', 'status', 'entry_date'] as $field) {
|
||||||
|
if (! array_key_exists($field, $historyPayload) || blank($historyPayload[$field])) {
|
||||||
|
$historyPayload[$field] = $student->{$field};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $historyPayload;
|
||||||
|
}
|
||||||
|
|
||||||
protected function statusOptions(): array
|
protected function statusOptions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -1241,6 +1337,40 @@ class StudentController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function hydrateImportPreviewDisplay(array $preview, array $referenceData): array
|
||||||
|
{
|
||||||
|
$academicYearsById = $referenceData['academicYearsById'] ?? [];
|
||||||
|
$departmentsById = $referenceData['departmentsById'] ?? [];
|
||||||
|
$classesById = $referenceData['classesById'] ?? [];
|
||||||
|
$statuses = $referenceData['statuses'] ?? [];
|
||||||
|
|
||||||
|
$preview['rows'] = array_map(function (array $row) use ($academicYearsById, $departmentsById, $classesById, $statuses) {
|
||||||
|
$academicYearId = $row['data']['academic_year_id'] ?? null;
|
||||||
|
$departmentId = $row['data']['department_id'] ?? null;
|
||||||
|
$classId = $row['data']['class_id'] ?? null;
|
||||||
|
$status = $row['data']['status'] ?? null;
|
||||||
|
$classMeta = $classId ? ($classesById[$classId] ?? null) : null;
|
||||||
|
|
||||||
|
$row['display'] = [
|
||||||
|
'academic_year' => $academicYearId ? ($academicYearsById[$academicYearId] ?? 'ID ' . $academicYearId) : '-',
|
||||||
|
'department' => $departmentId ? ($departmentsById[$departmentId] ?? 'ID ' . $departmentId) : '-',
|
||||||
|
'class' => $classMeta['name'] ?? ($classId ? 'ID ' . $classId : '-'),
|
||||||
|
'class_context' => $classMeta
|
||||||
|
? collect([$classMeta['academic_year'] ?? null, $classMeta['department'] ?? null])->filter()->implode(' · ')
|
||||||
|
: null,
|
||||||
|
'status' => $status ? ($statuses[$status] ?? strtoupper((string) $status)) : '-',
|
||||||
|
'province' => $row['data']['province'] ?? '-',
|
||||||
|
'city' => $row['data']['city'] ?? '-',
|
||||||
|
'district' => $row['data']['district'] ?? '-',
|
||||||
|
'village' => $row['data']['village'] ?? '-',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}, $preview['rows'] ?? []);
|
||||||
|
|
||||||
|
return $preview;
|
||||||
|
}
|
||||||
|
|
||||||
protected function normalizeImportRow(array $row): array
|
protected function normalizeImportRow(array $row): array
|
||||||
{
|
{
|
||||||
$normalized = [];
|
$normalized = [];
|
||||||
@@ -1292,6 +1422,10 @@ class StudentController extends Controller
|
|||||||
$regencyId = $this->resolveRegencyId($regencySource, $provinceId);
|
$regencyId = $this->resolveRegencyId($regencySource, $provinceId);
|
||||||
$districtId = $this->resolveDistrictId($districtSource, $regencyId);
|
$districtId = $this->resolveDistrictId($districtSource, $regencyId);
|
||||||
$villageId = $this->resolveVillageId($villageSource, $districtId);
|
$villageId = $this->resolveVillageId($villageSource, $districtId);
|
||||||
|
$provinceName = $provinceId ? Province::where('id', $provinceId)->value('name') : null;
|
||||||
|
$regencyName = $regencyId ? Regency::where('id', $regencyId)->value('name') : null;
|
||||||
|
$districtName = $districtId ? District::where('id', $districtId)->value('name') : null;
|
||||||
|
$villageName = $villageId ? Village::where('id', $villageId)->value('name') : null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'student_number' => $studentNumber,
|
'student_number' => $studentNumber,
|
||||||
@@ -1313,10 +1447,10 @@ class StudentController extends Controller
|
|||||||
'regency_id' => $regencyId,
|
'regency_id' => $regencyId,
|
||||||
'district_id' => $districtId,
|
'district_id' => $districtId,
|
||||||
'village_id' => $villageId,
|
'village_id' => $villageId,
|
||||||
'province' => $provinceId ? Province::where('id', $provinceId)->value('name') : null,
|
'province' => $provinceName,
|
||||||
'city' => $regencyId ? Regency::where('id', $regencyId)->value('name') : null,
|
'city' => $regencyName,
|
||||||
'district' => $districtId ? District::where('id', $districtId)->value('name') : null,
|
'district' => $districtName,
|
||||||
'village' => $villageId ? Village::where('id', $villageId)->value('name') : null,
|
'village' => $villageName,
|
||||||
'postal_code' => $postalCode,
|
'postal_code' => $postalCode,
|
||||||
'academic_year_id' => $academicYearId,
|
'academic_year_id' => $academicYearId,
|
||||||
'department_id' => $departmentId,
|
'department_id' => $departmentId,
|
||||||
@@ -1355,7 +1489,12 @@ class StudentController extends Controller
|
|||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Department::where('name', trim((string) $value))->value('id');
|
$normalizedValue = trim((string) $value);
|
||||||
|
|
||||||
|
return Department::query()
|
||||||
|
->where('name', $normalizedValue)
|
||||||
|
->orWhere('code', strtoupper($normalizedValue))
|
||||||
|
->value('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resolveClassId(mixed $value, mixed $academicYearId = null, mixed $departmentId = null): mixed
|
protected function resolveClassId(mixed $value, mixed $academicYearId = null, mixed $departmentId = null): mixed
|
||||||
@@ -1385,7 +1524,12 @@ class StudentController extends Controller
|
|||||||
return (string) $value;
|
return (string) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Province::where('name', trim((string) $value))->value('id');
|
$normalizedValue = $this->normalizeRegionLookupValue((string) $value);
|
||||||
|
|
||||||
|
return Province::query()
|
||||||
|
->get(['id', 'name'])
|
||||||
|
->first(fn ($province) => $this->normalizeRegionLookupValue($province->name) === $normalizedValue)
|
||||||
|
?->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resolveRegencyId(mixed $value, mixed $provinceId = null): mixed
|
protected function resolveRegencyId(mixed $value, mixed $provinceId = null): mixed
|
||||||
@@ -1398,10 +1542,19 @@ class StudentController extends Controller
|
|||||||
return (string) $value;
|
return (string) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$normalizedValue = $this->normalizeRegionLookupValue((string) $value);
|
||||||
|
|
||||||
return Regency::query()
|
return Regency::query()
|
||||||
->when($provinceId, fn ($query) => $query->where('province_id', $provinceId))
|
->when($provinceId, fn ($query) => $query->where('province_id', $provinceId))
|
||||||
->where('name', trim((string) $value))
|
->get(['id', 'name'])
|
||||||
->value('id');
|
->first(function ($regency) use ($normalizedValue) {
|
||||||
|
$candidate = $this->normalizeRegionLookupValue($regency->name);
|
||||||
|
|
||||||
|
return $candidate === $normalizedValue
|
||||||
|
|| $candidate === 'KABUPATEN ' . $normalizedValue
|
||||||
|
|| $candidate === 'KOTA ' . $normalizedValue;
|
||||||
|
})
|
||||||
|
?->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resolveDistrictId(mixed $value, mixed $regencyId = null): mixed
|
protected function resolveDistrictId(mixed $value, mixed $regencyId = null): mixed
|
||||||
@@ -1414,10 +1567,13 @@ class StudentController extends Controller
|
|||||||
return (string) $value;
|
return (string) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$normalizedValue = $this->normalizeRegionLookupValue((string) $value);
|
||||||
|
|
||||||
return District::query()
|
return District::query()
|
||||||
->when($regencyId, fn ($query) => $query->where('regency_id', $regencyId))
|
->when($regencyId, fn ($query) => $query->where('regency_id', $regencyId))
|
||||||
->where('name', trim((string) $value))
|
->get(['id', 'name'])
|
||||||
->value('id');
|
->first(fn ($district) => $this->normalizeRegionLookupValue($district->name) === $normalizedValue)
|
||||||
|
?->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resolveVillageId(mixed $value, mixed $districtId = null): mixed
|
protected function resolveVillageId(mixed $value, mixed $districtId = null): mixed
|
||||||
@@ -1430,10 +1586,22 @@ class StudentController extends Controller
|
|||||||
return (string) $value;
|
return (string) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$normalizedValue = $this->normalizeRegionLookupValue((string) $value);
|
||||||
|
|
||||||
return Village::query()
|
return Village::query()
|
||||||
->when($districtId, fn ($query) => $query->where('district_id', $districtId))
|
->when($districtId, fn ($query) => $query->where('district_id', $districtId))
|
||||||
->where('name', trim((string) $value))
|
->get(['id', 'name'])
|
||||||
->value('id');
|
->first(fn ($village) => $this->normalizeRegionLookupValue($village->name) === $normalizedValue)
|
||||||
|
?->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeRegionLookupValue(string $value): string
|
||||||
|
{
|
||||||
|
$normalized = strtoupper(trim($value));
|
||||||
|
$normalized = preg_replace('/\s+/', ' ', $normalized) ?? $normalized;
|
||||||
|
$normalized = str_replace(['KAB.', 'KABUPATEN.', 'KOTA.'], ['KABUPATEN', 'KABUPATEN', 'KOTA'], $normalized);
|
||||||
|
|
||||||
|
return trim($normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function normalizeImportDate(mixed $value): mixed
|
protected function normalizeImportDate(mixed $value): mixed
|
||||||
@@ -1463,13 +1631,68 @@ class StudentController extends Controller
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_int($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_float($value)) {
|
||||||
|
return (int) round($value);
|
||||||
|
}
|
||||||
|
|
||||||
if (is_numeric($value)) {
|
if (is_numeric($value)) {
|
||||||
return (int) $value;
|
return (int) round((float) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value)) {
|
||||||
|
$normalized = trim($value);
|
||||||
|
$normalized = str_replace([',', ' '], '', $normalized);
|
||||||
|
|
||||||
|
if (preg_match('/^\d+(?:\.0+)?$/', $normalized) === 1) {
|
||||||
|
return (int) round((float) $normalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function normalizeImportYear(mixed $value): mixed
|
||||||
|
{
|
||||||
|
$value = $this->nullIfBlank($value);
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof \DateTimeInterface) {
|
||||||
|
return (int) $value->format('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value)) {
|
||||||
|
$normalized = trim($value);
|
||||||
|
|
||||||
|
if (preg_match('/^(\d{4})[-\/]/', $normalized, $matches) === 1) {
|
||||||
|
return (int) $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
$numericValue = (float) $value;
|
||||||
|
$intValue = (int) round($numericValue);
|
||||||
|
|
||||||
|
if ($intValue >= 1900 && $intValue <= now()->year) {
|
||||||
|
return $intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (int) \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($numericValue)->format('Y');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return $intValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->normalizeImportInteger($value);
|
||||||
|
}
|
||||||
|
|
||||||
protected function normalizeImportBoolean(mixed $value): ?bool
|
protected function normalizeImportBoolean(mixed $value): ?bool
|
||||||
{
|
{
|
||||||
$value = $this->nullIfBlank($value);
|
$value = $this->nullIfBlank($value);
|
||||||
@@ -1496,7 +1719,7 @@ class StudentController extends Controller
|
|||||||
return [
|
return [
|
||||||
'name' => $this->nullIfBlank($normalized["parent_{$relationKey}_name"] ?? ($normalized["nama_{$legacyPrefix}"] ?? null)),
|
'name' => $this->nullIfBlank($normalized["parent_{$relationKey}_name"] ?? ($normalized["nama_{$legacyPrefix}"] ?? null)),
|
||||||
'nik' => $this->nullIfBlank($normalized["parent_{$relationKey}_nik"] ?? ($normalized["nik_{$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)),
|
'birth_year' => $this->normalizeImportYear($normalized["parent_{$relationKey}_birth_year"] ?? ($normalized["tahun_lahir_{$legacyPrefix}"] ?? null)),
|
||||||
'education' => $this->nullIfBlank($normalized["parent_{$relationKey}_education"] ?? ($normalized["pendidikan_{$legacyPrefix}"] ?? null)),
|
'education' => $this->nullIfBlank($normalized["parent_{$relationKey}_education"] ?? ($normalized["pendidikan_{$legacyPrefix}"] ?? null)),
|
||||||
'occupation' => $this->nullIfBlank($normalized["parent_{$relationKey}_occupation"] ?? ($normalized["pekerjaan_{$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)),
|
'phone_number' => $this->nullIfBlank($normalized["parent_{$relationKey}_phone_number"] ?? ($normalized["no_hp_{$legacyPrefix}"] ?? null)),
|
||||||
@@ -1662,7 +1885,13 @@ class StudentController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
ksort($values);
|
ksort($values);
|
||||||
$ordered = array_values($values);
|
|
||||||
|
$maxIndex = max(array_keys($values));
|
||||||
|
$ordered = [];
|
||||||
|
|
||||||
|
for ($index = 0; $index <= $maxIndex; $index++) {
|
||||||
|
$ordered[$index] = $values[$index] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
if ($headers === null) {
|
if ($headers === null) {
|
||||||
$headers = array_map(fn ($header) => trim((string) $header), $ordered);
|
$headers = array_map(fn ($header) => trim((string) $header), $ordered);
|
||||||
|
|||||||
+8
-4
@@ -14,15 +14,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const entity = form.dataset.entity || 'data ini';
|
const entity = form.dataset.entity || 'data ini';
|
||||||
|
const title = form.dataset.confirmTitle || 'Hapus data?';
|
||||||
|
const text = form.dataset.confirmText || `Yakin ingin menghapus ${entity}? Tindakan ini tidak bisa dibatalkan.`;
|
||||||
|
const confirmButtonColor = form.dataset.confirmButtonColor || '#e11d48';
|
||||||
|
const confirmButtonText = form.dataset.confirmButtonText || 'Ya, hapus';
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: 'Hapus data?',
|
title,
|
||||||
text: `Yakin ingin menghapus ${entity}? Tindakan ini tidak bisa dibatalkan.`,
|
text,
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonColor: '#e11d48',
|
confirmButtonColor,
|
||||||
cancelButtonColor: '#475569',
|
cancelButtonColor: '#475569',
|
||||||
confirmButtonText: 'Ya, hapus',
|
confirmButtonText,
|
||||||
cancelButtonText: 'Batal',
|
cancelButtonText: 'Batal',
|
||||||
reverseButtons: true,
|
reverseButtons: true,
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
|
|||||||
@@ -31,7 +31,13 @@
|
|||||||
{{ $documentType->max_file_size_kb ? number_format($documentType->max_file_size_kb / 1024, 2) . ' MB' : 'Default 5 MB' }}
|
{{ $documentType->max_file_size_kb ? number_format($documentType->max_file_size_kb / 1024, 2) . ' MB' : 'Default 5 MB' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<span class="rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700">Dokumen: {{ $documentType->student_documents_count }}</span>
|
<div class="flex flex-col gap-1">
|
||||||
|
<span class="rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700">Aktif: {{ $documentType->student_documents_count }}</span>
|
||||||
|
@php($trashedDocumentsCount = $documentType->student_documents_with_trashed_count - $documentType->student_documents_count)
|
||||||
|
@if ($trashedDocumentsCount > 0)
|
||||||
|
<span class="rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">Arsip: {{ $trashedDocumentsCount }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
@if ($documentType->is_required)
|
@if ($documentType->is_required)
|
||||||
@@ -50,16 +56,24 @@
|
|||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<a href="{{ route('document-types.edit', $documentType) }}" class="rounded-xl border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50">Edit</a>
|
<a href="{{ route('document-types.edit', $documentType) }}" class="rounded-xl border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50">Edit</a>
|
||||||
@if ($documentType->student_documents_count === 0)
|
@php($trashedDocumentsCount = $documentType->student_documents_with_trashed_count - $documentType->student_documents_count)
|
||||||
|
@if ($documentType->student_documents_count === 0 && $trashedDocumentsCount === 0)
|
||||||
<form action="{{ route('document-types.destroy', $documentType) }}" method="POST" class="delete-form" data-entity="jenis dokumen {{ $documentType->name }}">
|
<form action="{{ route('document-types.destroy', $documentType) }}" method="POST" class="delete-form" data-entity="jenis dokumen {{ $documentType->name }}">
|
||||||
@csrf
|
@csrf
|
||||||
@method('DELETE')
|
@method('DELETE')
|
||||||
<button type="submit" class="rounded-xl border border-rose-200 px-3 py-2 text-xs font-semibold text-rose-600 transition hover:bg-rose-50">Hapus</button>
|
<button type="submit" class="rounded-xl border border-rose-200 px-3 py-2 text-xs font-semibold text-rose-600 transition hover:bg-rose-50">Hapus</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@elseif ($documentType->student_documents_count > 0)
|
||||||
<button type="button" disabled class="cursor-not-allowed rounded-xl border border-slate-200 bg-slate-100 px-3 py-2 text-xs font-semibold text-slate-400" title="Tidak bisa dihapus karena masih dipakai data dokumen siswa">
|
<button type="button" disabled class="cursor-not-allowed rounded-xl border border-slate-200 bg-slate-100 px-3 py-2 text-xs font-semibold text-slate-400" title="Tidak bisa dihapus karena masih dipakai data dokumen siswa aktif">
|
||||||
Dipakai
|
Dipakai
|
||||||
</button>
|
</button>
|
||||||
|
@else
|
||||||
|
<form action="{{ route('document-types.purge-archived-documents', $documentType) }}" method="POST" class="delete-form" data-entity="arsip dokumen untuk jenis dokumen {{ $documentType->name }}" data-confirm-title="Bersihkan arsip dokumen?" data-confirm-text="Arsip dokumen siswa yang terkait akan dihapus permanen, lalu jenis dokumen ini ikut dihapus. Tindakan ini tidak bisa dibatalkan.">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-semibold text-amber-700 transition hover:bg-amber-100">
|
||||||
|
Bersihkan Arsip
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -85,14 +85,22 @@
|
|||||||
<div class="text-xs text-slate-400">{{ $row['data']['gender'] ?: '-' }} · {{ $row['data']['phone_number'] ?: '-' }}</div>
|
<div class="text-xs text-slate-400">{{ $row['data']['gender'] ?: '-' }} · {{ $row['data']['phone_number'] ?: '-' }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4 text-slate-600">
|
<td class="px-4 py-4 text-slate-600">
|
||||||
<div>Tahun: {{ $row['data']['academic_year_id'] ?: '-' }}</div>
|
<div>Tahun: {{ $row['display']['academic_year'] ?? ($row['data']['academic_year_id'] ?: '-') }}</div>
|
||||||
<div>Jurusan: {{ $row['data']['department_id'] ?: '-' }}</div>
|
<div>Jurusan: {{ $row['display']['department'] ?? ($row['data']['department_id'] ?: '-') }}</div>
|
||||||
<div>Kelas: {{ $row['data']['class_id'] ?: '-' }}</div>
|
<div>Kelas: {{ $row['display']['class'] ?? ($row['data']['class_id'] ?: '-') }}</div>
|
||||||
|
@if (!empty($row['display']['class_context']))
|
||||||
|
<div class="text-xs text-slate-400">{{ $row['display']['class_context'] }}</div>
|
||||||
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4">
|
<td class="px-4 py-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
<span class="rounded-full px-3 py-1 text-xs font-semibold {{ $row['action'] === 'update' ? 'bg-amber-100 text-amber-700' : 'bg-sky-100 text-sky-700' }}">
|
<span class="rounded-full px-3 py-1 text-xs font-semibold {{ $row['action'] === 'update' ? 'bg-amber-100 text-amber-700' : 'bg-sky-100 text-sky-700' }}">
|
||||||
{{ $row['action'] === 'update' ? 'Update' : 'Insert' }}
|
{{ $row['action'] === 'update' ? 'Update' : 'Insert' }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">
|
||||||
|
Status: {{ $row['display']['status'] ?? ($row['data']['status'] ?: '-') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-4 text-sm">
|
<td class="px-4 py-4 text-sm">
|
||||||
@if ($row['is_valid'])
|
@if ($row['is_valid'])
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
Route::post('classes/generate', [SchoolClassController::class, 'generate'])->name('classes.generate');
|
Route::post('classes/generate', [SchoolClassController::class, 'generate'])->name('classes.generate');
|
||||||
Route::delete('classes/bulk-delete', [SchoolClassController::class, 'bulkDestroy'])->name('classes.bulk-destroy');
|
Route::delete('classes/bulk-delete', [SchoolClassController::class, 'bulkDestroy'])->name('classes.bulk-destroy');
|
||||||
Route::resource('classes', SchoolClassController::class);
|
Route::resource('classes', SchoolClassController::class);
|
||||||
|
Route::post('document-types/{documentType}/purge-archived-documents', [DocumentTypeController::class, 'purgeArchivedDocuments'])->name('document-types.purge-archived-documents');
|
||||||
Route::resource('document-types', DocumentTypeController::class);
|
Route::resource('document-types', DocumentTypeController::class);
|
||||||
Route::prefix('settings')->name('settings.')->group(function () {
|
Route::prefix('settings')->name('settings.')->group(function () {
|
||||||
Route::resource('users', UserManagementController::class)->except(['show']);
|
Route::resource('users', UserManagementController::class)->except(['show']);
|
||||||
|
|||||||
Reference in New Issue
Block a user