Improve document type cleanup and student import reliability

This commit is contained in:
2026-05-02 10:42:33 +07:00
parent ac22f8b7c8
commit 700c24209d
6 changed files with 345 additions and 40 deletions
@@ -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.",
]); ]);
} }
+251 -22
View File
@@ -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
View File
@@ -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) => {
+18 -4
View File
@@ -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>
+14 -6
View File
@@ -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">
<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' }}"> <div class="flex flex-col gap-2">
{{ $row['action'] === 'update' ? 'Update' : 'Insert' }} <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> {{ $row['action'] === 'update' ? 'Update' : 'Insert' }}
</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'])
+1
View File
@@ -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']);