feat: implement missed attendance handling and split schedule cards into today and missed sections
This commit is contained in:
parent
7f29146452
commit
75c71a1ac9
@ -60,6 +60,18 @@ public function historyDescription(): string
|
|||||||
return SystemNotification::getByKey('labels.attendance_history.description');
|
return SystemNotification::getByKey('labels.attendance_history.description');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function missedHeading(): string
|
||||||
|
{
|
||||||
|
return SystemNotification::getByKey('labels.missed_attendance.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function missedDescription(): string
|
||||||
|
{
|
||||||
|
return SystemNotification::getByKey('labels.missed_attendance.description');
|
||||||
|
}
|
||||||
|
|
||||||
#[Computed]
|
#[Computed]
|
||||||
public function emptyHeading(): string
|
public function emptyHeading(): string
|
||||||
{
|
{
|
||||||
@ -110,7 +122,8 @@ public function getSchedules()
|
|||||||
->with(['course', 'attendances' => function ($q) use ($student) {
|
->with(['course', 'attendances' => function ($q) use ($student) {
|
||||||
$q->where('student_id', $student->id);
|
$q->where('student_id', $student->id);
|
||||||
}])
|
}])
|
||||||
->orderBy('start_time')
|
->orderBy('date', 'desc')
|
||||||
|
->orderBy('start_time', 'asc')
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,9 +136,14 @@ public function scheduleCards()
|
|||||||
$isAttended = $attendance !== null;
|
$isAttended = $attendance !== null;
|
||||||
$isSent = $schedule->is_sent_to_lecturer;
|
$isSent = $schedule->is_sent_to_lecturer;
|
||||||
|
|
||||||
|
$sessionStart = $schedule->date->copy()->setTimeFrom($schedule->start_time);
|
||||||
$now = now();
|
$now = now();
|
||||||
$startTime = now()->setTimeFrom($schedule->start_time);
|
$isPassed = $now->greaterThanOrEqualTo($sessionStart);
|
||||||
$canAttend = $now->greaterThanOrEqualTo($startTime) && ! $isSent;
|
$isMissed = $schedule->date->isPast() && ! $schedule->date->isToday();
|
||||||
|
|
||||||
|
// If it's a missed session, we don't care about the time match anymore,
|
||||||
|
// as long as it's not sent yet.
|
||||||
|
$canAttend = ! $isSent && ($isMissed || $isPassed);
|
||||||
|
|
||||||
$statusLabel = 'Belum Presensi';
|
$statusLabel = 'Belum Presensi';
|
||||||
$statusColor = 'warning';
|
$statusColor = 'warning';
|
||||||
@ -139,19 +157,24 @@ public function scheduleCards()
|
|||||||
$statusLabel = 'Terkirim';
|
$statusLabel = 'Terkirim';
|
||||||
$statusColor = 'danger';
|
$statusColor = 'danger';
|
||||||
$statusIcon = 'heroicon-o-paper-airplane';
|
$statusIcon = 'heroicon-o-paper-airplane';
|
||||||
} elseif (! $canAttend) {
|
} elseif (! $isPassed && ! $isMissed) {
|
||||||
$statusLabel = 'Belum Dimulai';
|
$statusLabel = 'Belum Dimulai';
|
||||||
$statusColor = 'gray';
|
$statusColor = 'gray';
|
||||||
$statusIcon = 'heroicon-o-lock-closed';
|
$statusIcon = 'heroicon-o-lock-closed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$isMissed = $schedule->date->isPast() && ! $schedule->date->isToday();
|
||||||
|
|
||||||
return (object) [
|
return (object) [
|
||||||
'id' => $schedule->id,
|
'id' => $schedule->id,
|
||||||
'course_name' => $schedule->course?->name,
|
'course_name' => $schedule->course?->name,
|
||||||
'lecturer_name' => $schedule->course?->lecturer ?? 'Belum Ditentukan',
|
'lecturer_name' => $schedule->course?->lecturer ?? 'Belum Ditentukan',
|
||||||
|
'session_number' => $schedule->session_number,
|
||||||
|
'date' => $schedule->date?->translatedFormat('l, d F Y'),
|
||||||
'time_range' => $schedule->start_time?->format('H:i').' - '.$schedule->end_time?->format('H:i'),
|
'time_range' => $schedule->start_time?->format('H:i').' - '.$schedule->end_time?->format('H:i'),
|
||||||
'is_attended' => $isAttended,
|
'is_attended' => $isAttended,
|
||||||
'is_sent_to_lecturer' => $isSent,
|
'is_sent_to_lecturer' => $isSent,
|
||||||
|
'is_missed' => $isMissed,
|
||||||
'can_attend' => $canAttend && ! $isAttended,
|
'can_attend' => $canAttend && ! $isAttended,
|
||||||
'status_label' => $statusLabel,
|
'status_label' => $statusLabel,
|
||||||
'status_color' => $statusColor,
|
'status_color' => $statusColor,
|
||||||
@ -197,6 +220,18 @@ public function scheduleCards()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function todayScheduleCards()
|
||||||
|
{
|
||||||
|
return $this->scheduleCards()->filter(fn ($card) => ! $card->is_missed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function missedScheduleCards()
|
||||||
|
{
|
||||||
|
return $this->scheduleCards()->filter(fn ($card) => $card->is_missed);
|
||||||
|
}
|
||||||
|
|
||||||
public function attend(int $sessionId): void
|
public function attend(int $sessionId): void
|
||||||
{
|
{
|
||||||
/** @var User|null $user */
|
/** @var User|null $user */
|
||||||
@ -223,23 +258,30 @@ public function attend(int $sessionId): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check session status
|
$isSent = $session->is_sent_to_lecturer;
|
||||||
if ($session->is_sent_to_lecturer) {
|
$isMissed = $session->date->isPast() && ! $session->date->isToday();
|
||||||
|
$now = now();
|
||||||
|
$startTime = $session->date->copy()->setTimeFrom($session->start_time);
|
||||||
|
|
||||||
|
// Attendance is allowed if:
|
||||||
|
// 1. Not sent yet
|
||||||
|
// 2. Either it's a missed session (from previous days) OR it's today and already started
|
||||||
|
if (! $isSent) {
|
||||||
|
if ($isMissed || $now->greaterThanOrEqualTo($startTime)) {
|
||||||
|
// Proceed with attendance
|
||||||
|
} else {
|
||||||
|
SystemNotification::send('attendance_not_started', type: 'warning')
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
SystemNotification::send('attendance_closed', type: 'danger')
|
SystemNotification::send('attendance_closed', type: 'danger')
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check time
|
|
||||||
$startTime = $session->start_time;
|
|
||||||
if (now()->lessThan($startTime)) {
|
|
||||||
SystemNotification::send('attendance_not_started', type: 'warning')
|
|
||||||
->send();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find schedule for compatibility
|
// Find schedule for compatibility
|
||||||
$schedule = CourseSchedule::where('course_id', $session->course_id)
|
$schedule = CourseSchedule::where('course_id', $session->course_id)
|
||||||
->where('day_of_week', $session->date?->dayOfWeekIso)
|
->where('day_of_week', $session->date?->dayOfWeekIso)
|
||||||
|
|||||||
@ -219,14 +219,18 @@
|
|||||||
],
|
],
|
||||||
'attendance_section' => [
|
'attendance_section' => [
|
||||||
'title' => 'Ayo Presensi Dulu! 🙋♂️✨',
|
'title' => 'Ayo Presensi Dulu! 🙋♂️✨',
|
||||||
'description' => 'Jangan lupa absen ya kalau kelasnya udah mulai! Biar tercatat hadir. 💪',
|
'description' => 'Jangan lupa presensi ya kalau kelasnya udah mulai! Biar tercatat hadir. 💪',
|
||||||
|
],
|
||||||
|
'missed_attendance' => [
|
||||||
|
'title' => 'Presensi Terlewat! ⌛🏃♂️',
|
||||||
|
'description' => 'Ayo segera presensi buat kelas yang udah lewat ini sebelum sesi ditutup! Jangan sampai alpa ya. 💪✨',
|
||||||
],
|
],
|
||||||
'attendance_history' => [
|
'attendance_history' => [
|
||||||
'title' => 'Jejak Presensimu 🕰️📜',
|
'title' => 'Jejak Presensimu 🕰️📜',
|
||||||
'description' => 'Di sini kamu bisa ngecek semua histori kehadiranmu sebelumnya. Rajin-rajin ya! 🎓',
|
'description' => 'Di sini kamu bisa ngecek semua histori kehadiranmu sebelumnya. Rajin-rajin ya! 🎓',
|
||||||
],
|
],
|
||||||
'empty_attendance' => [
|
'empty_attendance' => [
|
||||||
'title' => 'Yah, Absensi Belum Muncul! 💁♂️🌀',
|
'title' => 'Yah, Presensi Belum Muncul! 💁♂️🌀',
|
||||||
'description' => 'Sabar ya, setelah jadwal perkuliahan kamu muncul baru bisa diabsen di sini. Pantau terus jam kuliahnya! 🕒💪',
|
'description' => 'Sabar ya, setelah jadwal perkuliahan kamu muncul baru bisa diabsen di sini. Pantau terus jam kuliahnya! 🕒💪',
|
||||||
],
|
],
|
||||||
'assignment_list' => [
|
'assignment_list' => [
|
||||||
@ -537,6 +541,10 @@
|
|||||||
'title' => 'Sesi Kuliah',
|
'title' => 'Sesi Kuliah',
|
||||||
'description' => 'Silakan melakukan presensi sesuai dengan waktu kelas.',
|
'description' => 'Silakan melakukan presensi sesuai dengan waktu kelas.',
|
||||||
],
|
],
|
||||||
|
'missed_attendance' => [
|
||||||
|
'title' => 'Presensi Terlewat',
|
||||||
|
'description' => 'Segera lakukan presensi untuk sesi perkuliahan yang telah berlalu berikut sebelum akses ditutup.',
|
||||||
|
],
|
||||||
'attendance_history' => [
|
'attendance_history' => [
|
||||||
'title' => 'Riwayat Presensi',
|
'title' => 'Riwayat Presensi',
|
||||||
'description' => 'Berikut adalah riwayat kehadiran Anda pada sesi perkuliahan.',
|
'description' => 'Berikut adalah riwayat kehadiran Anda pada sesi perkuliahan.',
|
||||||
|
|||||||
@ -1,27 +1,30 @@
|
|||||||
<x-filament-panels::page>
|
<x-filament-panels::page>
|
||||||
<x-filament::section
|
<x-filament::section icon="{{ $this->sectionIcon }}" icon-color="primary">
|
||||||
icon="{{ $this->sectionIcon }}"
|
|
||||||
icon-color="primary"
|
|
||||||
>
|
|
||||||
<x-slot name="heading">{{ $this->heading }}</x-slot>
|
<x-slot name="heading">{{ $this->heading }}</x-slot>
|
||||||
<x-slot name="description">{{ $this->description }}</x-slot>
|
<x-slot name="description">{{ $this->description }}</x-slot>
|
||||||
|
|
||||||
@if (count($this->scheduleCards))
|
@if (count($this->todayScheduleCards))
|
||||||
<div class="columns-1 md:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6">
|
<div class="columns-1 md:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6">
|
||||||
@foreach ($this->scheduleCards as $card)
|
@foreach ($this->todayScheduleCards as $card)
|
||||||
<div class="break-inside-avoid mb-6">
|
<div class="break-inside-avoid mb-6">
|
||||||
<div class="{{ $card->card_classes }}">
|
<div class="{{ $card->card_classes }}">
|
||||||
|
|
||||||
<div class="p-6 space-y-4 flex-1">
|
<div class="p-6 space-y-4 flex-1">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h3 class="{{ $card->title_classes }}">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<x-heroicon-o-book-open @class([
|
<h3 class="{{ $card->title_classes }}">
|
||||||
'w-5 h-5 shrink-0',
|
<x-heroicon-o-book-open @class([
|
||||||
'opacity-40' => !$card->is_attended,
|
'w-5 h-5 shrink-0',
|
||||||
'text-primary-500' => $card->is_attended,
|
'opacity-40' => !$card->is_attended,
|
||||||
]) />
|
'text-primary-500' => $card->is_attended,
|
||||||
{{ $card->course_name }}
|
]) />
|
||||||
</h3>
|
{{ $card->course_name }}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 ring-1 ring-inset ring-gray-600/10 shrink-0">
|
||||||
|
Sesi {{ $card->session_number }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="{{ $card->lecturer_wrapper_classes }}">
|
<div class="{{ $card->lecturer_wrapper_classes }}">
|
||||||
<x-heroicon-m-user @class(['w-3.5 h-3.5 shrink-0', 'opacity-50' => !$card->is_attended]) />
|
<x-heroicon-m-user @class(['w-3.5 h-3.5 shrink-0', 'opacity-50' => !$card->is_attended]) />
|
||||||
<span class="text-[11px] font-medium leading-none">
|
<span class="text-[11px] font-medium leading-none">
|
||||||
@ -75,8 +78,8 @@ class="text-primary-600 dark:text-primary-200">{{ $card->attended_at }}</span>
|
|||||||
<div class="{{ $card->footer_classes }}">
|
<div class="{{ $card->footer_classes }}">
|
||||||
@if ($card->can_attend)
|
@if ($card->can_attend)
|
||||||
<x-filament::button wire:click="attend({{ $card->id }})" color="primary"
|
<x-filament::button wire:click="attend({{ $card->id }})" color="primary"
|
||||||
size="xs"
|
size="xs" icon="heroicon-m-finger-print"
|
||||||
class="rounded-lg shadow-sm font-bold uppercase tracking-tight text-[10px]">
|
class="rounded-lg shadow-sm font-bold uppercase tracking-tight text-[10px] w-full">
|
||||||
Presensi Sekarang
|
Presensi Sekarang
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
@elseif ($card->is_attended)
|
@elseif ($card->is_attended)
|
||||||
@ -109,19 +112,102 @@ class="text-[10px] font-bold text-gray-400 uppercase tracking-widest leading-non
|
|||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<x-filament::empty-state icon="heroicon-o-finger-print"
|
<x-filament::empty-state icon="heroicon-o-finger-print" heading="{{ $this->emptyHeading }}"
|
||||||
heading="{{ $this->emptyHeading }}"
|
description="{{ $this->emptyDescription }}" iconColor="gray">
|
||||||
description="{{ $this->emptyDescription }}"
|
|
||||||
iconColor="gray">
|
|
||||||
</x-filament::empty-state>
|
</x-filament::empty-state>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|
||||||
<x-filament::section
|
@if (count($this->missedScheduleCards))
|
||||||
icon="{{ $this->historyIcon }}"
|
<x-filament::section icon="heroicon-o-exclamation-triangle" icon-color="danger"
|
||||||
icon-color="gray"
|
class="mb-6 !bg-danger-50/5 border-danger-100 dark:border-danger-900/50">
|
||||||
>
|
<x-slot name="heading">{{ $this->missedHeading }}</x-slot>
|
||||||
|
<x-slot name="description">{{ $this->missedDescription }}</x-slot>
|
||||||
|
|
||||||
|
<div class="columns-1 md:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6 mt-4">
|
||||||
|
@foreach ($this->missedScheduleCards as $card)
|
||||||
|
<div class="break-inside-avoid mb-6">
|
||||||
|
<div class="{{ $card->card_classes }}">
|
||||||
|
|
||||||
|
<div class="p-6 space-y-4 flex-1">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<h3 class="{{ $card->title_classes }}">
|
||||||
|
<x-heroicon-o-book-open @class([
|
||||||
|
'w-5 h-5 shrink-0',
|
||||||
|
'opacity-40' => !$card->is_attended,
|
||||||
|
'text-primary-500' => $card->is_attended,
|
||||||
|
]) />
|
||||||
|
{{ $card->course_name }}
|
||||||
|
</h3>
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 ring-1 ring-inset ring-gray-600/10 shrink-0">
|
||||||
|
Sesi {{ $card->session_number }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="{{ $card->lecturer_wrapper_classes }}">
|
||||||
|
<x-heroicon-m-user @class(['w-3.5 h-3.5 shrink-0', 'opacity-50' => !$card->is_attended]) />
|
||||||
|
<span class="text-[11px] font-medium leading-none">
|
||||||
|
{{ $card->lecturer_name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400 pt-4">
|
||||||
|
<div class="{{ $card->icon_wrapper_classes }}">
|
||||||
|
<x-heroicon-m-clock @class([
|
||||||
|
'w-4 h-4',
|
||||||
|
'text-primary-600 dark:text-primary-300' => !$card->is_attended,
|
||||||
|
'text-primary-700 dark:text-primary-200' => $card->is_attended,
|
||||||
|
]) />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col min-w-0">
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold uppercase text-gray-400 tracking-widest leading-none mb-1">
|
||||||
|
{{ $card->date }}
|
||||||
|
</span>
|
||||||
|
<span class="{{ $card->time_label_classes }}">
|
||||||
|
{{ $card->time_range }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div @class([
|
||||||
|
'pt-4 border-t',
|
||||||
|
'border-gray-100 dark:border-gray-700' => !$card->is_attended,
|
||||||
|
'border-primary-200 dark:border-primary-800' => $card->is_attended,
|
||||||
|
])>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold text-gray-400 uppercase tracking-widest leading-none">Status</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="{{ $card->status_badge_classes }}">
|
||||||
|
{{ $card->status_label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="{{ $card->footer_classes }}">
|
||||||
|
@if ($card->can_attend)
|
||||||
|
<x-filament::button wire:click="attend({{ $card->id }})" color="primary"
|
||||||
|
size="xs" icon="heroicon-m-finger-print"
|
||||||
|
class="rounded-lg shadow-sm font-bold uppercase tracking-tight text-[10px] w-full">
|
||||||
|
Presensi Sekarang
|
||||||
|
</x-filament::button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<x-filament::section icon="{{ $this->historyIcon }}" icon-color="gray">
|
||||||
<x-slot name="heading">{{ $this->historyHeading }}</x-slot>
|
<x-slot name="heading">{{ $this->historyHeading }}</x-slot>
|
||||||
<x-slot name="description">{{ $this->historyDescription }}</x-slot>
|
<x-slot name="description">{{ $this->historyDescription }}</x-slot>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user