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:
@@ -25,3 +25,4 @@ _ide_helper.php
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
.env.bak*
|
||||
|
||||
@@ -300,7 +300,6 @@ class MasterDataController extends Controller
|
||||
'thresholds' => ['required', 'array'],
|
||||
'thresholds.*.id' => ['required', 'exists:sp_thresholds,id'],
|
||||
'thresholds.*.min_points' => ['required', 'integer', 'min:0'],
|
||||
'thresholds.*.max_points' => ['nullable', 'integer', 'min:0'],
|
||||
'thresholds.*.name' => ['required', 'string', 'max:255'],
|
||||
'thresholds.*.default_description' => ['nullable', 'string', 'max:500'],
|
||||
]);
|
||||
@@ -311,7 +310,6 @@ class MasterDataController extends Controller
|
||||
$update = [
|
||||
'name' => $data['name'],
|
||||
'min_points' => $data['min_points'],
|
||||
'max_points' => $data['max_points'],
|
||||
'default_description' => $data['default_description'],
|
||||
'is_active' => isset($inputs[$idx]['is_active']),
|
||||
];
|
||||
@@ -328,7 +326,6 @@ class MasterDataController extends Controller
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'min_points' => ['required', 'integer', 'min:0'],
|
||||
'max_points' => ['nullable', 'integer', 'min:0'],
|
||||
'default_description' => ['nullable', 'string', 'max:500'],
|
||||
'color' => ['nullable', 'string', 'max:20'],
|
||||
]);
|
||||
@@ -338,7 +335,6 @@ class MasterDataController extends Controller
|
||||
'name' => $data['name'],
|
||||
'slug' => $slug,
|
||||
'min_points' => $data['min_points'],
|
||||
'max_points' => $data['max_points'],
|
||||
'default_description' => $data['default_description'],
|
||||
'color' => $data['color'] ?? '#6b7280',
|
||||
'is_active' => true,
|
||||
|
||||
@@ -199,9 +199,9 @@ class ResetController extends Controller
|
||||
private function seedDefaultThresholds(): void
|
||||
{
|
||||
$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 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 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 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, '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, '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);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class DashboardController extends Controller
|
||||
->where('is_read', false)
|
||||
->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
|
||||
$calendarData = Violation::selectRaw('DATE(violation_date) as date, COUNT(*) as count')
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class SpThreshold extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name', 'slug', 'min_points', 'max_points',
|
||||
'name', 'slug', 'min_points',
|
||||
'color', 'default_description', 'is_active',
|
||||
];
|
||||
|
||||
@@ -15,7 +15,6 @@ class SpThreshold extends Model
|
||||
{
|
||||
return [
|
||||
'min_points' => 'integer',
|
||||
'max_points' => 'integer',
|
||||
'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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -218,12 +218,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 space-y-3">
|
||||
@foreach($spThresholds as $threshold)
|
||||
@foreach($spThresholds as $i => $threshold)
|
||||
@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;
|
||||
$maxOk = $threshold->max_points ?? 9999;
|
||||
return $pts >= $threshold->min_points && $pts <= $maxOk;
|
||||
if ($nextMin !== null) {
|
||||
return $pts >= $threshold->min_points && $pts < $nextMin;
|
||||
}
|
||||
return $pts >= $threshold->min_points;
|
||||
});
|
||||
$count = $studentsAtRisk->count();
|
||||
@endphp
|
||||
@@ -232,7 +236,7 @@
|
||||
<div class="flex items-center justify-between relative z-10">
|
||||
<div>
|
||||
<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 class="text-center">
|
||||
<p class="text-2xl font-bold" style="color: {{ $threshold->color }}">{{ $count }}</p>
|
||||
|
||||
@@ -53,9 +53,7 @@
|
||||
style="{{ $t->is_active ? 'background-color: ' . $t->color : '' }}"></div>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-0.5">Min {{ $t->min_points }} poin
|
||||
@if($t->max_points) – Maks {{ $t->max_points }} poin @else + (tak terbatas) @endif
|
||||
</p>
|
||||
<p class="text-xs text-gray-400 mt-0.5">Mulai dari {{ $t->min_points }} poin</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 flex-shrink-0">
|
||||
@@ -73,7 +71,7 @@
|
||||
</div>
|
||||
|
||||
{{-- 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>
|
||||
<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 }}"
|
||||
@@ -84,11 +82,6 @@
|
||||
<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">
|
||||
</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>
|
||||
<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 }}"
|
||||
@@ -159,17 +152,10 @@
|
||||
<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">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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
|
||||
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>
|
||||
<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
|
||||
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">Deskripsi</label>
|
||||
|
||||
Reference in New Issue
Block a user