feat: implement automated daily session generation command and update session management UI with attendance tracking

This commit is contained in:
Yoga Pangestu 2026-03-29 22:06:46 +07:00
parent c8a98d8d65
commit 59af7bb595
4 changed files with 105 additions and 66 deletions

View File

@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class GenerateDailySessions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:generate-daily-sessions';
protected $description = 'Membangun sesi kelas harian secara otomatis berdasarkan jadwal.';
/**
* Execute the console command.
*/
public function handle()
{
$dayOfWeek = now()->dayOfWeekIso; // 1 (Mon) - 7 (Sun)
$currentSemester = app(\App\Settings\GeneralSettings::class)->current_semester;
$schedules = \App\Models\CourseSchedule::query()
->where('day_of_week', $dayOfWeek)
->whereHas('course', function ($query) use ($currentSemester) {
$query->where('semester', $currentSemester);
})
->get();
$count = 0;
foreach ($schedules as $schedule) {
$exists = \App\Models\ClassSession::where('course_id', $schedule->course_id)
->whereDate('date', now())
->exists();
if (! $exists) {
$lastSession = \App\Models\ClassSession::where('course_id', $schedule->course_id)->max('session_number');
\App\Models\ClassSession::create([
'course_id' => $schedule->course_id,
'session_number' => ($lastSession ?? 0) + 1,
'date' => now()->toDateString(),
'start_time' => $schedule->start_time,
'end_time' => $schedule->end_time,
]);
$count++;
}
}
$this->info("Generated {$count} sessions for ".now()->toDateString());
}
}

View File

@ -3,7 +3,6 @@
namespace App\Filament\Resources\Learning\ClassSessions\Pages; namespace App\Filament\Resources\Learning\ClassSessions\Pages;
use App\Filament\Actions\Cheerful\DeleteAction; use App\Filament\Actions\Cheerful\DeleteAction;
use App\Filament\Actions\Cheerful\EditAction;
use App\Filament\Resources\Learning\ClassSessions\ClassSessionResource; use App\Filament\Resources\Learning\ClassSessions\ClassSessionResource;
use App\Models\ClassSession; use App\Models\ClassSession;
use App\Models\Course; use App\Models\Course;
@ -17,7 +16,6 @@
use Filament\Forms\Contracts\HasForms; use Filament\Forms\Contracts\HasForms;
use Filament\Resources\Pages\Page; use Filament\Resources\Pages\Page;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class ManageClassSessions extends Page implements HasActions, HasForms class ManageClassSessions extends Page implements HasActions, HasForms
@ -58,59 +56,12 @@ protected function getHeaderActions(): array
return []; return [];
} }
public function editSessionAction(): Action
{
return EditAction::make('editSession')
->record(fn (array $arguments) => ClassSession::find($arguments['session']))
->modalWidth(Width::TwoExtraLarge);
}
public function deleteSessionAction(): Action public function deleteSessionAction(): Action
{ {
return DeleteAction::make('deleteSession') return DeleteAction::make('deleteSession')
->record(fn (array $arguments) => ClassSession::find($arguments['session'])); ->record(fn (array $arguments) => ClassSession::find($arguments['session']));
} }
public function generateTodaySessionAction(): Action
{
return Action::make('generateTodaySession')
->label('Generate Sesi')
->icon('heroicon-o-sparkles')
->color('primary')
->requiresConfirmation()
->modalHeading('Generate Sesi Hari Ini?')
->modalDescription('Sistem akan membuat sesi baru secara otomatis berdasarkan jadwal aktif.')
->modalSubmitActionLabel('Generate Sekarang')
->action(function (array $arguments) {
$courseId = $arguments['course'];
$schedule = CourseSchedule::where('course_id', $courseId)->first();
if (! $schedule) {
\Filament\Notifications\Notification::make()
->title('Jadwal Tidak Ditemukan')
->danger()
->send();
return;
}
$lastSession = ClassSession::where('course_id', $courseId)->max('session_number');
ClassSession::create([
'course_id' => $courseId,
'session_number' => ($lastSession ?? 0) + 1,
'date' => now()->toDateString(),
'start_time' => $schedule->start_time,
'end_time' => $schedule->end_time,
]);
\Filament\Notifications\Notification::make()
->title('Sesi Berhasil Digenerate')
->success()
->send();
});
}
public function getTodaySessions(): Collection public function getTodaySessions(): Collection
{ {
$dayOfWeek = now()->dayOfWeekIso; // 1 (Mon) - 7 (Sun) $dayOfWeek = now()->dayOfWeekIso; // 1 (Mon) - 7 (Sun)
@ -120,7 +71,10 @@ public function getTodaySessions(): Collection
->whereHas('course', function ($query) { ->whereHas('course', function ($query) {
$query->where('semester', app(GeneralSettings::class)->current_semester); $query->where('semester', app(GeneralSettings::class)->current_semester);
}) })
->with(['course', 'course.classSessions' => fn ($q) => $q->whereDate('date', now())]) ->with([
'course',
'course.classSessions' => fn ($q) => $q->whereDate('date', now())->withCount('attendances'),
])
->orderBy('start_time', 'asc') ->orderBy('start_time', 'asc')
->get(); ->get();
@ -135,6 +89,7 @@ public function getTodaySessions(): Collection
'end_time' => $schedule->end_time, 'end_time' => $schedule->end_time,
'title' => $session?->title, 'title' => $session?->title,
'is_pending' => ! $session, 'is_pending' => ! $session,
'attendances_count' => $session?->attendances_count ?? 0,
]; ];
}); });
} }

View File

