diff --git a/app/Filament/Resources/Learning/Attendances/AttendanceResource.php b/app/Filament/Resources/Learning/Attendances/AttendanceResource.php new file mode 100644 index 0000000..174969f --- /dev/null +++ b/app/Filament/Resources/Learning/Attendances/AttendanceResource.php @@ -0,0 +1,34 @@ + AttendancePage::route('/'), + ]; + } +} diff --git a/app/Filament/Resources/Learning/Attendances/Pages/AttendancePage.php b/app/Filament/Resources/Learning/Attendances/Pages/AttendancePage.php new file mode 100644 index 0000000..b4845ed --- /dev/null +++ b/app/Filament/Resources/Learning/Attendances/Pages/AttendancePage.php @@ -0,0 +1,137 @@ +user()->student; + + $today = now()->dayOfWeek; + + return CourseSchedule::query() + ->where('day_of_week', $today) + ->with(['course', 'attendances' => function ($q) use ($student) { + $q->where('student_id', $student->id) + ->whereDate('date', now()->toDateString()); + }]) + ->orderBy('start_time') + ->get(); + } + + public function getScheduleCards() + { + return $this->getSchedules() + ->map(function ($schedule) { + $attendance = $schedule->attendances->first(); + $isAttended = $attendance !== null; + + $now = now(); + $startTime = now()->setTimeFrom($schedule->start_time); + + $canAttend = $now->greaterThanOrEqualTo($startTime); + + $statusLabel = 'Belum Presensi'; + $statusColor = 'warning'; + $statusIcon = 'heroicon-o-clock'; + + if ($isAttended) { + $statusLabel = 'Hadir'; + $statusColor = 'success'; + $statusIcon = 'heroicon-o-check-circle'; + } elseif (! $canAttend) { + $statusLabel = 'Belum Dimulai'; + $statusColor = 'gray'; + $statusIcon = 'heroicon-o-lock-closed'; + } + + return (object) [ + 'id' => $schedule->id, + 'course_name' => $schedule->course->name, + 'time_range' => $schedule->start_time->format('H:i').' - '.$schedule->end_time->format('H:i'), + 'is_attended' => $isAttended, + 'can_attend' => $canAttend && ! $isAttended, + 'status_label' => $statusLabel, + 'status_color' => $statusColor, + 'status_icon' => $statusIcon, + 'attended_at' => $isAttended ? $attendance->attended_at->format('H:i') : null, + ]; + }); + } + + protected function getViewData(): array + { + return [ + 'scheduleCards' => $this->getScheduleCards(), + ]; + } + + public function attend(int $scheduleId): void + { + /** @var User $user */ + $user = auth()->user(); + $student = $user->student; + if (! $student) { + return; + } + + $schedule = CourseSchedule::find($scheduleId); + if (! $schedule) { + return; + } + + // Check if already attended + $existing = Attendance::where('student_id', $student->id) + ->where('course_schedule_id', $scheduleId) + ->whereDate('date', now()->toDateString()) + ->first(); + + if ($existing) { + SystemNotification::warning( + 'Presensi Sudah Tercatat ⚠️', + 'Anda telah melakukan presensi untuk jadwal perkuliahan ini pada hari ini.' + )->send(); + + return; + } + + // Check time + $startTime = now()->setTimeFrom($schedule->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.' + )->send(); + + return; + } + + Attendance::create([ + 'student_id' => $student->id, + 'course_schedule_id' => $scheduleId, + 'date' => now()->toDateString(), + 'attended_at' => now(), + ]); + + SystemNotification::success( + 'Presensi Berhasil Dicatat ✅', + 'Kehadiran Anda telah berhasil direkam ke dalam sistem.' + )->send(); + } +} diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php new file mode 100644 index 0000000..2760cbc --- /dev/null +++ b/app/Models/Attendance.php @@ -0,0 +1,28 @@ + 'datetime', + 'date' => 'date', + ]; + } + + public function student() + { + return $this->belongsTo(Student::class); + } + + public function courseSchedule() + { + return $this->belongsTo(CourseSchedule::class); + } +} diff --git a/app/Models/Course.php b/app/Models/Course.php index 9f9e3d4..e80adc3 100644 --- a/app/Models/Course.php +++ b/app/Models/Course.php @@ -31,4 +31,9 @@ public function materials(): HasMany { return $this->hasMany(Material::class); } + + public function studyGroups(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->belongsToMany(StudyGroup::class, 'study_group_courses'); + } } diff --git a/app/Models/CourseSchedule.php b/app/Models/CourseSchedule.php index c934e28..dcf7a61 100644 --- a/app/Models/CourseSchedule.php +++ b/app/Models/CourseSchedule.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; class CourseSchedule extends Model @@ -25,4 +26,9 @@ public function course(): BelongsTo { return $this->belongsTo(Course::class); } + + public function attendances(): HasMany + { + return $this->hasMany(Attendance::class); + } } diff --git a/app/Models/Student.php b/app/Models/Student.php index da1f04d..052c4b6 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; class Student extends Model @@ -40,4 +41,9 @@ public function user(): BelongsTo { return $this->belongsTo(User::class); } + + public function attendances(): HasMany + { + return $this->hasMany(Attendance::class); + } } diff --git a/database/migrations/2026_03_11_133639_create_attendances_table.php b/database/migrations/2026_03_11_133639_create_attendances_table.php new file mode 100644 index 0000000..0c24b69 --- /dev/null +++ b/database/migrations/2026_03_11_133639_create_attendances_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('student_id')->constrained()->cascadeOnDelete(); + $table->foreignId('course_schedule_id')->constrained()->cascadeOnDelete(); + $table->date('date'); + $table->timestamp('attended_at')->nullable(); + $table->timestamps(); + + $table->unique(['student_id', 'course_schedule_id', 'date']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('attendances'); + } +}; diff --git a/resources/views/filament/resources/learning/attendances/pages/list-attendances.blade.php b/resources/views/filament/resources/learning/attendances/pages/list-attendances.blade.php new file mode 100644 index 0000000..cc65bba --- /dev/null +++ b/resources/views/filament/resources/learning/attendances/pages/list-attendances.blade.php @@ -0,0 +1,152 @@ + + + Sesi Kuliah + Silakan melakukan presensi sesuai dengan waktu kelas. + + @if ($scheduleCards->isNotEmpty()) +
+ @foreach ($scheduleCards as $card) +
+
!$card->is_attended, + 'bg-primary-50/30 dark:bg-primary-900/10 border-primary-500 shadow-md ring-1 ring-primary-500' => + $card->is_attended, + ])> + + @if ($card->is_attended) +
+ + + Hadir + +
+ @endif + +
+
+ !$card->is_attended, + 'bg-white dark:bg-primary-800/40 text-primary-700 dark:text-primary-200 ring-primary-500/30' => + $card->is_attended, + ])> + {{ $card->course_name }} + +
+ +
+

