feat: implement assignment delivery status workflow with lecturer notification lock and UI updates

This commit is contained in:
Yoga Pangestu 2026-04-07 10:26:26 +07:00
parent 3ee0e49723
commit 2d36e961ba
13 changed files with 350 additions and 58 deletions

View File

@ -0,0 +1,55 @@
<?php
namespace App\Filament\Resources\Learning\Assignments\Actions;
use App\Filament\Support\SystemNotification;
use App\Models\Assignment;
use Filament\Actions\Action;
class MarkAsSentAction extends Action
{
public static function getDefaultName(): ?string
{
return 'markAsSentAction';
}
protected function setUp(): void
{
parent::setUp();
$this->label(function (array $arguments, ?Assignment $record) {
$assignment = $record ?? Assignment::find($arguments['record'] ?? null);
return $assignment?->is_sent_to_lecturer ? 'Buka Kunci' : 'Tandai Terkirim';
})
->icon(function (array $arguments, ?Assignment $record) {
$assignment = $record ?? Assignment::find($arguments['record'] ?? null);
return $assignment?->is_sent_to_lecturer ? 'heroicon-o-lock-open' : 'heroicon-o-check-circle';
})
->color(function (array $arguments, ?Assignment $record) {
$assignment = $record ?? Assignment::find($arguments['record'] ?? null);
return $assignment?->is_sent_to_lecturer ? 'warning' : 'success';
})
->link()
->visible(fn () => auth()->user()->hasRole(['Kosma', 'Developer']))
->action(function (array $arguments, $livewire, ?Assignment $record) {
$assignment = $record ?? Assignment::find($arguments['record'] ?? null);
if (! $assignment) {
return;
}
$assignment->update([
'is_sent_to_lecturer' => ! $assignment->is_sent_to_lecturer,
]);
SystemNotification::send($assignment->is_sent_to_lecturer ? 'assignment_sent' : 'assignment_unlocked')
->send();
if (isset($livewire->assignments)) {
unset($livewire->assignments);
}
});
}
}

View File