@ -15,21 +15,31 @@
Sesi Hari Ini Sesi Hari Ini
</x-slot> </x-slot>
<x-slot name="description"> <x-slot name="description">
Sesi yang dijadwalkan pada {{ now()->format('l, d F Y') }} Sesi yang dijadwalkan pada {{ now()->translatedFormat('l, d F Y') }}
</x-slot> </x-slot>
<div class="space-y-4"> <div class="space-y-4">
@foreach ($this->getTodaySessions() as $session) @foreach ($this->getTodaySessions() as $session)
<div @class([ @php
'group flex w-full rounded-xl border overflow-hidden relative transition-all hover:shadow-md', $cardClass = \Illuminate\Support\Arr::toCssClasses([
'border-primary-300 dark:border-primary-700 bg-primary-50/30 dark:bg-primary-900/10 shadow-sm' => !$session->is_pending, 'group flex w-full rounded-xl border overflow-hidden relative transition-all hover:shadow-md',
'border-gray-300 dark:border-gray-700 bg-gray-50/30 dark:bg-gray-800/10 border-dashed' => $session->is_pending, 'border-primary-300 dark:border-primary-700 bg-primary-50/30 dark:bg-primary-900/10 shadow-sm' => !$session->is_pending,
])> 'border-gray-300 dark:border-gray-700 bg-gray-50/30 dark:bg-gray-800/10 border-dashed' => $session->is_pending,
]);
@endphp
@if ($session->is_pending)
<div class="{{ $cardClass }}">
@else
<a href="{{ \App\Filament\Resources\Learning\ClassSessions\ClassSessionResource::getUrl('course', ['courseId' => $session->course->id]) }}"
class="{{ $cardClass }}">
@endif
<div class="flex flex-1 items-start gap-4 p-5"> <div class="flex flex-1 items-start gap-4 p-5">
<div @class([ <div @class([
'flex h-12 w-12 shrink-0 flex-col items-center justify-center rounded-lg font-bold border', 'flex h-12 w-12 shrink-0 flex-col items-center justify-center rounded-lg font-bold border',
'bg-primary-100 dark:bg-primary-800 text-primary-700 dark:text-primary-300 border-primary-200 dark:border-primary-700' => !$session->is_pending, 'bg-primary-100 dark:bg-primary-800 text-primary-700 dark:text-primary-300 border-primary-200 dark:border-primary-700' => !$session->is_pending,
'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-700' => $session->is_pending, 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-700' =>
$session->is_pending,
])> ])>
<span class="text-[9px] uppercase leading-none opacity-60 mb-0.5 font-bold">Sesi</span> <span class="text-[9px] uppercase leading-none opacity-60 mb-0.5 font-bold">Sesi</span>
<span class="text-xl leading-none italic">#{{ $session->session_number }}</span> <span class="text-xl leading-none italic">#{{ $session->session_number }}</span>
@ -55,12 +65,14 @@
'bg-gray-500 text-white opacity-60' => $session->is_pending, 'bg-gray-500 text-white opacity-60' => $session->is_pending,
])> ])>
<x-heroicon-s-clock class="w-3.5 h-3.5" /> <x-heroicon-s-clock class="w-3.5 h-3.5" />
{{ $session->start_time->format('H:i') }} - {{ $session->end_time->format('H:i') }} {{ $session->start_time->format('H:i') }} -
{{ $session->end_time->format('H:i') }}
</div> </div>
@if ($session->is_pending) @if ($session->is_pending)
<div class="mt-2 flex items-center gap-1.5 text-[10px] text-gray-400 dark:text-gray-500 font-medium italic"> <div
<x-heroicon-m-sparkles class="w-3.5 h-3.5" /> class="mt-2 flex items-center gap-1.5 text-[10px] text-gray-400 dark:text-gray-500 font-medium italic">
Sesi belum digenerate hari ini <x-heroicon-m-calendar-days class="w-3.5 h-3.5" />
Sesuai Jadwal Hari Ini
</div> </div>
@endif @endif
</div> </div>
@ -72,13 +84,27 @@
'border-gray-200 dark:border-gray-800' => $session->is_pending, 'border-gray-200 dark:border-gray-800' => $session->is_pending,
])> ])>
@if ($session->is_pending) @if ($session->is_pending)
{{ ($this->generateTodaySessionAction)(['course' => $session->course->id]) }} <div class="flex items-center gap-2 text-gray-400 dark:text-gray-500 p-2 opacity-50">
<x-heroicon-m-clock class="w-5 h-5" />
</div>
@else @else
{{ ($this->editSessionAction)(['session' => $session->id]) }} <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>
@endif @endif
</div> </div>
</div> </div>
</div>
@if ($session->is_pending)
</div>
@else
</a>
@endif
@endforeach @endforeach
</div> </div>
</x-filament::section> </x-filament::section>
@ -122,8 +148,7 @@ class="text-base font-bold text-gray-950 dark:text-white leading-tight group-hov
</h3> </h3>
<div class="flex items-center text-sm text-gray-500 dark:text-gray-400"> <div class="flex items-center text-sm text-gray-500 dark:text-gray-400">
<x-heroicon-m-user class="w-4 h-4 mr-1.5 opacity-70" /> <x-heroicon-m-user class="w-4 h-4 mr-1.5 opacity-70" />
<span <span class="truncate">{{ $course->lecturer ?? 'Dosen Belum Ditentukan' }}</span>
class="truncate">{{ $course->lecturer ?? 'Dosen Belum Ditentukan' }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,3 +6,7 @@
Artisan::command('inspire', function () { Artisan::command('inspire', function () {
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote'); })->purpose('Display an inspiring quote');
use Illuminate\Support\Facades\Schedule;
Schedule::command('app:generate-daily-sessions')->dailyAt('00:00');