fix: presensi - filter tanggal-kelas live, urutan tanggal di depan, pembersihan data yatim

This commit is contained in:
Novy San87
2026-08-02 20:57:14 +07:00
parent 6e256661dd
commit 58ffd06625
4 changed files with 137 additions and 29 deletions
@@ -26,24 +26,34 @@ class AttendanceController extends Controller
$thisMonth = now()->month;
$thisYear = now()->year;
// Wali kelas murni → hanya siswa di kelas yang diwalikan
$scopedStudentIds = null;
if (auth()->user()->isScopedWaliKelas()) {
$scopedStudentIds = \App\Models\Student::whereIn('class_id', auth()->user()->homeroomClassIds())->pluck('id')->all();
}
// Hitung berdasarkan jumlah siswa unik, bukan total record per jam
$todayStudents = \App\Models\Attendance::where('date', $today)
->when($scopedStudentIds, fn ($q) => $q->whereIn('student_id', $scopedStudentIds))
->distinct('student_id')
->count('student_id');
$todayAlphaStudents = \App\Models\Attendance::where('date', $today)
->where('status', 'alpha')
->when($scopedStudentIds, fn ($q) => $q->whereIn('student_id', $scopedStudentIds))
->distinct('student_id')
->count('student_id');
$monthStudents = \App\Models\Attendance::whereMonth('date', $thisMonth)
->whereYear('date', $thisYear)
->when($scopedStudentIds, fn ($q) => $q->whereIn('student_id', $scopedStudentIds))
->distinct('student_id')
->count('student_id');
$monthAlphaStudents = \App\Models\Attendance::whereMonth('date', $thisMonth)
->whereYear('date', $thisYear)
->where('status', 'alpha')
->when($scopedStudentIds, fn ($q) => $q->whereIn('student_id', $scopedStudentIds))
->distinct('student_id')
->count('student_id');
@@ -53,12 +63,14 @@ class AttendanceController extends Controller
$totalRows = \App\Models\Attendance::selectRaw('date, COUNT(DISTINCT student_id) as total')
->whereBetween('date', [$calendarStart, $calendarEnd])
->when($scopedStudentIds, fn ($q) => $q->whereIn('student_id', $scopedStudentIds))
->groupBy('date')
->pluck('total', 'date');
$alphaRows = \App\Models\Attendance::selectRaw('date, COUNT(DISTINCT student_id) as alpha')
->whereBetween('date', [$calendarStart, $calendarEnd])
->where('status', 'alpha')
->when($scopedStudentIds, fn ($q) => $q->whereIn('student_id', $scopedStudentIds))
->groupBy('date')
->pluck('alpha', 'date');
@@ -77,6 +89,36 @@ class AttendanceController extends Controller
));
}
public function availability(Request $request): JsonResponse
{
$date = $request->input('date', now()->toDateString());
$classNames = Student::where('is_active', true)
->whereNotNull('class_name')
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereIn('class_id', auth()->user()->homeroomClassIds()))
->distinct()
->orderBy('class_name')
->pluck('class_name');
$result = [];
foreach ($classNames as $cn) {
$studentIdsInClass = Student::where('is_active', true)
->where('class_name', $cn)
->pluck('id');
$count = Attendance::where('date', $date)
->whereIn('student_id', $studentIdsInClass)
->distinct('student_id')
->count('student_id');
$result[$cn] = [
'has_data' => $count > 0,
'recorded' => $count,
'total' => $studentIdsInClass->count(),
];
}
return response()->json($result);
}
public function create(Request $request): View
{
$date = $request->input('date', now()->toDateString());
@@ -101,6 +143,7 @@ class AttendanceController extends Controller
$classNames = Student::where('is_active', true)
->whereNotNull('class_name')
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereIn('class_id', auth()->user()->homeroomClassIds()))
->distinct()
->orderBy('class_name')
->pluck('class_name');
@@ -143,6 +186,13 @@ class AttendanceController extends Controller
$date = $request->input('date');
$class_name = $request->input('class_name');
// Wali kelas murni hanya boleh input presensi kelasnya sendiri
if (auth()->user()->isScopedWaliKelas()) {
$ownNames = \App\Models\Classes::whereIn('id', auth()->user()->homeroomClassIds())->pluck('name')->all();
abort_unless($class_name && in_array($class_name, $ownNames, true), 403);
}
$saved = 0;
$autoViolations = 0;
$alphaIds = [];
@@ -285,6 +335,7 @@ class AttendanceController extends Controller
$classNames = Student::where('is_active', true)
->whereNotNull('class_name')
->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereIn('class_id', auth()->user()->homeroomClassIds()))
->distinct()
->orderBy('class_name')
->pluck('class_name');
+45 -18
View File
@@ -19,36 +19,36 @@
{{-- Pilih Kelas + Tanggal --}}
<div class="rounded-2xl bg-gradient-to-br from-white to-gray-50/80 border border-gray-200 shadow-sm mb-6 transition-all duration-200 hover:shadow-md">
<form method="GET" class="p-6">
<form method="GET" x-data="attendanceLink()" class="p-6">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">
<i class="fa-solid fa-calendar text-gray-400 mr-1"></i> Tanggal <span class="text-red-500">*</span>
</label>
<input type="date" name="date" value="{{ $date }}" x-model="date" @change="refreshStatus($event)"
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-white focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition shadow-sm">
</div>
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">
<i class="fa-solid fa-school text-gray-400 mr-1"></i> Kelas <span class="text-red-500">*</span>
</label>
<select name="class_name" required
<select name="class_name" required x-model="sel"
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-white focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition shadow-sm">
<option value="">Pilih kelas...</option>
@foreach($classNames as $cn)
@php
$stat = $classAttendanceStatus[$cn] ?? ['has_data' => false, 'recorded' => 0, 'total' => 0];
$label = $stat['has_data']
? '✅ ' . $cn . ' (' . $stat['recorded'] . '/' . $stat['total'] . ')'
: '⬜ ' . $cn;
@endphp
<option value="{{ $cn }}" @selected($className == $cn)>{{ $label }}</option>
@endforeach
<template x-for="cn in classList" :key="cn">
<option :value="cn" x-text="label(cn)"></option>
</template>
</select>
<div class="mt-1.5 flex items-center gap-3 text-[10px] text-gray-400">
<span class="inline-flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-gray-300"></span> Belum</span>
<span class="inline-flex items-center gap-1"><span class="w-2 h-2 rounded-sm bg-emerald-400"></span> Terisi</span>
</div>
</div>
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">
<i class="fa-solid fa-calendar text-gray-400 mr-1"></i> Tanggal <span class="text-red-500">*</span>
</label>
<input type="date" name="date" value="{{ $date }}"
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-white focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition shadow-sm">
<p class="mt-2 text-[11px] font-semibold text-emerald-600" x-show="terisiCount > 0">
<i class="fa-solid fa-circle-check"></i> <span x-text="terisiCount"></span> kelas terisi pada <span class="underline decoration-dotted" x-text="date"></span>
</p>
<p class="mt-2 text-[11px] font-semibold text-amber-600" x-show="terisiCount === 0">
<i class="fa-solid fa-circle-info"></i> Belum ada kelas terisi pada <span class="underline decoration-dotted" x-text="date"></span> — coba pilih tanggal lain
</p>
</div>
<div class="flex items-center pt-[22px]">
<button type="submit"
@@ -308,5 +308,32 @@ function rotateStatus(studentId, lessonHour) {
btn.textContent = STATUS_LABELS[next];
btn.title = STATUS_FULL[next];
}
function attendanceLink() {
return {
date: '{{ $date }}',
sel: '{{ $className }}',
status: @json($classAttendanceStatus),
get classList() {
return Object.keys(this.status || {});
},
get terisiCount() {
return Object.values(this.status || {}).filter(s => s.has_data).length;
},
label(cn) {
const s = this.status[cn];
if (!s) return cn;
return s.has_data ? '✅ ' + cn + ' (' + s.recorded + '/' + s.total + ')' : '⬜ ' + cn;
},
async refreshStatus(ev) {
if (ev && ev.target && ev.target.value) this.date = ev.target.value;
try {
const res = await fetch('{{ route("attendances.availability") }}?date=' + encodeURIComponent(this.date));
const j = await res.json();
this.status = j;
} catch (e) {}
},
};
}
</script>
@endpush
+11 -11
View File
@@ -48,17 +48,6 @@
<form method="GET">
<input type="hidden" name="type" value="{{ $type }}">
<div class="grid grid-cols-1 sm:grid-cols-4 gap-3">
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Kelas</label>
<select name="class_name"
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">
<option value="">Semua Kelas</option>
@foreach($classNames as $cn)
<option value="{{ $cn }}" @selected($className == $cn)>{{ $cn }}</option>
@endforeach
</select>
</div>
@if($type == 'daily')
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Tanggal</label>
@@ -92,6 +81,17 @@
</div>
@endif
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Kelas</label>
<select name="class_name"
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">
<option value="">Semua Kelas</option>
@foreach($classNames as $cn)
<option value="{{ $cn }}" @selected($className == $cn)>{{ $cn }}</option>
@endforeach
</select>
</div>
<div class="flex items-end">
<button type="submit"
class="w-full px-4 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
+30
View File
@@ -8,6 +8,7 @@ use App\Http\Controllers\Admin\SyncController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\FaceIdController;
use App\Http\Controllers\SpLetterController;
use App\Http\Controllers\StudentReportController;
use App\Http\Controllers\ViolationController;
@@ -45,6 +46,19 @@ Route::middleware('auth')->group(function () {
Route::post('/violations', [ViolationController::class, 'store'])->name('violations.store');
});
// Face ID: scan wajah (permission khusus)
Route::middleware('permission:face-scan')->group(function () {
Route::get('/face/scan', [FaceIdController::class, 'scan'])->name('face.scan');
Route::post('/face/scan', [FaceIdController::class, 'scanVerify'])->name('face.scan.verify');
});
// Face ID: registrasi wajah (permission khusus)
Route::middleware('permission:face-register')->group(function () {
Route::get('/face/register', [FaceIdController::class, 'register'])->name('face.register');
Route::post('/face/register', [FaceIdController::class, 'registerStore'])->name('face.register.store');
Route::post('/face/register/quick', [FaceIdController::class, 'registerQuick'])->name('face.register.quick');
});
Route::middleware('permission:view-violations')->group(function () {
Route::get('/violations', [ViolationController::class, 'index'])->name('violations.index');
Route::get('/violations/{violation}', [ViolationController::class, 'show'])->name('violations.show');
@@ -56,6 +70,11 @@ Route::middleware('auth')->group(function () {
Route::get('/api/students/search', [ViolationController::class, 'searchStudents'])->name('api.students.search');
});
// Notifikasi WA ke orang tua (permission khusus)
Route::middleware('permission:notify-parent-wa')->group(function () {
Route::get('/violations/{violation}/notify-wa', [ViolationController::class, 'notifyParentWa'])->name('violations.notify-wa');
});
Route::middleware('permission:view-students')->group(function () {
Route::get('/students', [StudentReportController::class, 'index'])->name('students.index');
Route::get('/students/{student}', [StudentReportController::class, 'show'])->name('students.show');
@@ -65,6 +84,7 @@ Route::middleware('auth')->group(function () {
Route::middleware('permission:manage-attendance')->group(function () {
Route::get('/attendances', [\App\Http\Controllers\AttendanceController::class, 'index'])->name('attendances.index');
Route::get('/attendances/create', [\App\Http\Controllers\AttendanceController::class, 'create'])->name('attendances.create');
Route::get('/attendances/availability', [\App\Http\Controllers\AttendanceController::class, 'availability'])->name('attendances.availability');
Route::post('/attendances', [\App\Http\Controllers\AttendanceController::class, 'store'])->name('attendances.store');
Route::get('/attendances/recap', [\App\Http\Controllers\AttendanceController::class, 'recap'])->name('attendances.recap');
Route::get('/attendances/calendar-data', [\App\Http\Controllers\AttendanceController::class, 'calendarData'])->name('attendances.calendar-data');
@@ -91,6 +111,16 @@ Route::middleware('auth')->group(function () {
Route::post('/settings/violation-types', [MasterDataController::class, 'storeType'])->name('settings.violation-types.store');
Route::put('/settings/violation-types/{type}', [MasterDataController::class, 'updateType'])->name('settings.violation-types.update');
Route::delete('/settings/violation-types/{type}', [MasterDataController::class, 'destroyType'])->name('settings.violation-types.destroy');
Route::get('/settings/handling-types', [MasterDataController::class, 'handlingTypes'])->name('settings.handling-types');
Route::post('/settings/handling-types', [MasterDataController::class, 'storeHandlingType'])->name('settings.handling-types.store');
Route::post('/settings/handling-types/reorder', [MasterDataController::class, 'reorderHandlingTypes'])->name('settings.handling-types.reorder');
Route::put('/settings/handling-types/{type}', [MasterDataController::class, 'updateHandlingType'])->name('settings.handling-types.update');
Route::delete('/settings/handling-types/{type}', [MasterDataController::class, 'destroyHandlingType'])->name('settings.handling-types.destroy');
Route::post('/settings/handling-roles', [MasterDataController::class, 'storeHandlingRole'])->name('settings.handling-roles.store');
Route::post('/settings/handling-roles/reorder', [MasterDataController::class, 'reorderHandlingRoles'])->name('settings.handling-roles.reorder');
Route::delete('/settings/handling-roles/{role}', [MasterDataController::class, 'destroyHandlingRole'])->name('settings.handling-roles.destroy');
Route::get('/settings/homeroom', [MasterDataController::class, 'homeroomTeachers'])->name('settings.homeroom');
Route::put('/settings/homeroom', [MasterDataController::class, 'updateHomeroomTeacher'])->name('settings.homeroom.update');
});
Route::middleware('permission:thresholds-manage')->group(function () {