Initial commit
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
<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 asal dan tujuan untuk menyiapkan rekomendasi promosi.">
|
||||
<form method="GET" action="{{ route('student-promotions.index') }}" class="grid grid-cols-1 gap-4 lg:grid-cols-3 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>
|
||||
<select id="from_academic_year_id" name="from_academic_year_id" class="w-full rounded-2xl border 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">
|
||||
<option value="">Pilih tahun ajaran asal</option>
|
||||
@foreach ($academicYears as $academicYear)
|
||||
<option value="{{ $academicYear->id }}" @selected((int) $filters['from_academic_year_id'] === (int) $academicYear->id)>
|
||||
{{ $academicYear->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="to_academic_year_id" class="mb-2 block text-sm font-medium text-slate-700">Tahun Ajaran Tujuan</label>
|
||||
<select id="to_academic_year_id" name="to_academic_year_id" class="w-full rounded-2xl border 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">
|
||||
<option value="">Pilih tahun ajaran tujuan</option>
|
||||
@foreach ($academicYears as $academicYear)
|
||||
<option value="{{ $academicYear->id }}" @selected((int) $filters['to_academic_year_id'] === (int) $academicYear->id)>
|
||||
{{ $academicYear->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
</x-master-card>
|
||||
|
||||
<x-master-card title="Daftar Rekomendasi Promosi" subtitle="Sistem memberi saran berdasarkan level kelas saat ini. Admin tetap bisa menyesuaikan sebelum diproses.">
|
||||
@if (blank($filters['from_academic_year_id']) || blank($filters['to_academic_year_id']))
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 px-5 py-4 text-sm text-amber-700">
|
||||
Pilih tahun ajaran asal dan tujuan terlebih dahulu untuk memuat rekomendasi promosi siswa.
|
||||
</div>
|
||||
@elseif ($students->isEmpty())
|
||||
<div class="text-sm text-slate-500">Tidak ada siswa aktif pada tahun ajaran asal yang dipilih.</div>
|
||||
@else
|
||||
@php
|
||||
$promotionCount = $students->filter(fn ($student) => $student->promotion_recommendation['action'] === 'promote')->count();
|
||||
$repeatCount = $students->filter(fn ($student) => $student->promotion_recommendation['action'] === 'repeat')->count();
|
||||
$graduateCount = $students->filter(fn ($student) => $student->promotion_recommendation['action'] === 'graduate')->count();
|
||||
@endphp
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 rounded-3xl border border-rose-200 bg-rose-50 px-5 py-4 text-sm text-rose-700">
|
||||
<div class="font-semibold">Proses promosi belum bisa dilanjutkan.</div>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="rounded-3xl border border-emerald-200 bg-emerald-50 px-5 py-4">
|
||||
<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">{{ $promotionCount }}</div>
|
||||
<div class="mt-1 text-sm text-emerald-600">Siswa direkomendasikan naik ke level berikutnya.</div>
|
||||
</div>
|
||||
<div class="rounded-3xl border border-amber-200 bg-amber-50 px-5 py-4">
|
||||
<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">{{ $repeatCount }}</div>
|
||||
<div class="mt-1 text-sm text-amber-600">Siswa akan tetap di level yang sama pada tahun ajaran baru.</div>
|
||||
</div>
|
||||
<div class="rounded-3xl border border-sky-200 bg-sky-50 px-5 py-4">
|
||||
<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">{{ $graduateCount }}</div>
|
||||
<div class="mt-1 text-sm text-sky-600">Siswa kelas akhir yang direkomendasikan lulus.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('student-promotions.store') }}" class="space-y-6" data-student-promotion-form>
|
||||
@csrf
|
||||
<input type="hidden" name="from_academic_year_id" value="{{ $filters['from_academic_year_id'] }}">
|
||||
<input type="hidden" name="to_academic_year_id" value="{{ $filters['to_academic_year_id'] }}">
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-5 py-4">
|
||||
<div class="text-sm font-medium text-slate-700">Aksi cepat:</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-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>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-3xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Siswa</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Posisi Saat Ini</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Rekomendasi</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Aksi Admin</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Kelas Tujuan</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Catatan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 bg-white">
|
||||
@foreach ($students as $student)
|
||||
@php
|
||||
$recommendation = $student->promotion_recommendation;
|
||||
$promoteClasses = $recommendation['available_classes']['promote'];
|
||||
$repeatClasses = $recommendation['available_classes']['repeat'];
|
||||
$actionField = 'promotions.'.$loop->index.'.action';
|
||||
$targetClassField = 'promotions.'.$loop->index.'.target_class_id';
|
||||
$notesField = 'promotions.'.$loop->index.'.notes';
|
||||
$studentField = 'promotions.'.$loop->index.'.student_id';
|
||||
$oldAction = old($actionField, $recommendation['action']);
|
||||
$oldTargetClassId = old($targetClassField, $recommendation['target_class_id']);
|
||||
$defaultClasses = $oldAction === 'repeat' ? $repeatClasses : $promoteClasses;
|
||||
$rowErrors = collect([
|
||||
$errors->first($studentField),
|
||||
$errors->first($actionField),
|
||||
$errors->first($targetClassField),
|
||||
$errors->first($notesField),
|
||||
])->filter();
|
||||
$selectedPromote = old($targetClassField, $promoteClasses->firstWhere('id', $recommendation['target_class_id'])?->id ?? ($promoteClasses->first()?->id ?? ''));
|
||||
$selectedRepeat = old($targetClassField, $repeatClasses->firstWhere('id', $recommendation['target_class_id'])?->id ?? ($repeatClasses->first()?->id ?? ''));
|
||||
$promoteClassOptions = $promoteClasses->map(fn ($classRoom) => [
|
||||
'id' => $classRoom->id,
|
||||
'name' => $classRoom->name,
|
||||
'option_label' => $classRoom->option_label,
|
||||
'capacity_label' => $classRoom->capacity_label,
|
||||
'remaining_capacity' => $classRoom->remaining_capacity,
|
||||
])->values();
|
||||
$repeatClassOptions = $repeatClasses->map(fn ($classRoom) => [
|
||||
'id' => $classRoom->id,
|
||||
'name' => $classRoom->name,
|
||||
'option_label' => $classRoom->option_label,
|
||||
'capacity_label' => $classRoom->capacity_label,
|
||||
'remaining_capacity' => $classRoom->remaining_capacity,
|
||||
])->values();
|
||||
$badgeClass = match ($recommendation['action']) {
|
||||
'promote' => 'bg-emerald-100 text-emerald-700',
|
||||
'repeat' => 'bg-amber-100 text-amber-700',
|
||||
'graduate' => 'bg-sky-100 text-sky-700',
|
||||
default => 'bg-slate-100 text-slate-700',
|
||||
};
|
||||
@endphp
|
||||
<tr class="align-top 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>
|
||||
@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>
|
||||
<ul class="mt-1 list-disc space-y-1 pl-4">
|
||||
@foreach ($rowErrors as $rowError)
|
||||
<li>{{ $rowError }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-slate-600">
|
||||
<div class="font-medium text-slate-700">{{ $student->classRoom?->name ?: '-' }}</div>
|
||||
<div class="mt-1">{{ $student->academicYear?->name ?: '-' }}</div>
|
||||
<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>
|
||||
@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'] }}">
|
||||
<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>
|
||||
<option value="skip" @selected($oldAction === 'skip')>Lewati</option>
|
||||
</select>
|
||||
<div class="mt-2 text-xs text-slate-500">Admin bisa ubah rekomendasi sebelum proses.</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-500 promotion-graduate-placeholder {{ $oldAction === 'graduate' ? '' : 'hidden' }}">
|
||||
{{ $recommendation['target_class_name'] ?: 'Aksi ini tidak memerlukan kelas tujuan.' }}
|
||||
</div>
|
||||
<input type="hidden" name="promotions[{{ $loop->index }}][target_class_id]" value="" data-promotion-target-hidden>
|
||||
|
||||
<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"
|
||||
data-options-promote='@json($promoteClassOptions)'
|
||||
data-options-repeat='@json($repeatClassOptions)'
|
||||
data-selected-promote="{{ $selectedPromote }}"
|
||||
data-selected-repeat="{{ $selectedRepeat }}"
|
||||
>
|
||||
<option value="">Pilih kelas tujuan</option>
|
||||
@foreach ($defaultClasses as $classRoom)
|
||||
<option value="{{ $classRoom->id }}" @selected((int) $oldTargetClassId === (int) $classRoom->id)>
|
||||
{{ $classRoom->option_label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="mt-2 text-xs {{ $defaultClasses->isEmpty() ? 'text-amber-600' : 'text-slate-500' }} promotion-target-help">
|
||||
{{ $defaultClasses->isEmpty() ? 'Belum ada kelas aktif yang cocok pada tahun ajaran target untuk aksi ini.' : 'Pilih kelas tujuan sesuai aksi. Naik kelas memakai level berikutnya, tinggal kelas memakai level yang sama.' }}
|
||||
</div>
|
||||
@if ($defaultClasses->isNotEmpty())
|
||||
<div class="mt-2 space-y-1 text-[11px] text-slate-500">
|
||||
@foreach ($defaultClasses as $classRoom)
|
||||
<div>• {{ $classRoom->name }} — {{ $classRoom->capacity_label }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@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>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</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="space-y-1 text-sm text-slate-500">
|
||||
<div>Total siswa siap diproses: <span class="font-semibold text-slate-800">{{ $students->count() }}</span></div>
|
||||
<div class="text-xs text-slate-400">Tips: untuk siswa tinggal kelas, pastikan tahun ajaran target punya kelas dengan level yang sama dan sisa kuota masih cukup.</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">
|
||||
Proses Promosi Siswa
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
</x-master-card>
|
||||
</x-master-page>
|
||||
Reference in New Issue
Block a user