feat: add attendance viewing functionality for class sessions, including a modal display of attendance records
This commit is contained in:
parent
1eaa8fff59
commit
0732a3b402
@ -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(),
|
||||
]);
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user