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.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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user