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
@@ -1,5 +1,6 @@
<x-master-page title="Kenaikan Kelas / Promosi Siswa" description="Proses semi otomatis untuk memindahkan siswa aktif ke kelas/tahun ajaran berikutnya.">
<x-master-card title="Filter Promosi" subtitle="Pilih tahun ajaran, lalu fokuskan proses per kelas agar pengecualian siswa lebih mudah dikelola.">
<div class="rounded-3xl border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-lime-50/30 p-4 shadow-sm ring-1 ring-white/70">
<form method="GET" action="{{ route('student-promotions.index') }}" class="grid grid-cols-1 gap-4 lg:grid-cols-4 lg:items-end">
<div>
<label for="from_academic_year_id" class="mb-2 block text-sm font-medium text-slate-700">Tahun Ajaran Asal</label>
@@ -38,11 +39,13 @@
</div>
<div>
<button type="submit" class="inline-flex w-full items-center justify-center rounded-2xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700">
Muat Rekomendasi
<button type="submit" class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700">
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span>Muat Rekomendasi</span>
</button>
</div>
</form>
</div>
</x-master-card>
<x-master-card title="Kenaikan Kelas per Rombel" subtitle="Mayoritas siswa biasanya mengikuti hasil kelasnya, lalu admin tinggal mengubah siswa pengecualian.">
@@ -63,32 +66,33 @@
@foreach ($classSummaries as $summary)
<a
href="{{ route('student-promotions.index', ['from_academic_year_id' => $filters['from_academic_year_id'], 'to_academic_year_id' => $filters['to_academic_year_id'], 'class_id' => $summary->id]) }}"
class="group rounded-3xl border border-slate-200 bg-white p-5 transition hover:border-indigo-200 hover:bg-indigo-50/40"
class="group rounded-3xl border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-lime-50/25 p-5 shadow-sm ring-1 ring-white/70 transition hover:-translate-y-0.5 hover:border-indigo-200 hover:bg-indigo-50/40 hover:shadow-lg hover:shadow-slate-200/60"
>
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<div class="text-lg font-semibold text-slate-800">{{ $summary->name }}</div>
<div class="mt-1 text-sm text-slate-500">{{ $summary->department_name ?: '-' }} • Level {{ $summary->level }}</div>
</div>
<div class="rounded-2xl bg-slate-100 px-3 py-2 text-right">
<div class="rounded-2xl bg-white/80 px-3 py-2 text-right shadow-sm ring-1 ring-white/70 backdrop-blur-sm">
<div class="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">Siswa Aktif</div>
<div class="mt-1 text-2xl font-bold text-slate-800">{{ $summary->active_students_count }}</div>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
<div class="rounded-2xl border border-slate-200 bg-white/80 px-4 py-3 shadow-sm ring-1 ring-white/70 backdrop-blur-sm">
<div class="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">Rekomendasi Default</div>
<div class="mt-1 font-semibold text-slate-800">{{ $summary->recommendation_label }}</div>
</div>
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
<div class="rounded-2xl border border-slate-200 bg-white/80 px-4 py-3 shadow-sm ring-1 ring-white/70 backdrop-blur-sm">
<div class="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">Target Umum</div>
<div class="mt-1 font-semibold text-slate-800">{{ $summary->recommendation_target_name ?: 'Perlu cek setelah kelas dipilih' }}</div>
</div>
</div>
<div class="mt-4 inline-flex items-center text-sm font-semibold text-indigo-700 transition group-hover:translate-x-0.5">
Proses kelas ini →
<div class="mt-4 inline-flex items-center gap-2 text-sm font-semibold text-indigo-700 transition group-hover:translate-x-0.5">
<i class="fa-solid fa-arrow-right"></i>
<span>Proses kelas ini</span>
</div>
</a>
@endforeach
@@ -116,44 +120,52 @@
</div>
@endif
<div class="mb-6 rounded-3xl border border-slate-200 bg-slate-50 px-5 py-4">
<div class="mb-6 rounded-3xl border border-slate-200 bg-gradient-to-r from-white via-slate-50 to-lime-50/30 px-5 py-4 shadow-sm ring-1 ring-white/70">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Kelas Sedang Diproses</div>
<div class="mt-2 text-2xl font-bold text-slate-800">{{ $selectedClass->name }}</div>
<div class="mt-1 text-sm text-slate-500">{{ $selectedClass->department?->name ?: '-' }} • {{ $selectedClass->active_students_count }} siswa aktif</div>
</div>
<a href="{{ route('student-promotions.index', ['from_academic_year_id' => $filters['from_academic_year_id'], 'to_academic_year_id' => $filters['to_academic_year_id']]) }}" class="inline-flex items-center rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-100">
Ganti Kelas
<a href="{{ route('student-promotions.index', ['from_academic_year_id' => $filters['from_academic_year_id'], 'to_academic_year_id' => $filters['to_academic_year_id']]) }}" class="inline-flex items-center gap-2 rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-100">
<i class="fa-solid fa-arrow-left"></i>
<span>Ganti Kelas</span>
</a>
</div>
</div>
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="rounded-3xl border border-emerald-200 bg-emerald-50 px-5 py-4" data-promotion-counter-card="promote">
<div class="rounded-3xl border border-emerald-200 bg-gradient-to-br from-white via-emerald-50/50 to-emerald-100/60 px-5 py-4 shadow-sm ring-1 ring-white/70" data-promotion-counter-card="promote">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-emerald-600">Naik Kelas</div>
<div class="mt-2 text-3xl font-bold text-emerald-700" data-promotion-counter="promote">{{ $promotionCount }}</div>
<div class="mt-1 text-sm text-emerald-600">Siswa yang akan naik ke level berikutnya.</div>
</div>
<div class="rounded-3xl border border-amber-200 bg-amber-50 px-5 py-4" data-promotion-counter-card="repeat">
<div class="rounded-3xl border border-amber-200 bg-gradient-to-br from-white via-amber-50/50 to-amber-100/60 px-5 py-4 shadow-sm ring-1 ring-white/70" data-promotion-counter-card="repeat">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-amber-600">Tinggal Kelas</div>
<div class="mt-2 text-3xl font-bold text-amber-700" data-promotion-counter="repeat">{{ $repeatCount }}</div>
<div class="mt-1 text-sm text-amber-600">Siswa yang akan tetap di level yang sama.</div>
</div>
<div class="rounded-3xl border border-sky-200 bg-sky-50 px-5 py-4" data-promotion-counter-card="graduate">
<div class="rounded-3xl border border-sky-200 bg-gradient-to-br from-white via-sky-50/50 to-sky-100/60 px-5 py-4 shadow-sm ring-1 ring-white/70" data-promotion-counter-card="graduate">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-sky-600">Lulus</div>
<div class="mt-2 text-3xl font-bold text-sky-700" data-promotion-counter="graduate">{{ $graduateCount }}</div>
<div class="mt-1 text-sm text-sky-600">Siswa kelas akhir yang akan diluluskan.</div>
</div>
<div class="rounded-3xl border border-slate-200 bg-slate-50 px-5 py-4" data-promotion-counter-card="skip">
<div class="rounded-3xl border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-slate-100/80 px-5 py-4 shadow-sm ring-1 ring-white/70" data-promotion-counter-card="skip">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Lewati</div>
<div class="mt-2 text-3xl font-bold text-slate-700" data-promotion-counter="skip">{{ $skipCount }}</div>
<div class="mt-1 text-sm text-slate-500">Siswa yang tidak ikut diproses pada batch ini.</div>
</div>
</div>
<div class="mb-6 rounded-3xl border border-indigo-200 bg-indigo-50 px-5 py-4 text-sm text-indigo-700" data-promotion-live-summary>
Ringkasan live: <span class="font-semibold">{{ $promotionCount }}</span> naik, <span class="font-semibold">{{ $repeatCount }}</span> tinggal kelas, <span class="font-semibold">{{ $graduateCount }}</span> lulus, dan <span class="font-semibold">{{ $skipCount }}</span> dilewati.
<div class="mb-6 rounded-3xl border border-indigo-200 bg-gradient-to-r from-indigo-50 to-white px-5 py-4 text-sm text-indigo-700 shadow-sm ring-1 ring-white/70" data-promotion-live-summary>
<div class="flex items-center gap-3">
<div class="inline-flex h-10 w-10 items-center justify-center rounded-2xl bg-white text-indigo-600 shadow-sm">
<i class="fa-solid fa-chart-line"></i>
</div>
<div>
Ringkasan live: <span class="font-semibold">{{ $promotionCount }}</span> naik, <span class="font-semibold">{{ $repeatCount }}</span> tinggal kelas, <span class="font-semibold">{{ $graduateCount }}</span> lulus, dan <span class="font-semibold">{{ $skipCount }}</span> dilewati.
</div>
</div>
</div>
<form method="POST" action="{{ route('student-promotions.store') }}" class="space-y-6" data-student-promotion-form>
@@ -162,12 +174,12 @@
<input type="hidden" name="to_academic_year_id" value="{{ $filters['to_academic_year_id'] }}">
<input type="hidden" name="class_id" value="{{ $filters['class_id'] }}">
<div class="flex flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4" data-promotion-toolbar data-class-level="{{ strtoupper((string) $selectedClass->level) }}">
<div class="flex flex-wrap items-center gap-3 rounded-3xl border border-slate-200 bg-gradient-to-r from-white via-slate-50 to-indigo-50/25 px-5 py-4 shadow-sm ring-1 ring-white/70" data-promotion-toolbar data-class-level="{{ strtoupper((string) $selectedClass->level) }}">
<div class="text-sm font-medium text-slate-700">Aksi cepat untuk kelas ini:</div>
<button type="button" class="rounded-xl border border-emerald-200 px-4 py-2 text-xs font-semibold text-emerald-700 transition hover:bg-emerald-50" data-promotion-bulk-action="promote">Set semua → Naik Kelas</button>
<button type="button" class="rounded-xl border border-sky-200 px-4 py-2 text-xs font-semibold text-sky-700 transition hover:bg-sky-50" data-promotion-bulk-action="graduate">Set semua → Lulus</button>
<button type="button" class="rounded-xl border border-amber-200 px-4 py-2 text-xs font-semibold text-amber-700 transition hover:bg-amber-50" data-promotion-bulk-action="repeat">Set semua → Tinggal Kelas</button>
<button type="button" class="rounded-xl border border-slate-200 px-4 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-100" data-promotion-bulk-action="skip">Set semua → Lewati</button>
<button type="button" class="inline-flex items-center gap-2 rounded-xl border border-emerald-200 px-4 py-2 text-xs font-semibold text-emerald-700 transition hover:bg-emerald-50" data-promotion-bulk-action="promote"><i class="fa-solid fa-arrow-trend-up"></i><span>Set semua → Naik Kelas</span></button>
<button type="button" class="inline-flex items-center gap-2 rounded-xl border border-sky-200 px-4 py-2 text-xs font-semibold text-sky-700 transition hover:bg-sky-50" data-promotion-bulk-action="graduate"><i class="fa-solid fa-graduation-cap"></i><span>Set semua → Lulus</span></button>
<button type="button" class="inline-flex items-center gap-2 rounded-xl border border-amber-200 px-4 py-2 text-xs font-semibold text-amber-700 transition hover:bg-amber-50" data-promotion-bulk-action="repeat"><i class="fa-solid fa-rotate-left"></i><span>Set semua → Tinggal Kelas</span></button>
<button type="button" class="inline-flex items-center gap-2 rounded-xl border border-slate-200 px-4 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-100" data-promotion-bulk-action="skip"><i class="fa-solid fa-forward"></i><span>Set semua → Lewati</span></button>
</div>
@if (strtoupper((string) $selectedClass->level) === 'XII')
@@ -179,7 +191,7 @@
<div class="hidden rounded-3xl border px-5 py-4 text-sm" data-promotion-capacity-summary></div>
<div class="hidden rounded-3xl border border-indigo-200 bg-indigo-50 px-5 py-4 text-sm text-indigo-700" data-promotion-final-preview></div>
<div class="overflow-x-auto rounded-3xl border border-slate-200">
<div class="overflow-x-auto rounded-3xl border border-slate-200 shadow-sm ring-1 ring-white/70">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
@@ -235,11 +247,18 @@
default => 'bg-slate-100 text-slate-700',
};
@endphp
<tr class="align-top hover:bg-slate-50/60 {{ $rowErrors->isNotEmpty() ? 'bg-rose-50/60' : '' }}">
<tr class="align-top transition hover:bg-slate-50/60 {{ $rowErrors->isNotEmpty() ? 'bg-rose-50/60' : '' }}">
<td class="px-4 py-4">
<input type="hidden" name="promotions[{{ $loop->index }}][student_id]" value="{{ $student->id }}">
<div class="font-semibold text-slate-800">{{ $student->full_name }}</div>
<div class="mt-1 text-xs text-slate-500">NIS: {{ $student->student_number ?: '-' }}</div>
<div class="flex items-start gap-3">
<div class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-lime-100 text-sm font-bold text-lime-700">
{{ strtoupper(mb_substr($student->full_name, 0, 1)) }}
</div>
<div>
<div class="font-semibold text-slate-800">{{ $student->full_name }}</div>
<div class="mt-1 text-xs text-slate-500">NIS: {{ $student->student_number ?: '-' }}</div>
</div>
</div>
@if ($rowErrors->isNotEmpty())
<div class="mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700">
<div class="font-semibold">Perlu diperbaiki</div>
@@ -257,15 +276,16 @@
<div class="text-xs text-slate-500">{{ $student->department?->name ?: '-' }}</div>
</td>
<td class="px-4 py-4">
<div class="inline-flex rounded-full px-3 py-1 text-xs font-semibold {{ $badgeClass }}">
{{ $recommendation['label'] }}
<div class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs font-semibold {{ $badgeClass }}">
<i class="fa-solid {{ $recommendation['action'] === 'promote' ? 'fa-arrow-trend-up' : ($recommendation['action'] === 'repeat' ? 'fa-rotate-left' : ($recommendation['action'] === 'graduate' ? 'fa-graduation-cap' : 'fa-forward')) }}"></i>
<span>{{ $recommendation['label'] }}</span>
</div>
@if ($recommendation['target_class_name'])
<div class="mt-2 text-xs text-slate-500">Target: {{ $recommendation['target_class_name'] }}</div>
@endif
</td>
<td class="px-4 py-4">
<select name="promotions[{{ $loop->index }}][action]" class="promotion-action-select w-44 rounded-2xl border {{ $errors->has($actionField) ? 'border-rose-300 bg-rose-50' : 'border-slate-200 bg-white' }} px-4 py-3 text-sm text-slate-700 focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100" data-default-action="{{ $recommendation['action'] }}">
<select name="promotions[{{ $loop->index }}][action]" class="promotion-action-select w-44 rounded-2xl border {{ $errors->has($actionField) ? 'border-rose-300 bg-rose-50' : 'border-slate-200 bg-white shadow-sm ring-1 ring-white/70' }} px-4 py-3 text-sm text-slate-700 focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100" data-default-action="{{ $recommendation['action'] }}">
<option value="promote" @selected($oldAction === 'promote')>Naik Kelas</option>
<option value="repeat" @selected($oldAction === 'repeat')>Tinggal Kelas</option>
<option value="graduate" @selected($oldAction === 'graduate')>Lulus</option>
@@ -281,7 +301,7 @@
<select
name="promotions[{{ $loop->index }}][target_class_id]"
class="promotion-target-select {{ $oldAction === 'graduate' ? 'hidden' : '' }} mt-0 w-60 rounded-2xl border {{ $errors->has($targetClassField) ? 'border-rose-300 bg-rose-50' : 'border-slate-200 bg-white' }} px-4 py-3 text-sm text-slate-700 focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100"
class="promotion-target-select {{ $oldAction === 'graduate' ? 'hidden' : '' }} mt-0 w-60 rounded-2xl border {{ $errors->has($targetClassField) ? 'border-rose-300 bg-rose-50' : 'border-slate-200 bg-white shadow-sm ring-1 ring-white/70' }} px-4 py-3 text-sm text-slate-700 focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100"
data-options-promote='@json($promoteClassOptions)'
data-options-repeat='@json($repeatClassOptions)'
data-selected-promote="{{ $selectedPromote }}"
@@ -307,7 +327,7 @@
@endif
</td>
<td class="px-4 py-4">
<textarea name="promotions[{{ $loop->index }}][notes]" rows="3" class="w-64 rounded-2xl border {{ $errors->has($notesField) ? 'border-rose-300 bg-rose-50' : 'border-slate-200 bg-white' }} px-4 py-3 text-sm text-slate-700 placeholder:text-slate-400 focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100" placeholder="Contoh: Tinggal kelas karena dokumen akademik belum lengkap...">{{ old($notesField) }}</textarea>
<textarea name="promotions[{{ $loop->index }}][notes]" rows="3" class="w-64 rounded-2xl border {{ $errors->has($notesField) ? 'border-rose-300 bg-rose-50' : 'border-slate-200 bg-white shadow-sm ring-1 ring-white/70' }} px-4 py-3 text-sm text-slate-700 placeholder:text-slate-400 focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100" placeholder="Contoh: Tinggal kelas karena dokumen akademik belum lengkap...">{{ old($notesField) }}</textarea>
</td>
</tr>
@endforeach
@@ -315,13 +335,14 @@
</table>
</div>
<div class="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4">
<div class="flex flex-wrap items-center justify-between gap-3 rounded-3xl border border-slate-200 bg-gradient-to-r from-white via-slate-50 to-lime-50/25 px-5 py-4 shadow-sm ring-1 ring-white/70">
<div class="space-y-1 text-sm text-slate-500">
<div>Total siswa siap diproses dari kelas ini: <span class="font-semibold text-slate-800">{{ $students->count() }}</span></div>
<div class="text-xs text-slate-400">Tips: pola kerja idealnya set semua sesuai rekomendasi kelas, lalu ubah hanya siswa yang menjadi pengecualian.</div>
</div>
<button type="submit" class="inline-flex items-center rounded-2xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700" data-promotion-submit-button>
Proses Promosi Kelas Ini
<button type="submit" class="inline-flex items-center gap-2 rounded-2xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700" data-promotion-submit-button>
<i class="fa-solid fa-paper-plane"></i>
<span>Proses Promosi Kelas Ini</span>
</button>
</div>
</form>