feat: add attendance viewing functionality for class sessions, including a modal display of attendance records

This commit is contained in:
Yoga Pangestu 2026-03-29 23:26:05 +07:00
parent 1eaa8fff59
commit 0732a3b402
6 changed files with 102 additions and 31 deletions

View File

@ -6,6 +6,7 @@
use App\Filament\Resources\Learning\Attendances\AttendanceResource;
use App\Filament\Support\SystemNotification;
use App\Models\Attendance;
use App\Models\ClassSession;
use App\Models\Course;
use App\Models\CourseSchedule;
use App\Models\User;
@ -45,17 +46,10 @@ public function getSchedules()
return collect();
}
$today = now()->dayOfWeekIso;
$semester = app(GeneralSettings::class)->current_semester;
return CourseSchedule::query()
->where('day_of_week', $today)
->whereHas('course', function ($q) use ($semester) {
$q->where('semester', $semester);
})
return ClassSession::query()
->whereDate('date', now())
->with(['course', 'attendances' => function ($q) use ($student) {
$q->where('student_id', $student->id)
->whereDate('date', now()->toDateString());
$q->where('student_id', $student->id);
}])
->orderBy('start_time')
->get();
@ -138,7 +132,7 @@ public function scheduleCards()
});
}
public function attend(int $scheduleId): void
public function attend(int $sessionId): void
{
/** @var User $user */
$user = auth()->user();
@ -147,41 +141,46 @@ public function attend(int $scheduleId): void
return;
}
$schedule = CourseSchedule::find($scheduleId);
if (! $schedule) {
$session = ClassSession::find($sessionId);
if (! $session) {
return;
}
// Check if already attended
$existing = Attendance::where('student_id', $student->id)
->where('course_schedule_id', $scheduleId)
->whereDate('date', now()->toDateString())
->where('class_session_id', $sessionId)
->first();
if ($existing) {
SystemNotification::warning(
'Presensi Sudah Tercatat ⚠️',
'Anda telah melakukan presensi untuk jadwal perkuliahan ini pada hari ini.'
'Anda telah melakukan presensi untuk sesi perkuliahan ini.'
)->send();
return;
}
// Check time
$startTime = now()->setTimeFrom($schedule->start_time);
$startTime = $session->start_time;
if (now()->lessThan($startTime)) {
SystemNotification::warning(
'Presensi Belum Dibuka ⏳',
'Waktu presensi untuk jadwal perkuliahan ini belum dimulai. Silakan lakukan presensi setelah waktu dimulai.'
'Waktu presensi untuk sesi perkuliahan ini belum dimulai.'
)->send();
return;
}
// Find schedule for compatibility
$schedule = CourseSchedule::where('course_id', $session->course_id)
->where('day_of_week', $session->date->dayOfWeekIso)
->first();
Attendance::create([
'student_id' => $student->id,
'course_schedule_id' => $scheduleId,
'date' => now()->toDateString(),
'class_session_id' => $sessionId,
'course_schedule_id' => $schedule?->id,
'date' => $session->date->toDateString(),
'attended_at' => now(),
]);

View File

@ -178,6 +178,19 @@ public function generateSessionsAction(): Action
});
}
public function viewAttendanceAction(): Action
{
return Action::make('viewAttendance')
->label('Lihat Presensi')
->modalHeading(fn (array $arguments) => 'Daftar Presensi - Sesi Ke-'.ClassSession::find($arguments['session'])->session_number)
->modalSubmitAction(false)
->modalCancelActionLabel('Tutup')
->modalWidth(Width::ExtraLarge)
->modalContent(fn (array $arguments) => view('filament.resources.learning.class-sessions.attendance-modal', [
'attendances' => ClassSession::find($arguments['session'])->attendances()->with('student')->latest('attended_at')->get(),
]));
}
public function editSessionAction(): Action
{
return EditAction::make('editSession')

View File

@ -6,6 +6,7 @@
use App\Models\ClassSession;
use App\Models\Course;
use App\Settings\GeneralSettings;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\TextInput;
@ -13,6 +14,7 @@
use Filament\Forms\Contracts\HasForms;
use Filament\Resources\Pages\Page;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
@ -94,9 +96,9 @@ public function courses(): Collection
if ($this->search) {
$query->where(function ($q) {
$q->where('name', 'like', "%{$this->search}%")
->orWhere('code', 'like', "%{$this->search}%")
->orWhere('lecturer', 'like', "%{$this->search}%");
$q->where('name', 'like', '%'.$this->search.'%')
->orWhere('code', 'like', '%'.$this->search.'%')
->orWhere('lecturer', 'like', '%'.$this->search.'%');
});
}
@ -111,4 +113,17 @@ public function courses(): Collection
'url' => ClassSessionResource::getUrl('course', ['courseId' => $course->id]),
]);
}
public function viewAttendanceAction(): Action
{
return Action::make('viewAttendance')
->label('Lihat Presensi')
->modalHeading(fn (array $arguments) => 'Daftar Presensi - Sesi Ke-'.ClassSession::find($arguments['session'])->session_number)
->modalSubmitAction(false)
->modalCancelActionLabel('Tutup')
->modalWidth(Width::ExtraLarge)
->modalContent(fn (array $arguments) => view('filament.resources.learning.class-sessions.attendance-modal', [
'attendances' => ClassSession::find($arguments['session'])->attendances()->with('student')->latest('attended_at')->get(),
]));
}
}

