Poles UI/UX modul Ambang SP premium

- Kartu threshold: strip gradient warna di atas, icon chip gradient, badge
  poin (≥ X poin), label Nonaktif, toggle & hapus di kanan, hover lift
- Field: label uppercase tracking, suffix 'poin' di input Min Poin
- Modal tambah: header gradient biru-indigo, color picker + 8 preset swatch
  interaktif (Alpine), preview warna terpilih
- Info card jadi 'Tangga SP' dinamis mengikuti data threshold asli (bukan
  hardcoded), lengkap status nonaktif
- Empty state & tombol simpan pakai sistem btn-primary
- 2 smoke test baru render halaman (31 test total)
This commit is contained in:
Novy San87
2026-08-03 17:38:48 +07:00
parent 151a66639b
commit 652422c3eb
2 changed files with 179 additions and 104 deletions
+140 -104
View File
@@ -5,18 +5,18 @@
@section('content')
<div>
{{-- Header --}}
<div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="mb-6 flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4">
<div>
<nav class="flex items-center gap-1.5 text-sm text-gray-400 mb-1">
<a href="{{ route('settings.index') }}" class="hover:text-gray-600 transition">Pengaturan</a>
<span class="text-gray-300">/</span>
<span class="text-gray-700 font-medium">Ambang SP</span>
<nav class="flex items-center gap-1.5 text-xs text-slate-400 mb-1.5">
<a href="{{ route('settings.index') }}" class="hover:text-blue-600 transition font-medium">Pengaturan</a>
<i class="fa-solid fa-chevron-right text-[9px]"></i>
<span class="text-slate-700 font-semibold">Ambang SP</span>
</nav>
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Ambang Surat Peringatan</h1>
<p class="text-sm text-gray-500 mt-1">Atur batas poin untuk setiap tingkat Surat Peringatan</p>
<h1 class="text-2xl font-extrabold text-slate-900 tracking-tight">Ambang Surat Peringatan</h1>
<p class="text-sm text-slate-500 mt-1">Atur batas poin untuk setiap tingkat Surat Peringatan</p>
</div>
<button type="button" x-data @click="window.dispatchEvent(new CustomEvent('open-modal', {detail: 'create-threshold'}))"
class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
class="btn-primary flex-shrink-0">
<i class="fa-solid fa-plus text-xs"></i>
Tambah Ambang SP
</button>
@@ -29,86 +29,99 @@
@foreach($thresholds as $t)
<input type="hidden" name="thresholds[{{ $loop->index }}][id]" value="{{ $t->id }}">
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden transition hover:shadow-md">
<div class="flex items-stretch">
{{-- Left color bar --}}
<div class="w-[5px] flex-shrink-0" style="background-color: {{ $t->color }}"></div>
<div class="group bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden transition-all duration-200 hover:shadow-lg hover:border-slate-300">
{{-- Top gradient strip --}}
<div class="h-1.5 w-full" style="background: linear-gradient(90deg, {{ $t->color }}, {{ $t->color }}88)"></div>
<div class="flex-1 px-5 py-4">
{{-- Header --}}
<div class="flex items-start justify-between gap-4 mb-4">
<div class="flex items-center gap-3 min-w-0">
<div class="w-9 h-9 rounded-xl flex items-center justify-center shadow-sm flex-shrink-0"
style="background-color: {{ $t->color }}20; color: {{ $t->color }}">
<i class="fa-solid fa-file-lines text-sm"></i>
</div>
<div class="min-w-0">
<div class="flex items-center gap-2">
<h3 class="text-sm font-bold text-gray-900" style="color: {{ $t->color }}">{{ $t->name }}</h3>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="thresholds[{{ $loop->index }}][is_active]" value="1"
{{ $t->is_active ? 'checked' : '' }}
class="sr-only peer">
<div class="w-8 h-4 bg-gray-200 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-3 after:w-3 after:transition-all"
style="{{ $t->is_active ? 'background-color: ' . $t->color : '' }}"></div>
</label>
</div>
<p class="text-xs text-gray-400 mt-0.5">Mulai dari {{ $t->min_points }} poin</p>
</div>
<div class="p-5">
{{-- Header --}}
<div class="flex items-center justify-between gap-4 mb-5">
<div class="flex items-center gap-3.5 min-w-0">
<div class="w-11 h-11 rounded-xl flex items-center justify-center shadow-md flex-shrink-0"
style="background: linear-gradient(135deg, {{ $t->color }}, {{ $t->color }}cc); color: #fff">
<i class="fa-solid fa-file-lines text-base"></i>
</div>
<div class="flex items-center gap-1 flex-shrink-0">
<form action="{{ route('settings.thresholds.destroy', $t->id) }}" method="POST" id="delete-threshold-{{ $t->id }}">
@csrf @method('DELETE')
<button type="button"
x-data
x-on:click="if(await window.confirmSwal({title:'Hapus {{ $t->name }}?',text:'Yakin ingin menghapus ambang SP ini?',icon:'question',confirmText:'Ya, Hapus!',cancelText:'Batal'})) document.getElementById('delete-threshold-{{ $t->id }}').submit()"
class="w-7 h-7 rounded-lg flex items-center justify-center text-gray-300 hover:text-red-500 hover:bg-red-50 transition"
title="Hapus">
<i class="fa-solid fa-trash-can text-xs"></i>
</button>
</form>
<div class="min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<h3 class="text-[15px] font-extrabold tracking-tight" style="color: {{ $t->color }}">{{ $t->name }}</h3>
<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-bold"
style="background-color: {{ $t->color }}15; color: {{ $t->color }}">
<i class="fa-solid fa-bolt text-[9px]"></i> ≥ {{ $t->min_points }} poin
</span>
@if(! $t->is_active)
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 text-slate-400">
<i class="fa-solid fa-pause text-[8px]"></i> Nonaktif
</span>
@endif
</div>
<p class="text-xs text-slate-400 mt-1 truncate max-w-md">{{ $t->default_description ?: 'Tidak ada deskripsi' }}</p>
</div>
</div>
{{-- Fields --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<div>
<label class="block text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-1">Nama</label>
<input type="text" name="thresholds[{{ $loop->index }}][name]" value="{{ $t->name }}"
class="w-full px-3.5 py-2 border border-slate-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
</div>
<div>
<label class="block text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-1">Min Poin</label>
<div class="flex items-center gap-1.5 flex-shrink-0">
{{-- Toggle aktif --}}
<label class="relative inline-flex items-center cursor-pointer p-1.5" title="{{ $t->is_active ? 'Klik untuk nonaktifkan' : 'Klik untuk aktifkan' }}">
<input type="checkbox" name="thresholds[{{ $loop->index }}][is_active]" value="1"
{{ $t->is_active ? 'checked' : '' }}
class="sr-only peer">
<div class="w-9 h-5 bg-slate-200 rounded-full peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:border-transparent"
style="{{ $t->is_active ? 'background-color: ' . $t->color : '' }}"></div>
</label>
{{-- Hapus --}}
<form action="{{ route('settings.thresholds.destroy', $t->id) }}" method="POST" id="delete-threshold-{{ $t->id }}">
@csrf @method('DELETE')
<button type="button"
x-data
x-on:click="if(await window.confirmSwal({title:'Hapus {{ $t->name }}?',text:'Yakin ingin menghapus ambang SP ini?',icon:'question',confirmText:'Ya, Hapus!',cancelText:'Batal'})) document.getElementById('delete-threshold-{{ $t->id }}').submit()"
class="w-8 h-8 rounded-lg flex items-center justify-center text-slate-300 hover:text-red-500 hover:bg-red-50 transition"
title="Hapus">
<i class="fa-solid fa-trash-can text-sm"></i>
</button>
</form>
</div>
</div>
{{-- Fields --}}
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-4 border-t border-slate-100">
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-[0.12em] mb-1.5">Nama</label>
<input type="text" name="thresholds[{{ $loop->index }}][name]" value="{{ $t->name }}"
class="input">
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-[0.12em] mb-1.5">Min Poin</label>
<div class="relative">
<input type="number" name="thresholds[{{ $loop->index }}][min_points]" value="{{ $t->min_points }}" min="0"
class="w-full px-3.5 py-2 border border-slate-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
</div>
<div>
<label class="block text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-1">Deskripsi</label>
<input type="text" name="thresholds[{{ $loop->index }}][default_description]" value="{{ $t->default_description }}"
class="w-full px-3.5 py-2 border border-slate-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
class="input pr-8">
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-bold text-slate-300 pointer-events-none">poin</span>
</div>
</div>
<div>
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-[0.12em] mb-1.5">Deskripsi</label>
<input type="text" name="thresholds[{{ $loop->index }}][default_description]" value="{{ $t->default_description }}" placeholder="Contoh: SP 1 — poin mencapai 50"
class="input">
</div>
</div>
</div>
</div>
@endforeach
<button type="submit"
class="w-full px-5 py-3 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm inline-flex items-center justify-center gap-2">
<i class="fa-solid fa-floppy-disk text-xs"></i>
<button type="submit" class="btn-primary w-full justify-center py-3.5">
<i class="fa-solid fa-floppy-disk text-sm"></i>
Simpan Semua Perubahan
</button>
</form>
@else
{{-- Empty state --}}
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 py-12 text-center">
<div class="w-16 h-16 rounded-2xl bg-gray-50 border border-gray-100 flex items-center justify-center mx-auto mb-4">
<i class="fa-solid fa-chart-bar text-gray-300 text-2xl"></i>
<div class="bg-white rounded-2xl shadow-sm border border-dashed border-slate-300 py-16 text-center">
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100 border border-blue-100 flex items-center justify-center mx-auto mb-4 shadow-sm">
<i class="fa-solid fa-chart-simple text-blue-400 text-2xl"></i>
</div>
<h4 class="text-sm font-semibold text-gray-500 mb-1">Belum Ada Ambang SP</h4>
<p class="text-xs text-gray-400 mb-4">Tambahkan ambang Surat Peringatan untuk memantau level pelanggaran siswa</p>
<h4 class="text-sm font-bold text-slate-600 mb-1">Belum Ada Ambang SP</h4>
<p class="text-xs text-slate-400 mb-5 max-w-sm mx-auto">Tambahkan ambang Surat Peringatan untuk memantau level pelanggaran siswa</p>
<button type="button" x-data @click="window.dispatchEvent(new CustomEvent('open-modal', {detail: 'create-threshold'}))"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold text-blue-600 bg-blue-50 border border-blue-200 rounded-xl hover:bg-blue-100 transition shadow-sm">
class="btn-primary">
<i class="fa-solid fa-plus text-xs"></i>
Tambah Ambang SP
</button>
@@ -116,8 +129,8 @@
@endif
{{-- Create Modal --}}
<div x-data="{ open: false }"
x-on:open-modal.window="if($event.detail === 'create-threshold') open = true"
<div x-data="{ open: false, color: '#8b5cf6' }"
x-on:open-modal.window="if($event.detail === 'create-threshold') { open = true; color = '#8b5cf6'; }"
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
@@ -127,19 +140,20 @@
x-transition:leave-end="opacity-0"
class="fixed inset-0 z-50 overflow-y-auto" style="display: none;">
<div class="flex items-center justify-center min-h-screen px-4 py-4">
<div class="fixed inset-0 bg-gray-900/40 backdrop-blur-sm"></div>
<div class="relative bg-white rounded-2xl shadow-xl border border-slate-200 w-full max-w-lg mx-4">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm"></div>
<div class="relative bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden">
{{-- Modal header --}}
<div class="px-6 py-5 bg-gradient-to-r from-blue-600 to-indigo-600 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-sm">
<div class="w-10 h-10 rounded-xl bg-white/15 border border-white/20 flex items-center justify-center shadow-sm backdrop-blur-sm">
<i class="fa-solid fa-plus text-white text-sm"></i>
</div>
<div>
<h3 class="text-sm font-semibold text-gray-900">Tambah Ambang SP</h3>
<p class="text-xs text-gray-400">Batas poin Surat Peringatan baru</p>
<h3 class="text-sm font-bold text-white">Tambah Ambang SP</h3>
<p class="text-xs text-blue-100/80">Batas poin Surat Peringatan baru</p>
</div>
</div>
<button @click="open = false" class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition">
<button @click="open = false" class="w-8 h-8 rounded-lg flex items-center justify-center text-blue-100 hover:text-white hover:bg-white/10 transition">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@@ -148,36 +162,45 @@
@csrf
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Nama Threshold <span class="text-red-500">*</span></label>
<label class="block text-xs font-bold text-slate-600 mb-1.5">Nama Threshold <span class="text-red-500">*</span></label>
<input type="text" name="name" placeholder="SP 4, SP 5, dll" required
class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
class="input">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Minimal Poin <span class="text-red-500">*</span></label>
<label class="block text-xs font-bold text-slate-600 mb-1.5">Minimal Poin <span class="text-red-500">*</span></label>
<input type="number" name="min_points" min="0" value="200" required
class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
class="input">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Deskripsi</label>
<label class="block text-xs font-bold text-slate-600 mb-1.5">Deskripsi</label>
<input type="text" name="default_description" placeholder="Contoh: SP 4 — poin mencapai 200"
class="w-full px-4 py-2.5 border border-slate-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
class="input">
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Warna</label>
<label class="block text-xs font-bold text-slate-600 mb-1.5">Warna Identitas</label>
<div class="flex items-center gap-3">
<input type="color" name="color" value="#8b5cf6"
class="w-10 h-10 rounded-lg border border-slate-200 cursor-pointer p-0.5">
<span class="text-xs text-gray-400">Pilih warna identitas threshold</span>
<input type="color" name="color" x-model="color"
class="w-11 h-11 rounded-xl border border-slate-200 cursor-pointer p-1 bg-white shadow-sm">
<div class="flex items-center gap-1.5 flex-wrap">
<template x-for="c in ['#eab308','#f97316','#ef4444','#8b5cf6','#2563eb','#10b981','#ec4899','#06b6d4']" :key="c">
<button type="button" @click="color = c"
class="w-7 h-7 rounded-lg border border-slate-200 transition-transform hover:scale-110"
:style="'background-color:' + c"
:class="color === c ? 'ring-2 ring-offset-2 ring-blue-400 scale-110' : ''"
:title="c"></button>
</template>
</div>
</div>
<p class="text-[11px] text-slate-400 mt-2" x-text="'Warna dipilih: ' + color"></p>
</div>
</div>
<div class="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-gray-100">
<div class="flex items-center justify-end gap-3 mt-6 pt-4 border-t border-slate-100">
<button type="button" @click="open = false"
class="px-5 py-2.5 text-sm font-medium text-gray-600 bg-white border border-slate-200 rounded-xl hover:bg-gray-50 transition">
class="btn-outline">
Batal
</button>
<button type="submit"
class="px-5 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm inline-flex items-center gap-2">
class="btn-primary">
<i class="fa-solid fa-floppy-disk text-xs"></i>
Simpan
</button>
@@ -187,24 +210,37 @@
</div>
</div>
{{-- Info Card --}}
<div class="mt-6 bg-gradient-to-br from-blue-50 to-indigo-50/50 border border-blue-100 rounded-2xl p-5">
{{-- Info Card: tangga SP dinamis --}}
@if($thresholds->count() > 0)
<div class="mt-6 bg-gradient-to-br from-blue-50 via-indigo-50/60 to-white border border-blue-100 rounded-2xl p-5">
<div class="flex items-start gap-3">
<div class="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center flex-shrink-0">
<i class="fa-solid fa-circle-info text-blue-600 text-sm"></i>
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center flex-shrink-0 shadow-md shadow-blue-500/25">
<i class="fa-solid fa-circle-info text-white text-sm"></i>
</div>
<div>
<h4 class="text-sm font-bold text-blue-800 mb-1">Cara Kerja Ambang SP</h4>
<ul class="text-xs text-blue-700 space-y-1">
<li><i class="fa-solid fa-check mr-1.5 text-blue-500"></i>SP1 (≥50 poin) — peringatan pertama</li>
<li><i class="fa-solid fa-check mr-1.5 text-blue-500"></i>SP2 (≥100 poin) — peringatan kedua</li>
<li><i class="fa-solid fa-check mr-1.5 text-blue-500"></i>SP3 (≥150 poin) — peringatan ketiga</li>
<li><i class="fa-solid fa-star mr-1.5 text-amber-500"></i>Setiap poin dihitung dari akumulasi semua pelanggaran siswa</li>
<li><i class="fa-solid fa-star mr-1.5 text-amber-500"></i>Toggle aktif/nonaktif untuk mengontrol threshold yang digunakan</li>
<li><i class="fa-solid fa-star mr-1.5 text-amber-500"></i>Threshold baru otomatis aktif saat dibuat</li>
</ul>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-extrabold text-slate-800 mb-3">Tangga Surat Peringatan</h4>
<div class="flex flex-wrap items-center gap-2">
@foreach($thresholds as $idx => $th)
<div class="flex items-center gap-2">
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-bold shadow-sm"
style="background-color: {{ $th->color }}12; color: {{ $th->color }}; {{ $th->is_active ? '' : 'opacity:0.45;' }}">
<i class="fa-solid fa-bolt text-[9px]"></i>
{{ $th->name }} · ≥{{ $th->min_points }} poin
@if(! $th->is_active) <span class="text-[9px] font-bold uppercase">(nonaktif)</span> @endif
</span>
@if(! $loop->last)
<i class="fa-solid fa-arrow-right-long text-slate-300 text-xs"></i>
@endif
</div>
@endforeach
</div>
<p class="text-xs text-slate-400 mt-3 leading-relaxed">
<i class="fa-solid fa-star mr-1 text-amber-400"></i>SP diterbitkan otomatis saat poin siswa mencapai ambang minimal level tertinggi yang belum dimiliki.
Toggle untuk menonaktifkan level, atau hapus level yang tidak dipakai.
</p>
</div>
</div>
</div>
@endif
</div>
@endsection