Sederhanakan ambang SP: hapus kolom max_points

- max_points tidak pernah dipakai logika penerbitan SP (trigger hanya baca
  min_points: level dgn ambang tertinggi <= total poin siswa)
- Migrasi drop kolom; model, MasterDataController, ResetController (seeder)
  dibersihkan; form Ambang SP & form tambah cukup satu angka (Min Poin)
- Dashboard Ambang SP: batas atas level dihitung dari min_points level
  berikutnya (level terakhir = tak terbatas), urutan dijamin orderBy min_points
- Test: 29 passed / 99 assertion
This commit is contained in:
Novy San87
2026-08-03 14:22:44 +07:00
parent e815bd28d5
commit 279ef07ae2
8 changed files with 47 additions and 35 deletions
+1
View File
@@ -25,3 +25,4 @@ _ide_helper.php
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
Thumbs.db Thumbs.db
.env.bak*
@@ -300,7 +300,6 @@ class MasterDataController extends Controller
'thresholds' => ['required', 'array'], 'thresholds' => ['required', 'array'],
'thresholds.*.id' => ['required', 'exists:sp_thresholds,id'], 'thresholds.*.id' => ['required', 'exists:sp_thresholds,id'],
'thresholds.*.min_points' => ['required', 'integer', 'min:0'], 'thresholds.*.min_points' => ['required', 'integer', 'min:0'],
'thresholds.*.max_points' => ['nullable', 'integer', 'min:0'],
'thresholds.*.name' => ['required', 'string', 'max:255'], 'thresholds.*.name' => ['required', 'string', 'max:255'],
'thresholds.*.default_description' => ['nullable', 'string', 'max:500'], 'thresholds.*.default_description' => ['nullable', 'string', 'max:500'],
]); ]);
@@ -311,7 +310,6 @@ class MasterDataController extends Controller
$update = [ $update = [
'name' => $data['name'], 'name' => $data['name'],
'min_points' => $data['min_points'], 'min_points' => $data['min_points'],
'max_points' => $data['max_points'],
'default_description' => $data['default_description'], 'default_description' => $data['default_description'],
'is_active' => isset($inputs[$idx]['is_active']), 'is_active' => isset($inputs[$idx]['is_active']),
]; ];
@@ -328,7 +326,6 @@ class MasterDataController extends Controller
$data = $request->validate([ $data = $request->validate([
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'min_points' => ['required', 'integer', 'min:0'], 'min_points' => ['required', 'integer', 'min:0'],
'max_points' => ['nullable', 'integer', 'min:0'],
'default_description' => ['nullable', 'string', 'max:500'], 'default_description' => ['nullable', 'string', 'max:500'],
'color' => ['nullable', 'string', 'max:20'], 'color' => ['nullable', 'string', 'max:20'],
]); ]);
@@ -338,7 +335,6 @@ class MasterDataController extends Controller
'name' => $data['name'], 'name' => $data['name'],
'slug' => $slug, 'slug' => $slug,
'min_points' => $data['min_points'], 'min_points' => $data['min_points'],
'max_points' => $data['max_points'],
'default_description' => $data['default_description'], 'default_description' => $data['default_description'],
'color' => $data['color'] ?? '#6b7280', 'color' => $data['color'] ?? '#6b7280',
'is_active' => true, 'is_active' => true,
@@ -199,9 +199,9 @@ class ResetController extends Controller
private function seedDefaultThresholds(): void private function seedDefaultThresholds(): void
{ {
$thresholds = [ $thresholds = [
['name' => 'SP 1', 'slug' => 'sp-1', 'min_points' => 50, 'max_points' => 99, 'color' => '#eab308', 'is_active' => true, 'default_description' => 'Surat Peringatan 1 — poin mencapai 50', 'created_at' => now(), 'updated_at' => now()], ['name' => 'SP 1', 'slug' => 'sp-1', 'min_points' => 50, 'color' => '#eab308', 'is_active' => true, 'default_description' => 'Surat Peringatan 1 — poin mencapai 50', 'created_at' => now(), 'updated_at' => now()],
['name' => 'SP 2', 'slug' => 'sp-2', 'min_points' => 100, 'max_points' => 149, 'color' => '#f97316', 'is_active' => true, 'default_description' => 'Surat Peringatan 2 — poin mencapai 100', 'created_at' => now(), 'updated_at' => now()], ['name' => 'SP 2', 'slug' => 'sp-2', 'min_points' => 100, 'color' => '#f97316', 'is_active' => true, 'default_description' => 'Surat Peringatan 2 — poin mencapai 100', 'created_at' => now(), 'updated_at' => now()],
['name' => 'SP 3', 'slug' => 'sp-3', 'min_points' => 150, 'max_points' => null, 'color' => '#ef4444', 'is_active' => true, 'default_description' => 'Surat Peringatan 3 — poin mencapai 150', 'created_at' => now(), 'updated_at' => now()], ['name' => 'SP 3', 'slug' => 'sp-3', 'min_points' => 150, 'color' => '#ef4444', 'is_active' => true, 'default_description' => 'Surat Peringatan 3 — poin mencapai 150', 'created_at' => now(), 'updated_at' => now()],
]; ];
DB::table('sp_thresholds')->insert($thresholds); DB::table('sp_thresholds')->insert($thresholds);
} }
+1 -1
View File
@@ -59,7 +59,7 @@ class DashboardController extends Controller
->where('is_read', false) ->where('is_read', false)
->count(); ->count();
$spThresholds = \App\Models\SpThreshold::where('is_active', true)->get(); $spThresholds = \App\Models\SpThreshold::where('is_active', true)->orderBy('min_points')->get();
// Data untuk calendar: pelanggaran per tanggal di bulan ini // Data untuk calendar: pelanggaran per tanggal di bulan ini
$calendarData = Violation::selectRaw('DATE(violation_date) as date, COUNT(*) as count') $calendarData = Violation::selectRaw('DATE(violation_date) as date, COUNT(*) as count')
+1 -2
View File
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
class SpThreshold extends Model class SpThreshold extends Model
{ {
protected $fillable = [ protected $fillable = [
'name', 'slug', 'min_points', 'max_points', 'name', 'slug', 'min_points',
'color', 'default_description', 'is_active', 'color', 'default_description', 'is_active',
]; ];
@@ -15,7 +15,6 @@ class SpThreshold extends Model
{ {
return [ return [
'min_points' => 'integer', 'min_points' => 'integer',
'max_points' => 'integer',
'is_active' => 'boolean', 'is_active' => 'boolean',
]; ];
} }
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* max_points tidak pernah dipakai dalam logika penerbitan SP
* (trigger hanya membaca min_points). Hapus agar desain simpel.
*/
public function up(): void
{
Schema::table('sp_thresholds', function (Blueprint $table) {
$table->dropColumn('max_points');
});
}
public function down(): void
{
Schema::table('sp_thresholds', function (Blueprint $table) {
$table->integer('max_points')->nullable()->after('min_points');
});
}
};
+9 -5
View File
@@ -218,12 +218,16 @@
</div> </div>
</div> </div>
<div class="p-5 space-y-3"> <div class="p-5 space-y-3">
@foreach($spThresholds as $threshold) @foreach($spThresholds as $i => $threshold)
@php @php
$studentsAtRisk = \App\Models\Student::where('is_active', true)->get()->filter(function($s) use ($threshold) { // Batas atas level ini = ambang minimal level berikutnya
$nextMin = $spThresholds[$i + 1]->min_points ?? null;
$studentsAtRisk = \App\Models\Student::where('is_active', true)->get()->filter(function($s) use ($threshold, $nextMin) {
$pts = $s->total_points; $pts = $s->total_points;
$maxOk = $threshold->max_points ?? 9999; if ($nextMin !== null) {
return $pts >= $threshold->min_points && $pts <= $maxOk; return $pts >= $threshold->min_points && $pts < $nextMin;
}
return $pts >= $threshold->min_points;
}); });
$count = $studentsAtRisk->count(); $count = $studentsAtRisk->count();
@endphp @endphp
@@ -232,7 +236,7 @@
<div class="flex items-center justify-between relative z-10"> <div class="flex items-center justify-between relative z-10">
<div> <div>
<p class="text-sm font-bold" style="color: {{ $threshold->color }}">{{ $threshold->name }}</p> <p class="text-sm font-bold" style="color: {{ $threshold->color }}">{{ $threshold->name }}</p>
<p class="text-xs text-gray-400 mt-0.5">{{ $threshold->min_points }}{{ $threshold->max_points ? '–'.$threshold->max_points : '+' }} poin</p> <p class="text-xs text-gray-400 mt-0.5">{{ $threshold->min_points }}{{ $nextMin !== null ? '–'.($nextMin - 1) : '+' }} poin</p>
</div> </div>
<div class="text-center"> <div class="text-center">
<p class="text-2xl font-bold" style="color: {{ $threshold->color }}">{{ $count }}</p> <p class="text-2xl font-bold" style="color: {{ $threshold->color }}">{{ $count }}</p>
+6 -20
View File
@@ -53,9 +53,7 @@
style="{{ $t->is_active ? 'background-color: ' . $t->color : '' }}"></div> style="{{ $t->is_active ? 'background-color: ' . $t->color : '' }}"></div>
</label> </label>
</div> </div>
<p class="text-xs text-gray-400 mt-0.5">Min {{ $t->min_points }} poin <p class="text-xs text-gray-400 mt-0.5">Mulai dari {{ $t->min_points }} poin</p>
@if($t->max_points) – Maks {{ $t->max_points }} poin @else + (tak terbatas) @endif
</p>
</div> </div>
</div> </div>
<div class="flex items-center gap-1 flex-shrink-0"> <div class="flex items-center gap-1 flex-shrink-0">
@@ -73,7 +71,7 @@
</div> </div>
{{-- Fields --}} {{-- Fields --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<div> <div>
<label class="block text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-1">Nama</label> <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 }}" <input type="text" name="thresholds[{{ $loop->index }}][name]" value="{{ $t->name }}"
@@ -84,11 +82,6 @@
<input type="number" name="thresholds[{{ $loop->index }}][min_points]" value="{{ $t->min_points }}" min="0" <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-gray-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="w-full px-3.5 py-2 border border-gray-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>
<div>
<label class="block text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-1">Maks Poin</label>
<input type="number" name="thresholds[{{ $loop->index }}][max_points]" value="{{ $t->max_points }}" min="0" placeholder="Tak terbatas"
class="w-full px-3.5 py-2 border border-gray-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> <div>
<label class="block text-[11px] font-semibold text-gray-400 uppercase tracking-wider mb-1">Deskripsi</label> <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 }}" <input type="text" name="thresholds[{{ $loop->index }}][default_description]" value="{{ $t->default_description }}"
@@ -159,17 +152,10 @@
<input type="text" name="name" placeholder="SP 4, SP 5, dll" required <input type="text" name="name" placeholder="SP 4, SP 5, dll" required
class="w-full px-4 py-2.5 border border-gray-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="w-full px-4 py-2.5 border border-gray-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>
<div class="grid grid-cols-2 gap-4"> <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-sm font-semibold text-gray-700 mb-1">Minimal Poin <span class="text-red-500">*</span></label> <input type="number" name="min_points" min="0" value="200" required
<input type="number" name="min_points" min="0" value="200" required class="w-full px-4 py-2.5 border border-gray-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="w-full px-4 py-2.5 border border-gray-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-sm font-semibold text-gray-700 mb-1">Maksimal Poin</label>
<input type="number" name="max_points" min="0" placeholder="Tak terbatas"
class="w-full px-4 py-2.5 border border-gray-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> </div>
<div> <div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Deskripsi</label> <label class="block text-sm font-semibold text-gray-700 mb-1">Deskripsi</label>