@ -7,6 +7,7 @@
use App\Filament\Resources\Learning\Assignments\Actions\CreateAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\DeleteAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\EditAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\MarkAsSentAction;
use App\Filament\Resources\Learning\Assignments\Actions\PinAction;
use App\Filament\Resources\Learning\Assignments\Actions\ShareAssignmentAction;
use App\Filament\Resources\Learning\Assignments\AssignmentResource;
@ -156,6 +157,11 @@ public function shareAssignmentAction(): Action
return ShareAssignmentAction::make();
}
public function markAsSentAction(): Action
{
return MarkAsSentAction::make();
}
#[Computed]
public function assignments(): Collection
{
@ -234,15 +240,20 @@ public function assignments(): Collection
private function getAssignmentPriority($assignment): int
{
$isSent = $assignment->is_sent_to_lecturer;
if ($isSent) {
return 5; // Closed
}
$isSubmitted = $assignment->assignmentSubmissions?->isNotEmpty();
$isOverdue = now()->isAfter($assignment->due_date);
if (! $isOverdue) {
return $isSubmitted ? 2 : 1;
return $isSubmitted ? 3 : 1; // 1: Active, 3: Completed Active
}
// Overdue
return $isSubmitted ? 3 : 4;
// Overdue but not sent
return $isSubmitted ? 4 : 2; // 2: Actionable Overdue, 4: Completed Overdue
}
#[Computed]
@ -269,8 +280,10 @@ public function assignmentCards()
$isLeader = $userGroup && $userGroup->leader_id === $studentProfile->id;
}
$isSentToLecturer = $assignment->is_sent_to_lecturer;
$isOverdue = now()->isAfter($assignment->due_date);
$canSubmit = ! $isOverdue;
$canSubmit = ! $isSentToLecturer;
$canSubmitByRole = ! $isGroup || $isLeader;
$canSubmitActual = $canSubmit && $canSubmitByRole;
@ -279,30 +292,35 @@ public function assignmentCards()
$isNew = $assignment->created_at?->diffInDays(now()) <= 3;
$isPinned = in_array($assignment->id, $pinnedIds);
$statusLabel = SystemNotification::getByKey('labels.assignment_status.not_submitted');
$statusColor = 'warning';
$statusIcon = 'heroicon-o-arrow-up-tray';
if ($isSubmitted) {
$statusLabel = SystemNotification::getByKey('labels.assignment_status.submitted');
$statusColor = 'success';
$statusIcon = 'heroicon-o-check-circle';
$submissionLabel = SystemNotification::getByKey('labels.assignment_status.submitted');
$submissionColor = 'success';
$submissionIcon = 'heroicon-o-check-circle';
} elseif ($isOverdue) {
$statusLabel = SystemNotification::getByKey('labels.assignment_status.overdue');
$statusColor = 'danger';
$statusIcon = 'heroicon-o-clock';
$submissionLabel = SystemNotification::getByKey('labels.assignment_status.overdue');
$submissionColor = 'danger';
$submissionIcon = 'heroicon-o-clock';
} elseif ($isGroup && ! $isLeader && ! $isSubmitted) {
$statusLabel = SystemNotification::getByKey('labels.assignment_status.waiting_leader');
$statusColor = 'gray';
$statusIcon = 'heroicon-o-user-group';
$submissionLabel = SystemNotification::getByKey('labels.assignment_status.waiting_leader');
$submissionColor = 'gray';
$submissionIcon = 'heroicon-o-user-group';
} else {
$submissionLabel = SystemNotification::getByKey('labels.assignment_status.not_submitted');
$submissionColor = 'warning';
$submissionIcon = 'heroicon-o-arrow-up-tray';
}
$deliveryLabel = $isSentToLecturer ? 'Telah Dikirim' : 'Belum Dikirim';
$deliveryColor = $isSentToLecturer ? 'success' : 'amber';
$deliveryIcon = $isSentToLecturer ? 'heroicon-o-check-badge' : 'heroicon-o-clock';
return (object) [
'id' => $assignment->id,
'title' => $assignment->title,
'course_name' => $assignment->course?->name ?? '-',
'due_date_formatted' => $assignment->due_date?->translatedFormat('l, d F Y H:i'),
'submitted_at_formatted' => ($isSubmitted && $submission->submitted_at) ? $submission->submitted_at?->translatedFormat('l, d F Y H:i') : null,
'is_sent_to_lecturer' => $isSentToLecturer,
'is_submitted' => $isSubmitted,
'is_group' => $isGroup,
'is_leader' => $isLeader,
@ -311,8 +329,28 @@ public function assignmentCards()
'is_urgent' => $isUrgent,
'is_new' => $isNew,
'is_pinned' => $isPinned,
'status_label' => $statusLabel,
'status_icon' => $statusIcon,
'submission_status' => (object) [
'label' => $submissionLabel,
'color' => $submissionColor,
'icon' => $submissionIcon,
'classes' => Arr::toCssClasses([
'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold', // font-semibold for clarity
'bg-success-100 text-success-700 dark:bg-success-900/30 dark:text-success-400' => $submissionColor === 'success',
'bg-danger-100 text-danger-700 dark:bg-danger-900/30 dark:text-danger-400' => $submissionColor === 'danger',
'bg-warning-100 text-warning-700 dark:bg-warning-900/30 dark:text-warning-400' => $submissionColor === 'warning',
'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400' => $submissionColor === 'gray',
]),
],
'delivery_status' => (object) [
'label' => $deliveryLabel,
'color' => $deliveryColor,
'icon' => $deliveryIcon,
'classes' => Arr::toCssClasses([
'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-bold ring-1 ring-inset', // font-bold and ring for "delivery" status to make it pop
'bg-success-50 text-success-700 ring-success-600/20 dark:bg-success-900/10 dark:text-success-400 dark:ring-success-500/20' => $deliveryColor === 'success',
'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-900/10 dark:text-amber-400 dark:ring-amber-500/20' => $deliveryColor === 'amber',
]),
],
'url' => AssignmentResource::getUrl('submit', ['record' => $assignment->id]),
// Pre-calculated classes
'card_classes' => Arr::toCssClasses([
@ -327,14 +365,6 @@ public function assignmentCards()
'bg-danger-50 dark:bg-danger-900/20 text-danger-600 dark:text-danger-400' => ! $isSubmitted && $isOverdue,
'bg-warning-50 dark:bg-warning-900/20 text-warning-600 dark:text-warning-400' => ! $isSubmitted && ! $isOverdue,
]),
'status_badge_classes' => Arr::toCssClasses([
'inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium',
'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400' => $statusColor === 'amber',
'bg-success-100 text-success-700 dark:bg-success-900/30 dark:text-success-400' => $statusColor === 'success',
'bg-danger-100 text-danger-700 dark:bg-danger-900/30 dark:text-danger-400' => $statusColor === 'danger',
'bg-warning-100 text-warning-700 dark:bg-warning-900/30 dark:text-warning-400' => $statusColor === 'warning',
'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400' => $statusColor === 'gray',
]),
'indicator_icon_classes' => Arr::toCssClasses([
'h-5 w-5 opacity-20',
'text-primary-500' => $isLeader,

View File

@ -4,7 +4,9 @@
use App\Enums\AssignmentType;
use App\Filament\Actions\BackAction;
use App\Filament\Resources\Learning\Assignments\Actions\MarkAsSentAction;
use App\Filament\Resources\Learning\Assignments\AssignmentResource;
use App\Filament\Support\SystemNotification;
use App\Models\Assignment;
use App\Models\AssignmentSubmission;
use Filament\Actions\Action;
@ -53,6 +55,14 @@ public function statCards(): array
'icon' => 'heroicon-o-chart-bar',
'color_classes' => 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400',
],
[
'label' => 'Status Dosen',
'value' => $this->record?->is_sent_to_lecturer ? 'SUDAH TERKIRIM' : 'BELUM TERKIRIM',
'icon' => $this->record?->is_sent_to_lecturer ? 'heroicon-o-check-badge' : 'heroicon-o-clock',
'color_classes' => $this->record?->is_sent_to_lecturer
? 'bg-success-500 text-white shadow-lg shadow-success-200 dark:shadow-none'
: 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400',
],
];
}
@ -164,9 +174,18 @@ public function assignmentSummary(): array
'due_date' => $this->record?->due_date?->translatedFormat('l, d F Y H:i'),
'type' => $this->record?->type->value,
'is_overdue' => $this->isOverdue,
'is_sent_to_lecturer' => $this->record?->is_sent_to_lecturer,
];
}
#[Computed]
public function sectionIcon(): string
{
return SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful
? 'heroicon-o-document-magnifying-glass'
: 'heroicon-o-document-text';
}
public function previewSubmissionAction(): Action
{
return Action::make('previewSubmission')
@ -186,6 +205,8 @@ public function previewSubmissionAction(): Action
protected function getHeaderActions(): array
{
return [
MarkAsSentAction::make()
->record($this->record),
BackAction::make()
->url(AssignmentResource::getUrl()),

View File

@ -56,6 +56,9 @@ public function form(Schema $schema): Schema
#[Computed]
public function statusCards(): array
{
$isSent = $this->record->is_sent_to_lecturer;
$existing = $this->existingSubmission;
return [
[
'label' => 'Batas Waktu',
@ -71,14 +74,28 @@ public function statusCards(): array
],
[
'label' => 'Status Pengumpulan',
'value' => $this->isOverdue ? 'Ditutup' : 'Terbuka',
'icon' => $this->isOverdue ? 'heroicon-o-lock-closed' : 'heroicon-o-check-circle',
'is_danger' => $this->isOverdue,
'is_success' => ! $this->isOverdue,
'value' => $existing ? 'Sudah Dikumpulkan' : ($isSent ? 'Terlambat & Terkunci' : ($this->isOverdue ? 'Belum (Masih Terbuka)' : 'Belum Dikumpulkan')),
'icon' => $existing ? 'heroicon-o-check-circle' : ($isSent ? 'heroicon-o-lock-closed' : 'heroicon-o-arrow-up-tray'),
'is_success' => (bool) $existing,
'is_danger' => ! $existing && $isSent,
'is_warning' => ! $existing && ! $isSent && $this->isOverdue,
'icon_classes' => Arr::toCssClasses([
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',
'bg-danger-50 dark:bg-danger-900/20 text-danger-600' => $this->isOverdue,
'bg-success-50 dark:bg-success-900/20 text-success-600' => ! $this->isOverdue,
'bg-success-50 dark:bg-success-900/20 text-success-600' => $existing,
'bg-danger-50 dark:bg-danger-900/20 text-danger-600' => ! $existing && $isSent,
'bg-warning-50 dark:bg-warning-900/20 text-warning-600' => ! $existing && ! $isSent && $this->isOverdue,
]),
],
[
'label' => 'Status ke Dosen',
'value' => $isSent ? 'Telah Dikirim' : 'Belum Dikirim',
'icon' => $isSent ? 'heroicon-o-paper-airplane' : 'heroicon-o-clock',
'is_success' => $isSent,
'badge' => $isSent ? '(Kunci Aktif)' : '(Masih Terbuka)',
'icon_classes' => Arr::toCssClasses([
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',
'bg-success-50 dark:bg-success-900/20 text-success-600' => $isSent,
'bg-gray-100 dark:bg-gray-800 text-gray-500' => ! $isSent,
]),
],
];
@ -243,6 +260,13 @@ public function submit(): void
return;
}
if ($assignment->is_sent_to_lecturer) {
SystemNotification::send('submission_locked', type: 'danger')
->send();
return;
}
if (! $this->canSubmit) {
SystemNotification::send('submission_not_leader', type: 'danger')
->send();
@ -313,7 +337,7 @@ protected function getHeaderActions(): array
return [
BackAction::make()
->url(url()->previous() ?? AssignmentResource::getUrl('index')),
->url(url()->previous() !== AssignmentResource::getUrl('submit', ['record' => $this->record->id]) ? url()->previous() : AssignmentResource::getUrl('index')),
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Filament\Resources\Learning\Attendances\Pages;
use App\Enums\NotifStyle;
use App\Filament\Resources\Learning\Attendances\AttendanceResource;
use App\Filament\Support\SystemNotification;
use App\Models\Attendance;
@ -71,6 +72,22 @@ public function emptyDescription(): string
return SystemNotification::getByKey('labels.empty_attendance.description');
}
#[Computed]
public function sectionIcon(): string
{
return SystemNotification::getNotifStyle() === NotifStyle::Cheerful
? 'heroicon-o-hand-raised'
: 'heroicon-o-clipboard-document-check';
}
#[Computed]
public function historyIcon(): string
{
return SystemNotification::getNotifStyle() === NotifStyle::Cheerful
? 'heroicon-o-clock'
: 'heroicon-o-archive-box';
}
public function getSchedules()
{
/** @var User|null $user */

View File

@ -30,6 +30,7 @@ protected function casts(): array
return [
'due_date' => 'datetime',
'type' => AssignmentType::class,
'is_sent_to_lecturer' => 'boolean',
];
}

View File

@ -0,0 +1,30 @@
<?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
{
if (!Schema::hasColumn('assignments', 'is_sent_to_lecturer')) {
Schema::table('assignments', function (Blueprint $table) {
$table->boolean('is_sent_to_lecturer')->default(false)->after('due_date');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('assignments', function (Blueprint $table) {
$table->dropColumn('is_sent_to_lecturer');
});
}
};

View File

@ -145,6 +145,18 @@
'title' => 'Mantap! Sudah Dibaca ✅✨',
'body' => 'Pembaruan ":title" sudah ditandai. Makin update makin jago! 🚀🔥',
],
'assignment_sent' => [
'title' => 'Tugas Berhasil Dikirim! 🚀✨',
'body' => 'Tugas kolektif sudah berhasil ditandai terkirim ke dosen. Status pengumpulan resmi ditutup! 🔒',
],
'assignment_unlocked' => [
'title' => 'Kunci Tugas Dibuka! 🔓✨',
'body' => 'Mahasiswa sekarang diperbolehkan kembali untuk mengumpulkan atau memperbarui tugas mereka. 📝',
],
'submission_locked' => [
'title' => 'Tugas Terkunci! 🔒',
'body' => 'Mohon maaf, tugas ini sudah ditandai terkirim ke dosen oleh Kosma dan tidak dapat diubah lagi. 🚫',
],
// UI Labels & Descriptions
'labels' => [
@ -437,6 +449,18 @@
'title' => 'Pembaruan Berhasil Ditandai',
'body' => 'Catatan rilis ":title" telah berhasil ditandai sebagai sudah dibaca.',
],
'assignment_sent' => [
'title' => 'Konfirmasi Pengiriman Berhasil',
'body' => 'Seluruh berkas tugas kolektif telah berhasil ditandai sebagai terkirim ke dosen pengampu.',
],
'assignment_unlocked' => [
'title' => 'Kunci Akses Dibuka',
'body' => 'Otoritas pengumpulan tugas telah diaktifkan kembali untuk seluruh mahasiswa terkait.',
],
'submission_locked' => [
'title' => 'Akses Pengumpulan Ditutup',
'body' => 'Tugas ini telah dalam status final (terkirim ke dosen). Perubahan data tidak lagi dimungkinkan.',
],
// UI Labels & Descriptions
'labels' => [

View File

@ -19,7 +19,7 @@
class="flex flex-1 items-start gap-4 p-5 text-left min-w-0 hover:bg-primary-500/5 transition-colors cursor-pointer">
<div class="{{ $card->icon_wrapper_classes }} h-12 w-12 mt-1 shrink-0">
<x-filament::icon :icon="$card->status_icon" class="h-6 w-6" />
<x-filament::icon :icon="$card->submission_status->icon" class="h-6 w-6" />
</div>
<div class="flex-1 min-w-0">
@ -51,11 +51,16 @@ class="inline-flex items-center gap-1.5 rounded-full bg-blue-100 text-blue-700 d
</span>
@endif
<span class="{{ $card->status_badge_classes }} px-2.5 py-1 whitespace-nowrap">
@if ($card->status_icon)
<x-filament::icon :icon="$card->status_icon" class="h-4 w-4" />
@endif
{{ $card->status_label }}
{{-- Status Pengumpulan (Submission Status) --}}
<span class="{{ $card->submission_status->classes }} whitespace-nowrap">
<x-filament::icon :icon="$card->submission_status->icon" class="h-3.5 w-3.5" />
{{ $card->submission_status->label }}
</span>
{{-- Status Pengiriman ke Dosen (Delivery Status) --}}
<span class="{{ $card->delivery_status->classes }} whitespace-nowrap">
<x-filament::icon :icon="$card->delivery_status->icon" class="h-3.5 w-3.5" />
{{ $card->delivery_status->label }}
</span>
@if ($card->is_urgent)
@ -91,6 +96,7 @@ class="flex flex-wrap items-center gap-3 px-5 py-4 border-t border-gray-100 dark
@endcan
@canAny(['Update:Assignment', 'Delete:Assignment'])
{{ ($this->markAsSentAction)(['record' => $card->id]) }}
{{ ($this->editAssignmentAction)(['record' => $card->id]) }}
{{ ($this->deleteAssignmentAction)(['record' => $card->id]) }}
@endcanAny

View File

@ -26,11 +26,18 @@
style="width: {{ $this->percentage }}%"></div>
</div>
<x-filament::section
icon="{{ \App\Filament\Support\SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful ? 'heroicon-o-document-magnifying-glass' : 'heroicon-o-document-text' }}"
<x-filament::section
icon="{{ $this->sectionIcon }}"
icon-color="primary"
>
<x-slot name="heading">{{ $this->assignmentSummary['title'] }}</x-slot>
<x-slot name="heading">
<div class="flex items-center gap-3">
{{ $this->assignmentSummary['title'] }}
@if ($this->assignmentSummary['is_sent_to_lecturer'])
<x-filament::badge color="success" icon="heroicon-m-check-badge" size="sm">Sudah Dikirim ke Dosen</x-filament::badge>
@endif
</div>
</x-slot>
<x-slot name="description">
{{ $this->assignmentSummary['course'] }}
&nbsp;·&nbsp;

View File

@ -5,7 +5,7 @@
<x-slot name="heading">{{ $this->record->title }}</x-slot>
<x-slot name="description">{{ $this->record->course?->name ?? '-' }}</x-slot>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mb-4">
@foreach ($this->statusCards as $card)
<div class="flex items-center gap-3 rounded-lg border border-gray-200 dark:border-gray-700 p-3">
<div class="{{ $card['icon_classes'] }}">
@ -44,29 +44,28 @@ class="block border-0"></iframe>
@endif
</x-filament::section>
@if ($this->isOverdue)
{{-- Kondisi Form: Muncul jika BELUM dikirim ke dosen --}}
@if ($this->record->is_sent_to_lecturer)
<x-filament::section :icon="$this->overdueIcon" icon-color="danger">
<x-slot name="heading">{{ $this->overdueHeading }}</x-slot>
<x-slot name="description">{{ $this->overdueDescription }}</x-slot>
<x-slot name="heading">Penerimaan Tugas Ditutup</x-slot>
<x-slot name="description">Tugas ini sudah dikirim ke dosen oleh Kosma dan tidak dapat diubah lagi.</x-slot>
<div class="flex items-center gap-3 mb-5">
<div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-danger-50 dark:bg-danger-900/20 text-danger-600">
<x-filament::icon icon="heroicon-o-lock-closed" class="h-5 w-5" />
</div>
<div class="flex flex-wrap items-center gap-2">
<x-filament::badge :color="$this->submissionStatus->badge_color" size="lg">
{{ $this->submissionStatus->badge_label }}
</x-filament::badge>
<div class="flex flex-wrap items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
Tugas sudah dikirim ke dosen pada status final.
</div>
</div>
@if ($url = $this->getSubmissionFileUrl())
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">File yang Dikumpulkan</p>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">File yang Terakhir Dikumpulkan</p>
<div
class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<iframe src="{{ $url }}" width="100%" height="500"
<iframe src="{{ $url }}" width="100%" height="400"
class="block border-0"></iframe>
</div>
</div>
@ -96,6 +95,16 @@ class="block border-0"></iframe>
@endif
</x-filament::section>
@else
@if ($this->isOverdue)
<div class="p-4 rounded-xl border border-warning-200 bg-warning-50 dark:bg-warning-900/10 dark:border-warning-900/20 flex gap-3">
<x-filament::icon icon="heroicon-m-exclamation-triangle" class="h-5 w-5 text-warning-600 shrink-0" />
<div>
<p class="text-sm font-bold text-warning-800 dark:text-warning-400">Tenggat Waktu Sudah Terlewat</p>
<p class="text-xs text-warning-700 dark:text-warning-500 mt-1">Anda masih diperbolehkan mengumpulkan tugas karena belum dikirim ke dosen oleh Kosma.</p>
</div>
</div>
@endif
<x-filament::section :icon="$this->submissionIcon" icon-color="primary">
<x-slot name="heading">{{ $this->submissionHeading }}</x-slot>
<x-slot name="description">{{ $this->submissionDescription }}</x-slot>

View File

@ -1,6 +1,6 @@
<x-filament-panels::page>
<x-filament::section
icon="{{ \App\Filament\Support\SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful ? 'heroicon-o-hand-raised' : 'heroicon-o-clipboard-document-check' }}"
<x-filament::section
icon="{{ $this->sectionIcon }}"
icon-color="primary"
>
<x-slot name="heading">{{ $this->heading }}</x-slot>
@ -102,7 +102,7 @@ class="text-[10px] font-bold text-gray-400 uppercase tracking-widest leading-non
@endforeach
</div>
@else
<x-filament::empty-state icon="heroicon-o-finger-print"
<x-filament::empty-state icon="heroicon-o-finger-print"
heading="{{ $this->emptyHeading }}"
description="{{ $this->emptyDescription }}"
iconColor="gray">
@ -111,8 +111,8 @@ class="text-[10px] font-bold text-gray-400 uppercase tracking-widest leading-non
</x-filament::section>
<x-filament::section
icon="{{ \App\Filament\Support\SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful ? 'heroicon-o-clock' : 'heroicon-o-archive-box' }}"
<x-filament::section
icon="{{ $this->historyIcon }}"
icon-color="gray"
>
<x-slot name="heading">{{ $this->historyHeading }}</x-slot>

View File

@ -366,3 +366,71 @@
->assertSee($assignment->title);
});
});
describe('Assignment Delivery Status', function () {
beforeEach(function () {
$this->kosmaUser = User::factory()->create();
$this->kosmaUser->assignRole(RoleEnum::Kosma);
$this->kosmaUser->givePermissionTo(['ViewAny:Assignment', 'View:Assignment', 'View:CourseSchedule']);
$this->studentUser = User::factory()->create();
$this->studentUser->assignRole(RoleEnum::Student);
$this->studentUser->givePermissionTo(['ViewAny:Assignment', 'View:CourseSchedule']);
$this->student = Student::factory()->create(['user_id' => $this->studentUser->id]);
$this->currentSemester = app(GeneralSettings::class)->current_semester;
});
it('can toggle assignment delivery status (Mark as Sent)', function () {
$assignment = Assignment::factory()->create([
'is_sent_to_lecturer' => false,
]);
$this->actingAs($this->kosmaUser);
Livewire::test(ListAssignments::class)
->callAction('markAsSentAction', [], ['record' => $assignment->id]);
expect($assignment->refresh()->is_sent_to_lecturer)->toBeTrue();
Livewire::test(ListAssignments::class)
->callAction('markAsSentAction', [], ['record' => $assignment->id]);
expect($assignment->refresh()->is_sent_to_lecturer)->toBeFalse();
});
it('allows students to submit overdue assignments if not yet sent to lecturer', function () {
$assignment = Assignment::factory()->create([
'due_date' => now()->subDays(2)->toDateTimeString(), // Far enough in the past
'is_sent_to_lecturer' => false,
'type' => AssignmentType::Individual,
]);
$assignment->students()->attach($this->student->id);
$assignment->course->update(['semester' => $this->currentSemester]);
$this->actingAs($this->studentUser);
Livewire::test(SubmitAssignmentPage::class, ['record' => $assignment])
->assertSuccessful()
->assertSet('isOverdue', true)
->assertSet('canSubmit', true)
->assertSee('Tenggat Waktu Sudah Terlewat');
});
it('prevents students from submitting if assignment is marked as sent to lecturer', function () {
$assignment = Assignment::factory()->create([
'due_date' => now()->addDay(),
'is_sent_to_lecturer' => true,
]);
$assignment->students()->attach($this->student->id);
$assignment->course->update(['semester' => $this->currentSemester]);
$this->actingAs($this->studentUser);
Livewire::test(SubmitAssignmentPage::class, ['record' => $assignment])
->assertSuccessful()
->assertSee('Penerimaan Tugas Ditutup')
->assertDontSeeHtml('type="file"');
});
});