View File

@ -0,0 +1,41 @@
<div class="space-y-3">
@if ($attendances->isEmpty())
<div class="flex flex-col items-center justify-center py-8 text-center">
<x-heroicon-o-user-group class="w-12 h-12 text-gray-400 mb-3" />
<p class="text-sm text-gray-500 font-medium">Belum ada mahasiswa yang melakukan presensi pada sesi ini.</p>
</div>
@else
<div class="fi-ta-content overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
<table class="w-full text-sm text-left divider-y dark:divider-white/5">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-4 py-3 font-bold text-gray-900 dark:text-white">Mahasiswa</th>
<th class="px-4 py-3 font-bold text-gray-900 dark:text-white text-right">Waktu Presensi</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/5">
@foreach ($attendances as $attendance)
<tr class="hover:bg-gray-50 dark:hover:bg-white/5 transition-colors">
<td class="px-4 py-3 text-gray-950 dark:text-white font-medium">
<div class="flex flex-col">
<span>{{ $attendance->student->full_name }}</span>
<span class="text-[10px] text-gray-500 dark:text-gray-400 font-normal uppercase tabular-nums">NIM: {{ $attendance->student->nim }}</span>
</div>
</td>
<td class="px-4 py-3 text-right">
<span class="inline-flex items-center rounded-md bg-success-50 dark:bg-success-400/10 px-2 py-1 text-xs font-bold text-success-700 dark:text-success-400 ring-1 ring-inset ring-success-600/20 tabular-nums">
{{ $attendance->attended_at->format('H:i') }} WIB
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-4 flex items-center justify-between px-2">
<p class="text-xs text-gray-500 font-medium">
Total Kehadiran: <span class="text-primary-600 dark:text-primary-400 font-bold font-mono">{{ $attendances->count() }} Mahasiswa</span>
</p>
</div>
@endif
</div>

View File

@ -57,14 +57,16 @@ class="mt-2 flex items-center gap-1.5 text-[10px] text-gray-400 dark:text-gray-5
<x-heroicon-m-clock class="w-5 h-5" />
</div>
@else
<div class="flex flex-col items-center justify-center min-w-[60px] p-2">
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 leading-none">
{{ $session->attendances_count }}
</span>
<span class="text-[9px] uppercase font-bold text-gray-500 mt-1 leading-none tracking-wider">
Hadir
</span>
</div>
<div class="flex flex-col items-center justify-center min-w-[60px] p-2 hover:bg-white/50 dark:hover:bg-white/5 rounded-lg transition-colors cursor-pointer"
onclick="event.preventDefault(); event.stopPropagation();"
wire:click="mountAction('viewAttendance', { session: {{ $session->id }} })">
<span class="text-lg font-bold text-primary-600 dark:text-primary-400 leading-none">
{{ $session->attendances_count }}
</span>
<span class="text-[9px] uppercase font-bold text-gray-500 mt-1 leading-none tracking-wider">
Hadir
</span>
</div>
@endif
</div>
</div>

View File

@ -40,6 +40,7 @@ class="flex items-center gap-1.5 font-bold text-success-600 dark:text-success-40
</div>
<div class="flex items-center gap-1">
{{ ($this->viewAttendanceAction)(['session' => $session->id]) }}
{{ ($this->editSessionAction)(['session' => $session->id]) }}
{{ ($this->deleteSessionAction)(['session' => $session->id]) }}
</div>