refactor: Remove legacy attendance management pages and integrate class session management functionality, including new models, migrations, and views for class sessions and attendance tracking.
This commit is contained in:
parent
de1877695d
commit
f0e6841628
@ -1,122 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Manage\Attendance;
|
||||
|
||||
use App\Filament\Actions\BackAction;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseSchedule;
|
||||
use App\Models\Student;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Detail extends Page
|
||||
{
|
||||
use HasPageShield;
|
||||
|
||||
public static function getPagePermission(): string
|
||||
{
|
||||
return 'View:AttendanceMonitoring';
|
||||
}
|
||||
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static ?string $title = 'Detail Monitoring Presensi';
|
||||
|
||||
protected static ?string $slug = 'manage/attendance-monitoring/{course}';
|
||||
|
||||
protected string $view = 'filament.pages.manage.attendance.detail';
|
||||
|
||||
public Course $course;
|
||||
|
||||
public function mount(Course $course): void
|
||||
{
|
||||
$this->course = $course;
|
||||
}
|
||||
|
||||
public function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
BackAction::make()
|
||||
->url(Index::getUrl()),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getViewData(): array
|
||||
{
|
||||
return [
|
||||
'selectedCourse' => $this->course,
|
||||
'meetingHistory' => $this->getMeetingHistory(),
|
||||
'activeMeetingStats' => $this->getActiveMeetingStats(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getMeetingHistory(): Collection
|
||||
{
|
||||
return Attendance::query()
|
||||
->whereHas('courseSchedule', function ($q) {
|
||||
$q->where('course_id', $this->course->id);
|
||||
})
|
||||
->select('date')
|
||||
->distinct()
|
||||
->orderBy('date', 'desc')
|
||||
->get()
|
||||
->map(function ($att) {
|
||||
$attendedCount = Attendance::whereHas('courseSchedule', function ($q) {
|
||||
$q->where('course_id', $this->course->id);
|
||||
})
|
||||
->whereDate('date', $att->date)
|
||||
->count();
|
||||
|
||||
$totalStudents = Student::count();
|
||||
|
||||
return (object) [
|
||||
'date' => [
|
||||
'month' => $att->date->translatedFormat('M'),
|
||||
'day' => $att->date->translatedFormat('d'),
|
||||
],
|
||||
'formatted_date' => $att->date->translatedFormat('l, d F Y'),
|
||||
'attended_count' => $attendedCount,
|
||||
'total_students' => $totalStudents,
|
||||
'share_url' => route('share.attendance', [
|
||||
'token' => $this->course->sharing_token,
|
||||
'date' => $att->date->toDateString(),
|
||||
]),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getActiveMeetingStats(): ?object
|
||||
{
|
||||
$today = now()->dayOfWeekIso;
|
||||
$nowTime = now()->format('H:i');
|
||||
|
||||
$activeSchedule = CourseSchedule::where('course_id', $this->course->id)
|
||||
->where('day_of_week', $today)
|
||||
->where('start_time', '<=', $nowTime)
|
||||
->where('end_time', '>=', $nowTime)
|
||||
->first();
|
||||
|
||||
if (! $activeSchedule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attendedIds = Attendance::where('course_schedule_id', $activeSchedule->id)
|
||||
->whereDate('date', now()->toDateString())
|
||||
->pluck('student_id')
|
||||
->toArray();
|
||||
|
||||
$totalStudents = Student::count();
|
||||
|
||||
$absentStudents = Student::whereNotIn('id', $attendedIds)->get();
|
||||
|
||||
return (object) [
|
||||
'time_range' => $activeSchedule->start_time->format('H:i').' - '.$activeSchedule->end_time->format('H:i'),
|
||||
'attended_count' => count($attendedIds),
|
||||
'total_students' => $totalStudents,
|
||||
'absent_students' => $absentStudents,
|
||||
'percentage' => round((count($attendedIds) / $totalStudents) * 100),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Manage\Attendance;
|
||||
|
||||
use App\Models\Course;
|
||||
use App\Settings\GeneralSettings;
|
||||
use BackedEnum;
|
||||
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Illuminate\Support\Collection;
|
||||
use UnitEnum;
|
||||
|
||||
class Index extends Page
|
||||
{
|
||||
use HasPageShield;
|
||||
|
||||
public static function getPagePermission(): string
|
||||
{
|
||||
return 'View:AttendanceMonitoring';
|
||||
}
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Kelola';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPresentationChartLine;
|
||||
|
||||
protected static ?string $navigationLabel = 'Monitoring Presensi';
|
||||
|
||||
protected static ?string $title = 'Monitoring Presensi';
|
||||
|
||||
protected static ?string $slug = 'manage/attendance-monitoring';
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
protected string $view = 'filament.pages.manage.attendance.index';
|
||||
|
||||
protected function getViewData(): array
|
||||
{
|
||||
return [
|
||||
'courses' => $this->getCourses(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getCourses(): Collection
|
||||
{
|
||||
$semester = app(GeneralSettings::class)->current_semester;
|
||||
$today = now()->dayOfWeekIso;
|
||||
|
||||
return Course::query()
|
||||
->where('semester', $semester)
|
||||
->with(['lecturer', 'courseSchedules'])
|
||||
->get()
|
||||
->map(function ($course) use ($today) {
|
||||
$isOngoing = $course->courseSchedules->contains(function ($schedule) use ($today) {
|
||||
return $schedule->day_of_week == $today;
|
||||
});
|
||||
|
||||
$course->lecturer_name = $course->lecturer?->full_name;
|
||||
$course->is_ongoing = $isOngoing;
|
||||
$course->detail_url = Detail::getUrl(['course' => $course]);
|
||||
|
||||
return $course;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Learning\ClassSessions;
|
||||
|
||||
use App\Enums\RoleEnum;
|
||||
use App\Filament\Actions\Cheerful\DeleteAction;
|
||||
use App\Filament\Actions\Cheerful\EditAction;
|
||||
use App\Filament\Actions\Cheerful\ForceDeleteAction;
|
||||
use App\Filament\Actions\Cheerful\RestoreAction;
|
||||
use App\Filament\Actions\DefaultBulkActions;
|
||||
use App\Filament\Columns\TimestampColumns;
|
||||
use App\Filament\Resources\Learning\ClassSessions\Pages\ListCourseSessions;
|
||||
use App\Filament\Resources\Learning\ClassSessions\Pages\ManageClassSessions;
|
||||
use App\Filament\Resources\Learning\ClassSessions\RelationManagers\AttendanceRelationManager;
|
||||
use App\Models\ClassSession;
|
||||
use App\Models\Course;
|
||||
use App\Settings\GeneralSettings;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\TimePicker;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use UnitEnum;
|
||||
|
||||
class ClassSessionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ClassSession::class;
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Pembelajaran';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPresentationChartBar;
|
||||
|
||||
protected static ?string $navigationLabel = 'Sesi Kelas';
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected static ?string $modelLabel = 'Sesi Kelas';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Sesi Kelas';
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Grid::make(2)
|
||||
->schema([
|
||||
Select::make('course_id')
|
||||
->label('Mata Kuliah')
|
||||
->placeholder('Pilih Mata Kuliah')
|
||||
->options(function () {
|
||||
return Course::query()
|
||||
->where('semester', app(GeneralSettings::class)->current_semester)
|
||||
->pluck('name', 'id')
|
||||
->toArray();
|
||||
})
|
||||
->searchable()
|
||||
->required()
|
||||
->preload()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, $set) {
|
||||
if (! $state) {
|
||||
return;
|
||||
}
|
||||
|
||||
$schedule = \App\Models\CourseSchedule::where('course_id', $state)->first();
|
||||
|
||||
if ($schedule) {
|
||||
$set('start_time', $schedule->start_time?->format('H:i'));
|
||||
$set('end_time', $schedule->end_time?->format('H:i'));
|
||||
}
|
||||
|
||||
// Ambil pertemuan terakhir + 1
|
||||
$lastSession = \App\Models\ClassSession::where('course_id', $state)->max('session_number');
|
||||
$set('session_number', ($lastSession ?? 0) + 1);
|
||||
})
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('session_number')
|
||||
->label('Pertemuan Ke-')
|
||||
->placeholder('1')
|
||||
->numeric()
|
||||
->required()
|
||||
->minValue(1)
|
||||
->maxValue(16)
|
||||
->autocomplete(false),
|
||||
|
||||
DatePicker::make('date')
|
||||
->label('Tanggal')
|
||||
->placeholder('Pilih Tanggal')
|
||||
->required()
|
||||
->native(false)
|
||||
->displayFormat('l, d F Y')
|
||||
->default(now()->toDateString()),
|
||||
|
||||
TimePicker::make('start_time')
|
||||
->label('Waktu Mulai')
|
||||
->placeholder('08:00')
|
||||
->native(false)
|
||||
->displayFormat('H:i')
|
||||
->seconds(false)
|
||||
->required(),
|
||||
|
||||
TimePicker::make('end_time')
|
||||
->label('Waktu Selesai')
|
||||
->placeholder('10:00')
|
||||
->native(false)
|
||||
->displayFormat('H:i')
|
||||
->seconds(false)
|
||||
->required(),
|
||||
])
|
||||
->columnSpanFull(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('session_number')
|
||||
->label('Pertemuan Ke-')
|
||||
->sortable()
|
||||
->badge(),
|
||||
|
||||
TextColumn::make('course.name')
|
||||
->label('Mata Kuliah')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->wrap()
|
||||
->description(fn (ClassSession $record) => $record->course->code),
|
||||
|
||||
TextColumn::make('date')
|
||||
->label('Tanggal')
|
||||
->date('l, d F Y')
|
||||
->sortable()
|
||||
->description(fn (ClassSession $record) => $record->start_time->format('H:i').' - '.$record->end_time->format('H:i').' WIB'),
|
||||
|
||||
TextColumn::make('attendances_count')
|
||||
->counts('attendances')
|
||||
->label('Presensi')
|
||||
->badge()
|
||||
->color('success'),
|
||||
|
||||
TextColumn::make('materials_count')
|
||||
->counts('materials')
|
||||
->label('Materi')
|
||||
->badge()
|
||||
->color('warning'),
|
||||
|
||||
TextColumn::make('assignments_count')
|
||||
->counts('assignments')
|
||||
->label('Tugas')
|
||||
->badge()
|
||||
->color('info'),
|
||||
|
||||
...TimestampColumns::make(),
|
||||
])
|
||||
->filters([
|
||||
TrashedFilter::make()
|
||||
->native(false)
|
||||
->visible(fn () => auth()->user()->hasRole(RoleEnum::Developer)),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->modalWidth(Width::TwoExtraLarge),
|
||||
|
||||
DeleteAction::make(),
|
||||
|
||||
ForceDeleteAction::make(),
|
||||
|
||||
RestoreAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
...DefaultBulkActions::make('Sesi Kelas'),
|
||||
]),
|
||||
])
|
||||
->emptyStateIcon(Heroicon::OutlinedPresentationChartBar)
|
||||
->emptyStateDescription('Setelah Anda membuat data pertama, maka akan muncul disini.')
|
||||
->defaultSort('created_at', 'desc')
|
||||
->deferFilters(false)
|
||||
->paginated([25, 50, 100, 'all'])
|
||||
->defaultPaginationPageOption(25);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
AttendanceRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ManageClassSessions::route('/'),
|
||||
'course' => ListCourseSessions::route('/course/{courseId}'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getEloquentQuery()
|
||||
->whereHas('course', function (Builder $query) {
|
||||
$query->where('semester', app(GeneralSettings::class)->current_semester);
|
||||
});
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Learning\ClassSessions\Pages;
|
||||
|
||||
use App\Filament\Actions\Cheerful\CreateAction;
|
||||
use App\Filament\Actions\Cheerful\DeleteAction;
|
||||
use App\Filament\Actions\Cheerful\EditAction;
|
||||
use App\Filament\Resources\Learning\ClassSessions\ClassSessionResource;
|
||||
use App\Models\ClassSession;
|
||||
use App\Models\Course;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ListCourseSessions extends Page implements HasActions, HasForms
|
||||
{
|
||||
use InteractsWithActions, InteractsWithForms;
|
||||
|
||||
protected static string $resource = ClassSessionResource::class;
|
||||
|
||||
protected string $view = 'filament.resources.learning.class-sessions.pages.list-course-sessions';
|
||||
|
||||
public $courseId;
|
||||
|
||||
public function mount($courseId): void
|
||||
{
|
||||
$this->courseId = $courseId;
|
||||
}
|
||||
|
||||
public function getCourse(): Course
|
||||
{
|
||||
return Course::findOrFail($this->courseId);
|
||||
}
|
||||
|
||||
public function getSessions(): Collection
|
||||
{
|
||||
return $this->getCourse()->classSessions()->orderBy('session_number', 'asc')->get();
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Sesi Kelas - '.$this->getCourse()->name;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Tambah Sesi')
|
||||
->modalHeading('Tambah Sesi')
|
||||
->modalSubmitActionLabel('Simpan')
|
||||
->modalCancelActionLabel('Batal')
|
||||
->mutateFormDataUsing(function (array $data): array {
|
||||
$data['course_id'] = $this->courseId;
|
||||
|
||||
return $data;
|
||||
})
|
||||
->extraModalFooterActions(fn (CreateAction $action): array => [
|
||||
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
|
||||
->label('Simpan dan Tambah Lagi'),
|
||||
])
|
||||
->modalWidth(Width::TwoExtraLarge),
|
||||
];
|
||||
}
|
||||
|
||||
public function editSessionAction(): Action
|
||||
{
|
||||
return EditAction::make('editSession')
|
||||
->record(fn (array $arguments) => ClassSession::find($arguments['session']))
|
||||
->modalWidth(Width::TwoExtraLarge);
|
||||
}
|
||||
|
||||
public function deleteSessionAction(): Action
|
||||
{
|
||||
return DeleteAction::make('deleteSession')
|
||||
->record(fn (array $arguments) => ClassSession::find($arguments['session']));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Learning\ClassSessions\Pages;
|
||||
|
||||
use App\Filament\Actions\Cheerful\DeleteAction;
|
||||
use App\Filament\Actions\Cheerful\EditAction;
|
||||
use App\Filament\Resources\Learning\ClassSessions\ClassSessionResource;
|
||||
use App\Models\ClassSession;
|
||||
use App\Models\Course;
|
||||
use App\Models\CourseSchedule;
|
||||
use App\Settings\GeneralSettings;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Resources\Pages\Page;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ManageClassSessions extends Page implements HasActions, HasForms
|
||||
{
|
||||
use InteractsWithActions, InteractsWithForms;
|
||||
|
||||
public static string $resource = ClassSessionResource::class;
|
||||
|
||||
protected static ?string $title = 'Sesi Kelas';
|
||||
|
||||
protected string $view = 'filament.pages.learning.class-session.index';
|
||||
|
||||
public ?string $search = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill();
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
TextInput::make('search')
|
||||
->label('Cari')
|
||||
->placeholder('Cari Mata Kuliah atau Dosen...')
|
||||
->autocomplete(false)
|
||||
->live(debounce: 500)
|
||||
->afterStateUpdated(fn ($state) => $this->search = $state),
|
||||
])
|
||||
->columns([
|
||||
'sm' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function editSessionAction(): Action
|
||||
{
|
||||
return EditAction::make('editSession')
|
||||
->record(fn (array $arguments) => ClassSession::find($arguments['session']))
|
||||
->modalWidth(Width::TwoExtraLarge);
|
||||
}
|
||||
|
||||
public function deleteSessionAction(): Action
|
||||
{
|
||||
return DeleteAction::make('deleteSession')
|
||||
->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
|
||||
{
|
||||
$dayOfWeek = now()->dayOfWeekIso; // 1 (Mon) - 7 (Sun)
|
||||
|
||||
$schedules = CourseSchedule::query()
|
||||
->where('day_of_week', $dayOfWeek)
|
||||
->whereHas('course', function ($query) {
|
||||
$query->where('semester', app(GeneralSettings::class)->current_semester);
|
||||
})
|
||||
->with(['course.lecturer', 'course.classSessions' => fn ($q) => $q->whereDate('date', now())])
|
||||
->orderBy('start_time', 'asc')
|
||||
->get();
|
||||
|
||||
return $schedules->map(function ($schedule) {
|
||||
$session = $schedule->course->classSessions->first();
|
||||
|
||||
return (object) [
|
||||
'id' => $session?->id,
|
||||
'course' => $schedule->course,
|
||||
'session_number' => $session?->session_number ?? (ClassSession::where('course_id', $schedule->course_id)->max('session_number') ?? 0) + 1,
|
||||
'start_time' => $schedule->start_time,
|
||||
'end_time' => $schedule->end_time,
|
||||
'title' => $session?->title,
|
||||
'is_pending' => ! $session,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function getCourses(): Collection
|
||||
{
|
||||
$query = Course::query()
|
||||
->where('semester', app(GeneralSettings::class)->current_semester)
|
||||
->with(['classSessions' => fn ($q) => $q->orderBy('session_number', 'asc'), 'lecturer']);
|
||||
|
||||
if ($this->search) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('name', 'like', "%{$this->search}%")
|
||||
->orWhere('code', 'like', "%{$this->search}%")
|
||||
->orWhereHas('lecturer', fn ($sq) => $sq->where('full_name', 'like', "%{$this->search}%"));
|
||||
});
|
||||
}
|
||||
|
||||
return $query->get()
|
||||
->sortBy('name');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Learning\ClassSessions\RelationManagers;
|
||||
|
||||
use App\Filament\Actions\Cheerful\CreateAction;
|
||||
use App\Filament\Actions\Cheerful\DeleteAction;
|
||||
use App\Filament\Actions\Cheerful\EditAction;
|
||||
use App\Filament\Actions\DefaultBulkActions;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AttendanceRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'attendances';
|
||||
|
||||
protected static ?string $title = 'Presensi Mahasiswa';
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Select::make('student_id')
|
||||
->label('Mahasiswa')
|
||||
->placeholder('Pilih Mahasiswa')
|
||||
->relationship('student', 'full_name')
|
||||
->searchable()
|
||||
->required()
|
||||
->preload()
|
||||
->helperText('Pilih mahasiswa yang hadir pada pertemuan ini.'),
|
||||
|
||||
DateTimePicker::make('attended_at')
|
||||
->label('Waktu Presensi')
|
||||
->default(now())
|
||||
->native(false)
|
||||
->helperText('Tanggal dan jam mahasiswa melakukan presensi.'),
|
||||
])
|
||||
->columns(1);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('student.full_name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('student.student_number')
|
||||
->label('NIM')
|
||||
->copyable()
|
||||
->searchable()
|
||||
->sortable()
|
||||
->color('gray'),
|
||||
|
||||
Tables\Columns\TextColumn::make('student.full_name')
|
||||
->label('Nama Mahasiswa')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
Tables\Columns\TextColumn::make('attended_at')
|
||||
->label('Waktu Presensi')
|
||||
->dateTime('l, d F Y H:i')
|
||||
->sortable()
|
||||
->description(fn ($record) => $record->attended_at->format('H:i').' WIB')
|
||||
->color('primary'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()
|
||||
->modalWidth(Width::Large),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make()
|
||||
->modalWidth(Width::Large),
|
||||
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
...DefaultBulkActions::make('Presensi Mahasiswa'),
|
||||
]),
|
||||
])
|
||||
->emptyStateDescription('Belum ada data presensi untuk pertemuan ini.');
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ class Assignment extends Model implements HasMedia
|
||||
|
||||
protected $fillable = [
|
||||
'course_id',
|
||||
'class_session_id',
|
||||
'description',
|
||||
'due_date',
|
||||
'title',
|
||||
@ -52,6 +53,11 @@ public function course(): BelongsTo
|
||||
return $this->belongsTo(Course::class);
|
||||
}
|
||||
|
||||
public function classSession(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClassSession::class);
|
||||
}
|
||||
|
||||
public function students(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
|
||||
@ -25,4 +25,9 @@ public function courseSchedule()
|
||||
{
|
||||
return $this->belongsTo(CourseSchedule::class);
|
||||
}
|
||||
|
||||
public function classSession()
|
||||
{
|
||||
return $this->belongsTo(ClassSession::class);
|
||||
}
|
||||
}
|
||||
|
||||
49
app/Models/ClassSession.php
Normal file
49
app/Models/ClassSession.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
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 ClassSession extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'course_id',
|
||||
'session_number',
|
||||
'date',
|
||||
'start_time',
|
||||
'end_time',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'session_number' => 'integer',
|
||||
'start_time' => 'datetime',
|
||||
'end_time' => 'datetime',
|
||||
];
|
||||
|
||||
public function course(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Course::class);
|
||||
}
|
||||
|
||||
public function materials(): HasMany
|
||||
{
|
||||
return $this->hasMany(Material::class);
|
||||
}
|
||||
|
||||
public function assignments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Assignment::class);
|
||||
}
|
||||
|
||||
public function attendances(): HasMany
|
||||
{
|
||||
return $this->hasMany(Attendance::class);
|
||||
}
|
||||
}
|
||||
@ -49,4 +49,9 @@ public function courseSchedules(): HasMany
|
||||
{
|
||||
return $this->hasMany(CourseSchedule::class);
|
||||
}
|
||||
|
||||
public function classSessions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ClassSession::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,4 +23,9 @@ public function course(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Course::class);
|
||||
}
|
||||
|
||||
public function classSession(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ClassSession::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('class_sessions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedTinyInteger('session_number'); // e.g., 1, 2, ...
|
||||
$table->date('date');
|
||||
$table->time('start_time');
|
||||
$table->time('end_time');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['course_id', 'session_number'], 'class_sessions_course_session_unique');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('class_sessions');
|
||||
}
|
||||
};
|
||||
@ -15,6 +15,7 @@ public function up(): void
|
||||
Schema::create('assignments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('class_session_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('title', 100);
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamp('due_date');
|
||||
|
||||
@ -14,12 +14,14 @@ public function up(): void
|
||||
Schema::create('attendances', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('student_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('course_schedule_id')->constrained()->cascadeOnDelete();
|
||||
$table->date('date');
|
||||
$table->foreignId('class_session_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('course_schedule_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->date('date')->nullable();
|
||||
$table->timestamp('attended_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['student_id', 'course_schedule_id', 'date']);
|
||||
$table->unique(['student_id', 'class_session_id']);
|
||||
$table->index(['student_id', 'course_schedule_id', 'date']);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ public function up(): void
|
||||
Schema::create('materials', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('class_session_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('title', 100);
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamp('published_at')->nullable();
|
||||
|
||||
@ -0,0 +1,139 @@
|
||||
<x-filament-panels::page>
|
||||
<!-- Search Section -->
|
||||
<x-filament::section>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="w-full max-w-xl">
|
||||
{{ $this->form }}
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<!-- Today's Sessions Section -->
|
||||
@if ($this->getTodaySessions()->isNotEmpty())
|
||||
<x-filament::section icon="heroicon-o-bolt" icon-color="primary">
|
||||
<x-slot name="heading">
|
||||
Sesi Hari Ini
|
||||
</x-slot>
|
||||
<x-slot name="description">
|
||||
Sesi yang dijadwalkan pada {{ now()->format('l, d F Y') }}
|
||||
</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach ($this->getTodaySessions() as $session)
|
||||
<div @class([
|
||||
'group flex w-full rounded-xl border overflow-hidden relative transition-all hover:shadow-md',
|
||||
'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,
|
||||
])>
|
||||
<div class="flex flex-1 items-start gap-4 p-5">
|
||||
<div @class([
|
||||
'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-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-xl leading-none italic">#{{ $session->session_number }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div>
|
||||
<h3 @class([
|
||||
'font-bold transition-colors',
|
||||
'text-gray-900 dark:text-white group-hover:text-primary-600' => !$session->is_pending,
|
||||
'text-gray-500 dark:text-gray-400' => $session->is_pending,
|
||||
])>
|
||||
{{ $session->course->name }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5 font-medium">
|
||||
{{ $session->course->code }} • {{ $session->course->lecturer->full_name ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div @class([
|
||||
'flex items-center gap-1.5 rounded-full px-3 py-1 text-[11px] font-bold shadow-sm',
|
||||
'bg-primary-500 text-white animate-pulse' => !$session->is_pending,
|
||||
'bg-gray-500 text-white opacity-60' => $session->is_pending,
|
||||
])>
|
||||
<x-heroicon-s-clock class="w-3.5 h-3.5" />
|
||||
{{ $session->start_time->format('H:i') }} - {{ $session->end_time->format('H:i') }}
|
||||
</div>
|
||||
@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">
|
||||
<x-heroicon-m-sparkles class="w-3.5 h-3.5" />
|
||||
Sesi belum digenerate hari ini
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div @class([
|
||||
'flex flex-col items-center justify-center border-l p-2 bg-white/30 dark:bg-black/10 transition-colors',
|
||||
'border-primary-200 dark:border-primary-800 group-hover:bg-primary-500/5' => !$session->is_pending,
|
||||
'border-gray-200 dark:border-gray-800' => $session->is_pending,
|
||||
])>
|
||||
@if ($session->is_pending)
|
||||
{{ ($this->generateTodaySessionAction)(['course' => $session->course->id]) }}
|
||||
@else
|
||||
{{ ($this->editSessionAction)(['session' => $session->id]) }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
<!-- All Courses Section -->
|
||||
<x-filament::section icon="heroicon-o-academic-cap" icon-color="gray">
|
||||
<x-slot name="heading">
|
||||
Daftar Mata Kuliah Semester Ini
|
||||
</x-slot>
|
||||
<x-slot name="description">
|
||||
Pilih mata kuliah untuk melihat dan mengelola semua riwayat sesi.
|
||||
</x-slot>
|
||||
|
||||
@if ($this->getCourses()->isNotEmpty())
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@foreach ($this->getCourses() as $course)
|
||||
<a href="{{ \App\Filament\Resources\Learning\ClassSessions\ClassSessionResource::getUrl('course', ['courseId' => $course->id]) }}"
|
||||
class="fi-card rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm overflow-hidden transition hover:shadow-md h-fit block group">
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md bg-primary-50 dark:bg-primary-500/10 px-2 py-1 text-xs font-semibold text-primary-700 dark:text-primary-300 ring-1 ring-inset ring-primary-600/20">
|
||||
{{ $course->code }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-400 dark:text-gray-500">
|
||||
<span class="text-xs font-medium">
|
||||
{{ $course->classSessions->count() }} Sesi
|
||||
</span>
|
||||
<x-heroicon-m-chevron-right
|
||||
class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<h3
|
||||
class="text-base font-bold text-gray-950 dark:text-white leading-tight group-hover:text-primary-600 dark:group-hover:text-primary-400 transition">
|
||||
{{ $course->name }}
|
||||
</h3>
|
||||
<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" />
|
||||
<span
|
||||
class="truncate">{{ $course->lecturer->full_name ?? 'Dosen Belum Ditentukan' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<x-filament::empty-state icon="heroicon-o-presentation-chart-bar" heading="Tidak ada data yang ditemukan"
|
||||
description="Setelah Anda membuat data pertama, maka akan muncul disini." iconColor="gray">
|
||||
</x-filament::empty-state>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</x-filament-panels::page>
|
||||
@ -1,145 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Left: History -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Riwayat Pertemuan</x-slot>
|
||||
<x-slot name="description">Rekapitulasi presensi tiap pertemuan.</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
@forelse ($meetingHistory as $meeting)
|
||||
<div
|
||||
class="fi-card flex items-center justify-between p-4 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm transition duration-200 hover:shadow-md hover:border-primary-500">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-lg bg-gray-50 dark:bg-gray-800 flex flex-col items-center justify-center border border-gray-100 dark:border-gray-700">
|
||||
<span
|
||||
class="text-[10px] font-bold uppercase text-gray-400">{{ $meeting->date['month'] }}</span>
|
||||
<span
|
||||
class="text-xl font-bold text-gray-700 dark:text-gray-200">{{ $meeting->date['day'] }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-bold text-sm text-gray-950 dark:text-white">
|
||||
{{ $meeting->formatted_date }}</h4>
|
||||
<p
|
||||
class="text-[11px] font-medium text-gray-500 uppercase tracking-tight mt-0.5">
|
||||
HADIR: <span
|
||||
class="text-primary-600 dark:text-primary-400">{{ $meeting->attended_count }}
|
||||
/ {{ $meeting->total_students }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-filament::button
|
||||
x-on:click="window.navigator.clipboard.writeText('{{ $meeting->share_url }}'); new FilamentNotification().title('Tautan Berhasil Disalin ✨').body('Tautan presensi telah berhasil disalin ke papan klip.').success().send()"
|
||||
color="gray" size="xs" icon="heroicon-m-share"
|
||||
class="rounded-lg shadow-sm font-bold uppercase tracking-tight text-[10px]">
|
||||
Salin Link
|
||||
</x-filament::button>
|
||||
</div>
|
||||
@empty
|
||||
<x-filament::empty-state icon="heroicon-o-calendar" heading="Belum Ada Riwayat Pertemuan"
|
||||
description="Riwayat pertemuan untuk mata kuliah ini belum tersedia. Data presensi akan muncul setelah sesi perkuliahan dilakukan."
|
||||
iconColor="gray">
|
||||
</x-filament::empty-state>
|
||||
@endforelse
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
|
||||
<!-- Right: Ongoing Stats -->
|
||||
<div class="space-y-6">
|
||||
@if ($activeMeetingStats)
|
||||
<div
|
||||
class="fi-card flex flex-col justify-between rounded-xl border transition duration-200 relative bg-primary-50/30 dark:bg-primary-900/10 border-primary-500 shadow-md ring-1 ring-primary-500">
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="space-y-1">
|
||||
<h3
|
||||
class="text-lg font-bold leading-tight flex items-center gap-2 text-primary-900 dark:text-primary-100">
|
||||
<x-heroicon-o-presentation-chart-line class="w-5 h-5 shrink-0 text-primary-500" />
|
||||
Monitoring Sesi Ini
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 pt-4 border-t border-primary-200 dark:border-primary-800">
|
||||
<div
|
||||
class="flex justify-between items-center text-[10px] font-bold uppercase tracking-widest text-primary-700 dark:text-primary-300">
|
||||
<span>WAKTU PERKULIAHAN</span>
|
||||
<span
|
||||
class="font-mono bg-primary-100 dark:bg-primary-800 px-2 py-0.5 rounded text-primary-600 dark:text-primary-400">{{ $activeMeetingStats->time_range }}</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-end">
|
||||
<div
|
||||
class="text-3xl font-black text-primary-950 dark:text-white tracking-tight">
|
||||
{{ $activeMeetingStats->attended_count }} <span
|
||||
class="text-sm font-normal text-primary-600/70 italic">/
|
||||
{{ $activeMeetingStats->total_students }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="text-xs font-bold text-primary-600 dark:text-primary-400 bg-primary-100 dark:bg-primary-800 px-2 py-1 rounded-lg">
|
||||
{{ $activeMeetingStats->percentage }}%
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-3 bg-white/50 dark:bg-gray-800/50 rounded-full overflow-hidden border border-primary-200 dark:border-primary-700 p-0.5">
|
||||
<div class="h-full bg-primary-500 rounded-full transition-all duration-1000 shadow-[0_0_12px_rgba(var(--primary-500),0.4)]"
|
||||
style="width: {{ $activeMeetingStats->percentage }}%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-primary-200 dark:border-primary-800">
|
||||
<h5
|
||||
class="text-[10px] font-bold text-primary-700 dark:text-primary-300 uppercase tracking-widest mb-3 flex items-center justify-between">
|
||||
<span>Belum Melakukan Absen</span>
|
||||
<span
|
||||
class="bg-warning-500 text-white px-2 py-0.5 rounded text-[10px] shadow-sm font-bold">{{ $activeMeetingStats->total_students - $activeMeetingStats->attended_count }}</span>
|
||||
</h5>
|
||||
<div class="max-h-[350px] overflow-y-auto space-y-2 thin-scrollbar pr-1">
|
||||
@foreach ($activeMeetingStats->absent_students as $absent)
|
||||
<div
|
||||
class="p-3 rounded-xl bg-white/60 dark:bg-gray-900/60 border border-primary-200/50 dark:border-primary-800/50 text-xs shadow-sm group hover:border-warning-500 transition">
|
||||
<div class="font-bold text-gray-950 dark:text-white truncate">
|
||||
{{ $absent->full_name }}</div>
|
||||
<div class="text-[10px] text-gray-500 font-mono mt-0.5">
|
||||
{{ $absent->nim }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<x-filament::empty-state icon="heroicon-o-clock" heading="Tidak Ada Sesi Perkuliahan Aktif"
|
||||
description="Saat ini belum ada sesi perkuliahan yang berlangsung. Silakan lihat daftar riwayat pertemuan di sebelah kiri."
|
||||
iconColor="gray">
|
||||
</x-filament::empty-state>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.thin-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dark .thin-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
||||
</x-filament-panels::page>
|
||||
@ -1,102 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">Monitoring Presensi</x-slot>
|
||||
<x-slot name="description">Pilih mata kuliah untuk memantau kehadiran mahasiswa per pertemuan.</x-slot>
|
||||
|
||||
<div class="columns-1 md:columns-2 lg:columns-3 xl:columns-4 gap-6 space-y-6">
|
||||
@foreach ($courses as $course)
|
||||
<a href="{{ $course->detail_url }}" class="break-inside-avoid mb-6 block group">
|
||||
<div @class([
|
||||
'fi-card flex flex-col justify-between rounded-xl border transition duration-200 group relative',
|
||||
'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md hover:border-primary-500' => !$course->is_ongoing,
|
||||
'bg-primary-50/30 dark:bg-primary-900/10 border-primary-500 shadow-md ring-1 ring-primary-500' =>
|
||||
$course->is_ongoing,
|
||||
])>
|
||||
|
||||
@if ($course->is_ongoing)
|
||||
<div class="absolute top-0 right-0 -translate-x-1 -translate-y-1/2 z-10">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full bg-primary-600 px-3 py-1 text-[10px] font-bold text-white shadow-lg uppercase tracking-widest">
|
||||
<div class="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div>
|
||||
Hari Ini
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="p-6 space-y-4 flex-1">
|
||||
<div class="space-y-1">
|
||||
<h3 @class([
|
||||
'text-lg font-bold leading-tight flex items-center gap-2 transition-colors',
|
||||
'text-gray-950 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400' => !$course->is_ongoing,
|
||||
'text-primary-900 dark:text-primary-100' => $course->is_ongoing,
|
||||
])>
|
||||
<x-heroicon-o-folder @class([
|
||||
'w-5 h-5 shrink-0',
|
||||
'opacity-40' => !$course->is_ongoing,
|
||||
'text-primary-500' => $course->is_ongoing,
|
||||
]) />
|
||||
{{ $course->name }}
|
||||
</h3>
|
||||
<div @class([
|
||||
'flex items-center gap-1.5',
|
||||
'text-gray-500 dark:text-gray-400' => !$course->is_ongoing,
|
||||
'text-primary-600 dark:text-primary-400' => $course->is_ongoing,
|
||||
])>
|
||||
<x-heroicon-m-hashtag @class(['w-3.5 h-3.5 shrink-0', 'opacity-50' => !$course->is_ongoing]) />
|
||||
<span class="text-[11px] font-medium leading-none uppercase tracking-widest">
|
||||
{{ $course->code }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div @class([
|
||||
'pt-4 border-t',
|
||||
'border-gray-100 dark:border-gray-800' => !$course->is_ongoing,
|
||||
'border-primary-200 dark:border-primary-800' => $course->is_ongoing,
|
||||
])>
|
||||
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
<div @class([
|
||||
'w-8 h-8 rounded-full flex items-center justify-center mr-3 shrink-0 border',
|
||||
'bg-primary-100 dark:bg-primary-900/40 border-primary-200 dark:border-primary-800' => !$course->is_ongoing,
|
||||
'bg-white dark:bg-primary-800 border-primary-300 dark:border-primary-600' =>
|
||||
$course->is_ongoing,
|
||||
])>
|
||||
<x-heroicon-m-user @class([
|
||||
'w-4 h-4',
|
||||
'text-primary-600 dark:text-primary-300' => !$course->is_ongoing,
|
||||
'text-primary-700 dark:text-primary-200' => $course->is_ongoing,
|
||||
]) />
|
||||
</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">Dosen
|
||||
Pengampu</span>
|
||||
<span @class([
|
||||
'truncate font-medium',
|
||||
'text-gray-900 dark:text-gray-200' => !$course->is_ongoing,
|
||||
'text-primary-900 dark:text-primary-50' => $course->is_ongoing,
|
||||
])>
|
||||
{{ $course->lecturer_name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div @class([
|
||||
'flex items-center justify-end gap-2 p-4 pt-0 rounded-b-xl',
|
||||
'bg-primary-100/30 dark:bg-primary-900/10 pt-4' => $course->is_ongoing,
|
||||
])>
|
||||
<span
|
||||
class="text-[10px] font-bold text-primary-600 dark:text-primary-400 uppercase tracking-tight group-hover:underline">
|
||||
Buka →
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
@ -0,0 +1,103 @@
|
||||
<x-filament-panels::page>
|
||||
<x-filament::section>
|
||||
<div class="flex items-center justify-between gap-4 mb-6">
|
||||
<div class="w-full max-w-xl">
|
||||
{{ $this->form }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($this->getCourses()->isNotEmpty())
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
@foreach($this->getCourses() as $course)
|
||||
<div
|
||||
x-data="{ open: false }"
|
||||
class="fi-card rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm overflow-hidden transition hover:shadow-md h-fit"
|
||||
>
|
||||
<!-- Course Header Card -->
|
||||
<div
|
||||
@click="open = !open"
|
||||
class="p-5 cursor-pointer space-y-4 group"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="inline-flex items-center rounded-md bg-primary-50 dark:bg-primary-500/10 px-2 py-1 text-xs font-semibold text-primary-700 dark:text-primary-300 ring-1 ring-inset ring-primary-600/20">
|
||||
{{ $course->code }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-400 dark:text-gray-500">
|
||||
<span class="text-xs font-medium">
|
||||
{{ $course->classSessions->count() }} Sesi
|
||||
</span>
|
||||
<x-heroicon-m-chevron-down
|
||||
class="w-4 h-4 transition-transform duration-300"
|
||||
x-bind:class="open ? 'rotate-180' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-bold text-gray-950 dark:text-white leading-tight group-hover:text-primary-600 dark:group-hover:text-primary-400 transition">
|
||||
{{ $course->name }}
|
||||
</h3>
|
||||
<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" />
|
||||
<span class="truncate">{{ $course->lecturer->full_name ?? 'Dosen Belum Ditentukan' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sessions List (Collapsible) -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-collapse
|
||||
x-cloak
|
||||
class="border-t border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/20"
|
||||
>
|
||||
@if($course->classSessions->isNotEmpty())
|
||||
<div class="divide-y divide-gray-100 dark:divide-gray-800 max-h-80 overflow-y-auto">
|
||||
@foreach($course->classSessions as $session)
|
||||
<div class="p-4 flex items-center justify-between hover:bg-gray-100/50 dark:hover:bg-gray-800/50 transition">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-[10px] font-bold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
|
||||
#{{ $session->session_number }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $session->date->format('l, d/m/Y') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[12px] text-gray-600 dark:text-gray-400 line-clamp-1">
|
||||
{{ $session->title ?? 'Materi belum diisi' }}
|
||||
</p>
|
||||
<div class="flex items-center gap-1.5 text-[11px] text-gray-500 dark:text-gray-400">
|
||||
<x-heroicon-o-clock class="w-3.5 h-3.5 text-primary-500" />
|
||||
{{ $session->start_time->format('H:i') }} - {{ $session->end_time->format('H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
{{ ($this->editSessionAction)(['session' => $session->id]) }}
|
||||
{{ ($this->deleteSessionAction)(['session' => $session->id]) }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="p-6 text-center">
|
||||
<x-heroicon-o-chat-bubble-bottom-center-text class="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 italic">
|
||||
Belum ada laporan sesi untuk mata kuliah ini.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<x-filament::empty-state
|
||||
icon="heroicon-o-presentation-chart-bar"
|
||||
heading="Tidak ada mata kuliah"
|
||||
description="Mata kuliah untuk semester ini belum tersedia atau tidak sesuai dengan pencarian."
|
||||
/>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</x-filament-panels::page>
|
||||
@ -0,0 +1,58 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<a href="{{ \App\Filament\Resources\Learning\ClassSessions\ClassSessionResource::getUrl('index') }}"
|
||||
class="fi-btn fi-btn-size-md relative grid-flow-col items-center justify-center font-semibold outline-none transition duration-75 focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-70 fi-btn-color-gray fi-color-gray bg-white dark:bg-white/5 text-gray-950 dark:text-white shadow-sm ring-1 ring-gray-950/10 dark:ring-white/20 hover:bg-gray-50 dark:hover:bg-white/10 fi-btn-icon-start gap-1 p-2 rounded-lg">
|
||||
<x-heroicon-m-arrow-left class="w-5 h-5 text-gray-500" />
|
||||
</a>
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-gray-950 dark:text-white sm:text-3xl">
|
||||
{{ $this->getTitle() }}
|
||||
</h1>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Data sesi pembelajaran untuk mata kuliah {{ $this->getCourse()->name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-filament::section>
|
||||
@if ($this->getSessions()->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach ($this->getSessions() as $session)
|
||||
<div
|
||||
class="fi-card p-5 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm flex items-center justify-between hover:shadow-md transition">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center w-12 h-12 rounded-lg bg-primary-100 dark:bg-primary-500/10 text-primary-700 dark:text-primary-300 font-bold border border-primary-200 dark:border-primary-500/20">
|
||||
<span class="text-[10px] uppercase leading-none opacity-60 mb-0.5 mt-1">Sesi</span>
|
||||
<span class="text-xl leading-none mb-1">#{{ $session->session_number }}</span>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div class="flex items-center gap-1.5 font-bold text-gray-900 dark:text-white">
|
||||
<x-heroicon-o-calendar class="w-4 h-4 text-primary-500" />
|
||||
{{ $session->date->format('l, d F Y') }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 font-medium">
|
||||
<x-heroicon-o-clock class="w-4 h-4 text-primary-500" />
|
||||
{{ $session->start_time->format('H:i') }} -
|
||||
{{ $session->end_time->format('H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
{{ ($this->editSessionAction)(['session' => $session->id]) }}
|
||||
{{ ($this->deleteSessionAction)(['session' => $session->id]) }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<x-filament::empty-state icon="heroicon-o-presentation-chart-bar" heading="Tidak ada data yang ditemukan"
|
||||
description="Setelah Anda membuat data pertama, maka akan muncul disini." iconColor="gray">
|
||||
</x-filament::empty-state>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</x-filament-panels::page>
|
||||
@ -3,7 +3,7 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
return redirect()->route('filament.admin.auth.login');
|
||||
});
|
||||
|
||||
Route::get('/share/presensi/{token}', [\App\Http\Controllers\ShareAttendanceController::class, 'show'])->name('share.attendance');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user