!$card->is_attended, + 'text-primary-900 dark:text-primary-100' => $card->is_attended, + ])> + !$card->is_attended, + 'text-primary-500' => $card->is_attended, + ]) /> + Presensi Kuliah +

+
+ +
+
+
!$card->is_attended, + 'bg-white dark:bg-primary-800 border-primary-300 dark:border-primary-600' => + $card->is_attended, + ])> + !$card->is_attended, + 'text-primary-700 dark:text-primary-200' => $card->is_attended, + ]) /> +
+
+ Waktu + Kuliah + !$card->is_attended, + 'text-primary-900 dark:text-primary-50' => $card->is_attended, + ])> + {{ $card->time_range }} + +
+
+ +
!$card->is_attended, + 'border-primary-200 dark:border-primary-800' => $card->is_attended, + ])> +
+ Status + + $card->is_attended, + 'bg-warning-50 dark:bg-warning-500/10 text-warning-600 dark:text-warning-400 ring-warning-600/20' => + !$card->is_attended && $card->can_attend, + 'bg-gray-50 dark:bg-gray-500/10 text-gray-600 dark:text-gray-400 ring-gray-600/20' => + !$card->is_attended && !$card->can_attend, + ])> + {{ $card->status_label }} + +
+ @if ($card->is_attended) + + Tercatat: {{ $card->attended_at }} + + @endif +
+
+
+ +
+ $card->is_attended || $card->can_attend, + ])> + @if ($card->can_attend) + + Absen Sekarang + + @elseif ($card->is_attended) +
+ + + Selesai + +
+ @else +
+ Belum + Dibuka +
+ @endif +
+
+
+ @endforeach +
+ @else + + + @endif +
+