Update graduation workflow, promotions, and admin module improvements

This commit is contained in:
2026-05-02 13:09:26 +07:00
parent 700c24209d
commit 96d6e364a6
16 changed files with 1593 additions and 32 deletions
+110
View File
@@ -4,6 +4,116 @@
action-label="Tambah Kelas"
:action-url="route('classes.create')"
>
<x-master-card title="Import Data Kelas" subtitle="Upload Excel/CSV untuk menambah atau memperbarui data kelas secara massal.">
<form method="POST" action="{{ route('classes.import.preview') }}" enctype="multipart/form-data" class="space-y-5">
@csrf
<div>
<label for="import_file" class="mb-2 block text-sm font-medium text-slate-700">File Excel / CSV</label>
<input id="import_file" name="import_file" type="file" accept=".xlsx,.csv,.txt" class="block w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-700 file:mr-4 file:rounded-xl file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100">
<p class="mt-2 text-xs text-slate-500">Gunakan template agar format kolom sesuai. Import ini akan update data lama jika nama kelas pada tahun ajaran yang sama sudah ada.</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<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">Preview Import</button>
<a href="{{ route('classes.import.template') }}" class="inline-flex items-center rounded-2xl border border-emerald-200 bg-emerald-50 px-5 py-3 text-sm font-semibold text-emerald-700 transition hover:bg-emerald-100">Download Template</a>
</div>
</form>
@if (!empty($importPreview))
<div class="mt-6 rounded-3xl border border-slate-200 bg-white p-5">
<div class="mb-5 grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="rounded-3xl border border-slate-200 bg-slate-50 px-5 py-4">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Total Baris</div>
<div class="mt-2 text-3xl font-bold text-slate-800">{{ $importPreview['summary']['total_rows'] }}</div>
</div>
<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">Valid</div>
<div class="mt-2 text-3xl font-bold text-emerald-700">{{ $importPreview['summary']['valid_rows'] }}</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">Insert Baru</div>
<div class="mt-2 text-3xl font-bold text-sky-700">{{ $importPreview['summary']['insert_rows'] }}</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">Update Data</div>
<div class="mt-2 text-3xl font-bold text-amber-700">{{ $importPreview['summary']['update_rows'] }}</div>
</div>
</div>
@if (!empty($importPreview['errors']))
<div class="mb-5 rounded-2xl border border-rose-200 bg-rose-50 px-5 py-4 text-sm text-rose-700">
<div class="font-semibold">Ada baris yang belum valid.</div>
<ul class="mt-2 list-disc space-y-1 pl-5">
@foreach (array_slice($importPreview['errors'], 0, 10) as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="overflow-x-auto rounded-3xl border border-slate-200">
<table class="min-w-full divide-y divide-slate-200 text-sm">
<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">Baris</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Akademik</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Nama Kelas</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Detail</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Aksi</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Validasi</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white">
@foreach ($importPreview['rows'] as $row)
<tr class="align-top {{ $row['is_valid'] ? '' : 'bg-rose-50/50' }}">
<td class="px-4 py-4 font-semibold text-slate-700">{{ $row['row_number'] }}</td>
<td class="px-4 py-4 text-slate-600">
<div>Tahun: {{ $importReferenceData['academicYears'][$row['data']['academic_year_id']] ?? '-' }}</div>
<div>Jurusan: {{ $importReferenceData['departments'][$row['data']['department_id']] ?? '-' }}</div>
</td>
<td class="px-4 py-4 text-slate-700">
<div class="font-medium">{{ $row['data']['name'] ?: '-' }}</div>
<div class="text-xs text-slate-400">Tingkat: {{ $row['data']['level'] ?: '-' }}</div>
</td>
<td class="px-4 py-4 text-slate-600">
<div>Wali Kelas: {{ $row['data']['homeroom_teacher'] ?: '-' }}</div>
<div>Kapasitas: {{ $row['data']['capacity'] ?: '-' }}</div>
<div>Status: {{ ($row['data']['is_active'] ?? false) ? 'Aktif' : 'Nonaktif' }}</div>
</td>
<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' }}">{{ $row['action'] === 'update' ? 'Update' : 'Insert' }}</span>
</td>
<td class="px-4 py-4 text-sm">
@if ($row['is_valid'])
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700">Valid</span>
@else
<div class="space-y-1 text-rose-700">
<div class="inline-flex rounded-full bg-rose-100 px-3 py-1 text-xs font-semibold">Perlu perbaikan</div>
<ul class="list-disc pl-4 text-xs">
@foreach ($row['errors'] as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if ($importPreview['summary']['valid_rows'] > 0)
<form method="POST" action="{{ route('classes.import') }}" class="mt-6 flex flex-wrap items-center gap-3">
@csrf
<input type="hidden" name="preview_token" value="{{ $importPreview['token'] }}">
<button type="submit" class="inline-flex items-center rounded-2xl bg-emerald-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-emerald-200 transition hover:bg-emerald-700">Konfirmasi Import ke Database</button>
<span class="text-sm text-slate-500">Hanya baris valid yang akan diproses.</span>
</form>
@endif
</div>
@endif
</x-master-card>
<x-master-card title="Generate Kelas Tahun Ajaran Baru" subtitle="Salin kelas dari tahun ajaran sumber ke tahun ajaran target. Cocok untuk menyiapkan kelas tahun ajaran baru sebelum promosi siswa.">
<form method="POST" action="{{ route('classes.generate') }}" class="grid grid-cols-1 gap-4 xl:grid-cols-4 xl:items-end">
@csrf
+103
View File
@@ -4,6 +4,109 @@
action-label="Tambah Program Keahlian"
:action-url="route('departments.create')"
>
<x-master-card title="Import Program Keahlian" subtitle="Upload Excel/CSV untuk menambah atau memperbarui data program keahlian secara massal.">
<form method="POST" action="{{ route('departments.import.preview') }}" enctype="multipart/form-data" class="space-y-5">
@csrf
<div>
<label for="import_file" class="mb-2 block text-sm font-medium text-slate-700">File Excel / CSV</label>
<input id="import_file" name="import_file" type="file" accept=".xlsx,.csv,.txt" class="block w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-700 file:mr-4 file:rounded-xl file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100">
<p class="mt-2 text-xs text-slate-500">Gunakan template agar format kolom sesuai. Import ini akan update data lama jika kode atau nama program keahlian sudah ada.</p>
</div>
<div class="flex flex-wrap items-center gap-3">
<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">Preview Import</button>
<a href="{{ route('departments.import.template') }}" class="inline-flex items-center rounded-2xl border border-emerald-200 bg-emerald-50 px-5 py-3 text-sm font-semibold text-emerald-700 transition hover:bg-emerald-100">Download Template</a>
</div>
</form>
@if (!empty($importPreview))
<div class="mt-6 rounded-3xl border border-slate-200 bg-white p-5">
<div class="mb-5 grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="rounded-3xl border border-slate-200 bg-slate-50 px-5 py-4">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Total Baris</div>
<div class="mt-2 text-3xl font-bold text-slate-800">{{ $importPreview['summary']['total_rows'] }}</div>
</div>
<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">Valid</div>
<div class="mt-2 text-3xl font-bold text-emerald-700">{{ $importPreview['summary']['valid_rows'] }}</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">Insert Baru</div>
<div class="mt-2 text-3xl font-bold text-sky-700">{{ $importPreview['summary']['insert_rows'] }}</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">Update Data</div>
<div class="mt-2 text-3xl font-bold text-amber-700">{{ $importPreview['summary']['update_rows'] }}</div>
</div>
</div>
@if (!empty($importPreview['errors']))
<div class="mb-5 rounded-2xl border border-rose-200 bg-rose-50 px-5 py-4 text-sm text-rose-700">
<div class="font-semibold">Ada baris yang belum valid.</div>
<ul class="mt-2 list-disc space-y-1 pl-5">
@foreach (array_slice($importPreview['errors'], 0, 10) as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="overflow-x-auto rounded-3xl border border-slate-200">
<table class="min-w-full divide-y divide-slate-200 text-sm">
<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">Baris</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Kode</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Nama</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Status</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Aksi</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.16em] text-slate-500">Validasi</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white">
@foreach ($importPreview['rows'] as $row)
<tr class="align-top {{ $row['is_valid'] ? '' : 'bg-rose-50/50' }}">
<td class="px-4 py-4 font-semibold text-slate-700">{{ $row['row_number'] }}</td>
<td class="px-4 py-4 text-slate-700">{{ $row['data']['code'] ?: '-' }}</td>
<td class="px-4 py-4 text-slate-700">
<div class="font-medium">{{ $row['data']['name'] ?: '-' }}</div>
<div class="text-xs text-slate-400">{{ $row['data']['description'] ?: 'Tanpa deskripsi' }}</div>
</td>
<td class="px-4 py-4 text-slate-600">{{ ($row['data']['is_active'] ?? false) ? 'Aktif' : 'Nonaktif' }}</td>
<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' }}">{{ $row['action'] === 'update' ? 'Update' : 'Insert' }}</span>
</td>
<td class="px-4 py-4 text-sm">
@if ($row['is_valid'])
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700">Valid</span>
@else
<div class="space-y-1 text-rose-700">
<div class="inline-flex rounded-full bg-rose-100 px-3 py-1 text-xs font-semibold">Perlu perbaikan</div>
<ul class="list-disc pl-4 text-xs">
@foreach ($row['errors'] as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if ($importPreview['summary']['valid_rows'] > 0)
<form method="POST" action="{{ route('departments.import') }}" class="mt-6 flex flex-wrap items-center gap-3">
@csrf
<input type="hidden" name="preview_token" value="{{ $importPreview['token'] }}">
<button type="submit" class="inline-flex items-center rounded-2xl bg-emerald-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-emerald-200 transition hover:bg-emerald-700">Konfirmasi Import ke Database</button>
<span class="text-sm text-slate-500">Hanya baris valid yang akan diproses.</span>
</form>
@endif
</div>
@endif
</x-master-card>
<x-master-card title="Filter Data" subtitle="Cari berdasarkan kode, nama, atau deskripsi program keahlian.">
<form method="GET" action="{{ route('departments.index') }}" class="flex flex-col gap-4 md:flex-row md:items-end">
<div class="md:flex-1">
@@ -78,7 +78,7 @@
</div>
</div>
<form method="POST" action="{{ route('graduated-students.bulk-upload.preview') }}" enctype="multipart/form-data" class="space-y-5">
<form method="POST" action="{{ route('graduated-students.bulk-upload.preview') }}" enctype="multipart/form-data" class="space-y-5" data-bulk-upload-form data-bulk-upload-mode="preview" data-bulk-upload-file-count-source="input" data-bulk-upload-ajax-preview="true" data-bulk-upload-preview-url="{{ route('graduated-students.bulk-upload.preview-ajax') }}">
@csrf
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
@@ -130,9 +130,9 @@
</div>
<div class="flex flex-wrap items-center gap-3">
<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">
<i class="fa-solid fa-magnifying-glass"></i>
<span>Preview Mapping Dokumen</span>
<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 disabled:cursor-not-allowed disabled:opacity-70" data-bulk-upload-submit-button>
<i class="fa-solid fa-magnifying-glass" data-bulk-upload-submit-icon></i>
<span data-bulk-upload-submit-label>Preview Mapping Dokumen</span>
</button>
<a href="{{ route('graduated-students.index') }}" class="inline-flex items-center gap-2 rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">
<i class="fa-solid fa-arrow-left"></i>
@@ -262,10 +262,10 @@
</div>
</div>
<form method="POST" action="{{ route('graduated-students.bulk-upload.store') }}" class="mt-6 flex flex-wrap items-center gap-3">
<form method="POST" action="{{ route('graduated-students.bulk-upload.store') }}" class="mt-6 flex flex-wrap items-center gap-3" data-bulk-upload-form data-bulk-upload-mode="process" data-bulk-upload-file-count-source="preview" data-bulk-upload-preview-count="{{ $preview['summary']['ready_to_upload'] ?? $preview['summary']['total_files'] ?? 0 }}">
@csrf
<input type="hidden" name="preview_token" value="{{ $preview['token'] }}">
<button type="submit" class="inline-flex items-center gap-2 rounded-2xl bg-emerald-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-emerald-200 transition hover:bg-emerald-700">
<button type="submit" class="inline-flex items-center gap-2 rounded-2xl bg-emerald-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-emerald-200 transition hover:bg-emerald-700 disabled:cursor-not-allowed disabled:opacity-70" data-bulk-upload-submit-button>
<i class="fa-solid fa-cloud-arrow-up"></i>
<span>Proses Bulk Upload</span>
</button>
@@ -273,4 +273,55 @@
</x-master-card>
@endif
</div>
<div class="fixed inset-0 z-[11000] hidden items-center justify-center bg-slate-950/60 px-4" data-bulk-upload-overlay>
<div class="w-full max-w-lg rounded-3xl border border-white/10 bg-slate-900/95 p-6 text-white shadow-2xl backdrop-blur">
<div class="flex items-start gap-4">
<div class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-indigo-500/15 text-indigo-200">
<i class="fa-solid fa-arrows-rotate animate-spin text-2xl"></i>
</div>
<div>
<div class="text-lg font-bold" data-bulk-upload-overlay-title>Sedang memproses dokumen...</div>
<div class="mt-2 text-sm leading-6 text-slate-300" data-bulk-upload-overlay-message>Mohon tunggu. File sedang diunggah dan dipetakan oleh sistem. Jangan tutup tab ini agar proses tidak terputus.</div>
</div>
</div>
<div class="mt-5">
<div class="mb-2 flex items-center justify-between gap-3 text-xs text-slate-300">
<span class="font-semibold" data-bulk-upload-overlay-progress-label>Menunggu proses dimulai...</span>
<span class="font-semibold" data-bulk-upload-overlay-progress-percent>0%</span>
</div>
<div class="overflow-hidden rounded-full bg-white/10">
<div class="h-2 w-0 rounded-full bg-gradient-to-r from-indigo-400 via-sky-300 to-emerald-300 transition-all duration-300" data-bulk-upload-overlay-progress-bar></div>
</div>
</div>
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3">
<div class="rounded-2xl border border-white/10 bg-white/5 px-4 py-3">
<div class="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400">Tahap 1</div>
<div class="mt-1 text-sm font-semibold text-white" data-bulk-upload-stage-upload>Upload file</div>
</div>
<div class="rounded-2xl border border-white/10 bg-white/5 px-4 py-3">
<div class="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400">Tahap 2</div>
<div class="mt-1 text-sm font-semibold text-white" data-bulk-upload-stage-match>Cocokkan data</div>
</div>
<div class="rounded-2xl border border-white/10 bg-white/5 px-4 py-3">
<div class="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400">Tahap 3</div>
<div class="mt-1 text-sm font-semibold text-white" data-bulk-upload-stage-save>Simpan hasil</div>
</div>
</div>
<div class="mt-4 flex flex-wrap items-center gap-3 text-xs text-slate-300">
<div class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1.5">
<i class="fa-solid fa-files"></i>
<span data-bulk-upload-overlay-file-count>Menyiapkan jumlah file...</span>
</div>
<div class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1.5">
<i class="fa-solid fa-hourglass-half"></i>
<span>Durasi tergantung jumlah dan ukuran file</span>
</div>
</div>
<div class="mt-4 rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-xs leading-5 text-slate-300">
Proses bulk upload bisa butuh beberapa detik, terutama jika file banyak atau ukuran file mendekati batas maksimum.
</div>
</div>
</div>
</x-master-page>
@@ -81,11 +81,11 @@
<x-master-card title="Filter Arsip Kelulusan" subtitle="Cari lulusan berdasarkan nama, identitas siswa, jurusan, kelas terakhir, tahun ajaran, dan status dokumen.">
<div class="rounded-3xl border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-sky-50/30 p-4 shadow-sm ring-1 ring-white/70">
<form method="GET" action="{{ $indexRoute }}" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5">
<form method="GET" action="{{ $indexRoute }}" class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5" data-graduation-filter-form>
<x-form-input name="search" label="Pencarian" :value="$search" placeholder="Nama / NIS / NISN" />
<x-form-select name="academic_year_id" label="Tahun Ajaran" :options="$academicYears->all()" :value="$filters['academic_year_id']" />
<x-form-select name="academic_year_id" label="Tahun Ajaran" :options="$academicYears->all()" :value="$filters['academic_year_id']" data-graduation-academic-year-filter />
<x-form-select name="department_id" label="Program Keahlian" :options="$departments->all()" :value="$filters['department_id']" />
<x-form-select name="class_id" label="Kelas Terakhir" :options="$classes->all()" :value="$filters['class_id']" />
<x-form-select name="class_id" label="Kelas Terakhir" :options="$classes->all()" :value="$filters['class_id']" :placeholder="$filters['academic_year_id'] ? 'Pilih kelas terakhir' : 'Pilih tahun ajaran dulu'" />
<x-form-select name="document_status" label="Status Dokumen" :options="$documentStatusOptions" :value="$filters['document_status']" />
<input type="hidden" name="sort" value="{{ $sort }}">
@@ -222,8 +222,8 @@
$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 ?? ''));
$selectedPromote = old($targetClassField, $recommendation['action'] === 'promote' ? ($recommendation['target_class_id'] ?? '') : '');
$selectedRepeat = old($targetClassField, $recommendation['action'] === 'repeat' ? ($recommendation['target_class_id'] ?? '') : '');
$promoteClassOptions = $promoteClasses->map(fn ($classRoom) => [
'id' => $classRoom->id,
'name' => $classRoom->name,
+13 -2
View File
@@ -1,13 +1,24 @@
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<x-form-input name="name" label="Nama Lengkap" :value="$user->name ?? ''" required />
<x-form-input name="email" label="Email" type="email" :value="$user->email ?? ''" required />
<x-form-select name="role" label="Role" :options="$roleOptions" :value="$user->role ?? 'staff'" placeholder="-- Pilih role --" required />
<div>
<x-form-select name="role" label="Role" :options="$roleOptions" :value="$user->role ?? 'staff'" placeholder="-- Pilih role --" required />
<p class="mt-2 text-xs text-slate-500">Administrator punya akses penuh ke pengaturan sistem, staff hanya untuk operasional harian.</p>
</div>
<div class="rounded-3xl border border-slate-200 bg-slate-50 p-5 md:col-span-2">
<div class="mb-4 flex items-start gap-3 rounded-2xl border border-indigo-100 bg-indigo-50 px-4 py-3 text-sm text-indigo-700">
<i class="fa-solid fa-shield-halved mt-0.5"></i>
<div>
<div class="font-semibold">Keamanan akun</div>
<div class="mt-1 text-xs text-indigo-700/80">Gunakan password minimal 8 karakter. Untuk akun yang sedang diedit, password boleh dikosongkan jika tidak ingin diubah.</div>
</div>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<x-form-input name="password" label="Password {{ isset($user) ? '(opsional jika tidak diubah)' : '' }}" type="password" />
<x-form-input name="password_confirmation" label="Konfirmasi Password" type="password" />
</div>
<p class="mt-3 text-xs text-slate-500">Password minimal 8 karakter.</p>
<p class="mt-3 text-xs text-slate-500">Tips: kombinasikan huruf besar, huruf kecil, angka, dan simbol untuk password yang lebih kuat.</p>
</div>
</div>
+52 -11
View File
@@ -1,10 +1,34 @@
<x-master-page title="Manajemen Pengguna" description="Kelola akun administrator dan staff yang dapat mengakses panel Database Kesiswaan." action-label="Tambah Pengguna" :action-url="route('settings.users.create')">
<div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="rounded-3xl border border-slate-200 bg-slate-50 px-5 py-4">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Total Pengguna</div>
<div class="mt-2 text-3xl font-bold text-slate-800">{{ $stats['total'] }}</div>
</div>
<div class="rounded-3xl border border-indigo-200 bg-indigo-50 px-5 py-4">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-600">Administrator</div>
<div class="mt-2 text-3xl font-bold text-indigo-700">{{ $stats['admins'] }}</div>
</div>
<div class="rounded-3xl border border-slate-200 bg-white px-5 py-4">
<div class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Staff</div>
<div class="mt-2 text-3xl font-bold text-slate-700">{{ $stats['staff'] }}</div>
</div>
</div>
<x-master-card title="Daftar Pengguna" subtitle="Gunakan halaman ini untuk menambah, mengubah, dan menghapus akun admin internal.">
<form method="GET" class="mb-5">
<div class="grid grid-cols-1 gap-3 md:grid-cols-[1fr_auto]">
<x-form-input name="search" label="Cari Pengguna" :value="$search" placeholder="Cari nama atau email..." />
<div class="flex items-end">
<button type="submit" class="inline-flex h-12 items-center justify-center rounded-2xl bg-indigo-600 px-5 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700">Cari</button>
<div class="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
<div class="grid grid-cols-1 gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">
<x-form-input name="search" label="Cari Pengguna" :value="$search" placeholder="Cari nama atau email..." />
<div class="flex flex-col gap-2 sm:flex-row md:justify-end">
<button type="submit" class="inline-flex h-12 items-center justify-center rounded-2xl bg-indigo-600 px-5 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700">
<i class="fa-solid fa-magnifying-glass mr-2 text-xs"></i>
Cari
</button>
<a href="{{ route('settings.users.index') }}" class="inline-flex h-12 items-center justify-center rounded-2xl border border-slate-200 bg-white px-5 text-sm font-semibold text-slate-700 transition hover:bg-slate-100">
<i class="fa-solid fa-rotate-left mr-2 text-xs"></i>
Reset
</a>
</div>
</div>
</div>
</form>
@@ -23,22 +47,39 @@
<tbody class="divide-y divide-slate-100 bg-white">
@forelse ($users as $user)
<tr>
<td class="px-4 py-4 font-medium text-slate-800">{{ $user->name }}</td>
<td class="px-4 py-4">
<div class="font-medium text-slate-800">{{ $user->name }}</div>
@if (auth()->id() === $user->id)
<div class="mt-1 text-xs font-semibold text-emerald-600">Akun yang sedang dipakai</div>
@endif
</td>
<td class="px-4 py-4 text-slate-600">{{ $user->email }}</td>
<td class="px-4 py-4">
<span class="rounded-full px-3 py-1 text-xs font-semibold {{ $user->role === 'admin' ? 'bg-indigo-100 text-indigo-700' : 'bg-slate-100 text-slate-700' }}">
{{ $roleOptions[$user->role] ?? ucfirst($user->role) }}
</span>
</td>
<td class="px-4 py-4 text-slate-500">{{ auth()->id() === $user->id ? 'Sedang dipakai login' : 'Aktif' }}</td>
<td class="px-4 py-4">
@if (auth()->id() === $user->id)
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-700">Login saat ini</span>
@else
<span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600">Aktif</span>
@endif
</td>
<td class="px-4 py-4">
<div class="flex justify-end gap-2">
<a href="{{ route('settings.users.edit', $user) }}" class="inline-flex items-center rounded-xl border border-slate-200 px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50">Edit</a>
<form method="POST" action="{{ route('settings.users.destroy', $user) }}" onsubmit="return confirm('Hapus akun ini?')">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center 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>
@php($isCurrentUser = auth()->id() === $user->id)
@php($isLastAdmin = $user->role === 'admin' && ($stats['admins'] ?? 0) <= 1)
@if ($isCurrentUser || $isLastAdmin)
<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="{{ $isCurrentUser ? 'Akun yang sedang dipakai login tidak bisa dihapus' : 'Minimal harus ada satu akun admin aktif' }}">Hapus</button>
@else
<form method="POST" action="{{ route('settings.users.destroy', $user) }}" class="delete-form" data-entity="akun pengguna {{ $user->name }}" data-confirm-title="Hapus akun pengguna?" data-confirm-text="Akun {{ $user->name }} akan dihapus dari sistem. Tindakan ini tidak bisa dibatalkan.">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center 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>
@endif
</div>
</td>
</tr>