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 = [
|
protected $fillable = [
|
||||||
'course_id',
|
'course_id',
|
||||||
|
'class_session_id',
|
||||||
'description',
|
'description',
|
||||||
'due_date',
|
'due_date',
|
||||||
'title',
|
'title',
|
||||||
@ -52,6 +53,11 @@ public function course(): BelongsTo
|
|||||||
return $this->belongsTo(Course::class);
|
return $this->belongsTo(Course::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function classSession(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ClassSession::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function students(): BelongsToMany
|
public function students(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(
|
return $this->belongsToMany(
|
||||||
|
|||||||
@ -25,4 +25,9 @@ public function courseSchedule()
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(CourseSchedule::class);
|
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);
|
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);
|
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) {
|
Schema::create('assignments', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('class_session_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->string('title', 100);
|
$table->string('title', 100);
|
||||||
$table->text('description')->nullable();
|
$table->text('description')->nullable();
|
||||||
$table->timestamp('due_date');
|
$table->timestamp('due_date');
|
||||||
|
|||||||
@ -14,12 +14,14 @@ public function up(): void
|
|||||||
Schema::create('attendances', function (Blueprint $table) {
|
Schema::create('attendances', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('student_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('student_id')->constrained()->cascadeOnDelete();
|
||||||
$table->foreignId('course_schedule_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('class_session_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->date('date');
|
$table->foreignId('course_schedule_id')->nullable()->constrained()->cascadeOnDelete();
|
||||||
|
$table->date('date')->nullable();
|
||||||
$table->timestamp('attended_at')->nullable();
|
$table->timestamp('attended_at')->nullable();
|
||||||
$table->timestamps();
|
$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) {
|
Schema::create('materials', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
|
$table->foreignId('course_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('class_session_id')->nullable()->constrained()->nullOnDelete();
|
||||||
$table->string('title', 100);
|
$table->string('title', 100);
|
||||||
$table->text('description')->nullable();
|
$table->text('description')->nullable();
|
||||||
$table->timestamp('published_at')->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;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', function () {
|
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');
|
Route::get('/share/presensi/{token}', [\App\Http\Controllers\ShareAttendanceController::class, 'show'])->name('share.attendance');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user