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\CreateAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\DeleteAssignmentAction; use App\Filament\Resources\Learning\Assignments\Actions\DeleteAssignmentAction;
use App\Filament\Resources\Learning\Assignments\Actions\EditAssignmentAction; 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\PinAction;
use App\Filament\Resources\Learning\Assignments\Actions\ShareAssignmentAction; use App\Filament\Resources\Learning\Assignments\Actions\ShareAssignmentAction;
use App\Filament\Resources\Learning\Assignments\AssignmentResource; use App\Filament\Resources\Learning\Assignments\AssignmentResource;
@ -156,6 +157,11 @@ public function shareAssignmentAction(): Action
return ShareAssignmentAction::make(); return ShareAssignmentAction::make();
} }
public function markAsSentAction(): Action
{
return MarkAsSentAction::make();
}
#[Computed] #[Computed]
public function assignments(): Collection public function assignments(): Collection
{ {
@ -234,15 +240,20 @@ public function assignments(): Collection
private function getAssignmentPriority($assignment): int private function getAssignmentPriority($assignment): int
{ {
$isSent = $assignment->is_sent_to_lecturer;
if ($isSent) {
return 5; // Closed
}
$isSubmitted = $assignment->assignmentSubmissions?->isNotEmpty(); $isSubmitted = $assignment->assignmentSubmissions?->isNotEmpty();
$isOverdue = now()->isAfter($assignment->due_date); $isOverdue = now()->isAfter($assignment->due_date);
if (! $isOverdue) { if (! $isOverdue) {
return $isSubmitted ? 2 : 1; return $isSubmitted ? 3 : 1; // 1: Active, 3: Completed Active
} }
// Overdue // Overdue but not sent
return $isSubmitted ? 3 : 4; return $isSubmitted ? 4 : 2; // 2: Actionable Overdue, 4: Completed Overdue
} }
#[Computed] #[Computed]
@ -269,8 +280,10 @@ public function assignmentCards()
$isLeader = $userGroup && $userGroup->leader_id === $studentProfile->id; $isLeader = $userGroup && $userGroup->leader_id === $studentProfile->id;
} }
$isSentToLecturer = $assignment->is_sent_to_lecturer;
$isOverdue = now()->isAfter($assignment->due_date); $isOverdue = now()->isAfter($assignment->due_date);
$canSubmit = ! $isOverdue;
$canSubmit = ! $isSentToLecturer;
$canSubmitByRole = ! $isGroup || $isLeader; $canSubmitByRole = ! $isGroup || $isLeader;
$canSubmitActual = $canSubmit && $canSubmitByRole; $canSubmitActual = $canSubmit && $canSubmitByRole;
@ -279,30 +292,35 @@ public function assignmentCards()
$isNew = $assignment->created_at?->diffInDays(now()) <= 3; $isNew = $assignment->created_at?->diffInDays(now()) <= 3;
$isPinned = in_array($assignment->id, $pinnedIds); $isPinned = in_array($assignment->id, $pinnedIds);
$statusLabel = SystemNotification::getByKey('labels.assignment_status.not_submitted');
$statusColor = 'warning';
$statusIcon = 'heroicon-o-arrow-up-tray';
if ($isSubmitted) { if ($isSubmitted) {
$statusLabel = SystemNotification::getByKey('labels.assignment_status.submitted'); $submissionLabel = SystemNotification::getByKey('labels.assignment_status.submitted');
$statusColor = 'success'; $submissionColor = 'success';
$statusIcon = 'heroicon-o-check-circle'; $submissionIcon = 'heroicon-o-check-circle';
} elseif ($isOverdue) { } elseif ($isOverdue) {
$statusLabel = SystemNotification::getByKey('labels.assignment_status.overdue'); $submissionLabel = SystemNotification::getByKey('labels.assignment_status.overdue');
$statusColor = 'danger'; $submissionColor = 'danger';
$statusIcon = 'heroicon-o-clock'; $submissionIcon = 'heroicon-o-clock';
} elseif ($isGroup && ! $isLeader && ! $isSubmitted) { } elseif ($isGroup && ! $isLeader && ! $isSubmitted) {
$statusLabel = SystemNotification::getByKey('labels.assignment_status.waiting_leader'); $submissionLabel = SystemNotification::getByKey('labels.assignment_status.waiting_leader');
$statusColor = 'gray'; $submissionColor = 'gray';
$statusIcon = 'heroicon-o-user-group'; $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) [ return (object) [
'id' => $assignment->id, 'id' => $assignment->id,
'title' => $assignment->title, 'title' => $assignment->title,
'course_name' => $assignment->course?->name ?? '-', 'course_name' => $assignment->course?->name ?? '-',
'due_date_formatted' => $assignment->due_date?->translatedFormat('l, d F Y H:i'), '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, '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_submitted' => $isSubmitted,
'is_group' => $isGroup, 'is_group' => $isGroup,
'is_leader' => $isLeader, 'is_leader' => $isLeader,
@ -311,8 +329,28 @@ public function assignmentCards()
'is_urgent' => $isUrgent, 'is_urgent' => $isUrgent,
'is_new' => $isNew, 'is_new' => $isNew,
'is_pinned' => $isPinned, 'is_pinned' => $isPinned,
'status_label' => $statusLabel, 'submission_status' => (object) [
'status_icon' => $statusIcon, '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]), 'url' => AssignmentResource::getUrl('submit', ['record' => $assignment->id]),
// Pre-calculated classes // Pre-calculated classes
'card_classes' => Arr::toCssClasses([ '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-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, '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([ 'indicator_icon_classes' => Arr::toCssClasses([
'h-5 w-5 opacity-20', 'h-5 w-5 opacity-20',
'text-primary-500' => $isLeader, 'text-primary-500' => $isLeader,

View File

@ -4,7 +4,9 @@
use App\Enums\AssignmentType; use App\Enums\AssignmentType;
use App\Filament\Actions\BackAction; use App\Filament\Actions\BackAction;
use App\Filament\Resources\Learning\Assignments\Actions\MarkAsSentAction;
use App\Filament\Resources\Learning\Assignments\AssignmentResource; use App\Filament\Resources\Learning\Assignments\AssignmentResource;
use App\Filament\Support\SystemNotification;
use App\Models\Assignment; use App\Models\Assignment;
use App\Models\AssignmentSubmission; use App\Models\AssignmentSubmission;
use Filament\Actions\Action; use Filament\Actions\Action;
@ -53,6 +55,14 @@ public function statCards(): array
'icon' => 'heroicon-o-chart-bar', 'icon' => 'heroicon-o-chart-bar',
'color_classes' => 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400', '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'), 'due_date' => $this->record?->due_date?->translatedFormat('l, d F Y H:i'),
'type' => $this->record?->type->value, 'type' => $this->record?->type->value,
'is_overdue' => $this->isOverdue, '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 public function previewSubmissionAction(): Action
{ {
return Action::make('previewSubmission') return Action::make('previewSubmission')
@ -186,6 +205,8 @@ public function previewSubmissionAction(): Action
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
MarkAsSentAction::make()
->record($this->record),
BackAction::make() BackAction::make()
->url(AssignmentResource::getUrl()), ->url(AssignmentResource::getUrl()),

View File

@ -56,6 +56,9 @@ public function form(Schema $schema): Schema
#[Computed] #[Computed]
public function statusCards(): array public function statusCards(): array
{ {
$isSent = $this->record->is_sent_to_lecturer;
$existing = $this->existingSubmission;
return [ return [
[ [
'label' => 'Batas Waktu', 'label' => 'Batas Waktu',
@ -71,14 +74,28 @@ public function statusCards(): array
], ],
[ [
'label' => 'Status Pengumpulan', 'label' => 'Status Pengumpulan',
'value' => $this->isOverdue ? 'Ditutup' : 'Terbuka', 'value' => $existing ? 'Sudah Dikumpulkan' : ($isSent ? 'Terlambat & Terkunci' : ($this->isOverdue ? 'Belum (Masih Terbuka)' : 'Belum Dikumpulkan')),
'icon' => $this->isOverdue ? 'heroicon-o-lock-closed' : 'heroicon-o-check-circle', 'icon' => $existing ? 'heroicon-o-check-circle' : ($isSent ? 'heroicon-o-lock-closed' : 'heroicon-o-arrow-up-tray'),
'is_danger' => $this->isOverdue, 'is_success' => (bool) $existing,
'is_success' => ! $this->isOverdue, 'is_danger' => ! $existing && $isSent,
'is_warning' => ! $existing && ! $isSent && $this->isOverdue,
'icon_classes' => Arr::toCssClasses([ 'icon_classes' => Arr::toCssClasses([
'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg', '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' => $existing,
'bg-success-50 dark:bg-success-900/20 text-success-600' => ! $this->isOverdue, '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; return;
} }
if ($assignment->is_sent_to_lecturer) {
SystemNotification::send('submission_locked', type: 'danger')
->send();
return;
}
if (! $this->canSubmit) { if (! $this->canSubmit) {
SystemNotification::send('submission_not_leader', type: 'danger') SystemNotification::send('submission_not_leader', type: 'danger')
->send(); ->send();
@ -313,7 +337,7 @@ protected function getHeaderActions(): array
return [ return [
BackAction::make() 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; namespace App\Filament\Resources\Learning\Attendances\Pages;
use App\Enums\NotifStyle;
use App\Filament\Resources\Learning\Attendances\AttendanceResource; use App\Filament\Resources\Learning\Attendances\AttendanceResource;
use App\Filament\Support\SystemNotification; use App\Filament\Support\SystemNotification;
use App\Models\Attendance; use App\Models\Attendance;
@ -71,6 +72,22 @@ public function emptyDescription(): string
return SystemNotification::getByKey('labels.empty_attendance.description'); 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() public function getSchedules()
{ {
/** @var User|null $user */ /** @var User|null $user */

View File

@ -30,6 +30,7 @@ protected function casts(): array
return [ return [
'due_date' => 'datetime', 'due_date' => 'datetime',
'type' => AssignmentType::class, '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 ✅✨', 'title' => 'Mantap! Sudah Dibaca ✅✨',
'body' => 'Pembaruan ":title" sudah ditandai. Makin update makin jago! 🚀🔥', '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 // UI Labels & Descriptions
'labels' => [ 'labels' => [
@ -437,6 +449,18 @@
'title' => 'Pembaruan Berhasil Ditandai', 'title' => 'Pembaruan Berhasil Ditandai',
'body' => 'Catatan rilis ":title" telah berhasil ditandai sebagai sudah dibaca.', '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 // UI Labels & Descriptions
'labels' => [ '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"> 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"> <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>
<div class="flex-1 min-w-0"> <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> </span>
@endif @endif
<span class="{{ $card->status_badge_classes }} px-2.5 py-1 whitespace-nowrap"> {{-- Status Pengumpulan (Submission Status) --}}
@if ($card->status_icon) <span class="{{ $card->submission_status->classes }} whitespace-nowrap">
<x-filament::icon :icon="$card->status_icon" class="h-4 w-4" /> <x-filament::icon :icon="$card->submission_status->icon" class="h-3.5 w-3.5" />
@endif {{ $card->submission_status->label }}
{{ $card->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> </span>
@if ($card->is_urgent) @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 @endcan
@canAny(['Update:Assignment', 'Delete:Assignment']) @canAny(['Update:Assignment', 'Delete:Assignment'])
{{ ($this->markAsSentAction)(['record' => $card->id]) }}
{{ ($this->editAssignmentAction)(['record' => $card->id]) }} {{ ($this->editAssignmentAction)(['record' => $card->id]) }}
{{ ($this->deleteAssignmentAction)(['record' => $card->id]) }} {{ ($this->deleteAssignmentAction)(['record' => $card->id]) }}
@endcanAny @endcanAny

View File

@ -27,10 +27,17 @@
</div> </div>
<x-filament::section <x-filament::section
icon="{{ \App\Filament\Support\SystemNotification::getNotifStyle() === \App\Enums\NotifStyle::Cheerful ? 'heroicon-o-document-magnifying-glass' : 'heroicon-o-document-text' }}" icon="{{ $this->sectionIcon }}"
icon-color="primary" 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"> <x-slot name="description">
{{ $this->assignmentSummary['course'] }} {{ $this->assignmentSummary['course'] }}
&nbsp;·&nbsp; &nbsp;·&nbsp;

View File

@ -5,7 +5,7 @@
<x-slot name="heading">{{ $this->record->title }}</x-slot> <x-slot name="heading">{{ $this->record->title }}</x-slot>
<x-slot name="description">{{ $this->record->course?->name ?? '-' }}</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) @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="flex items-center gap-3 rounded-lg border border-gray-200 dark:border-gray-700 p-3">
<div class="{{ $card['icon_classes'] }}"> <div class="{{ $card['icon_classes'] }}">
@ -44,29 +44,28 @@ class="block border-0"></iframe>
@endif @endif
</x-filament::section> </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-filament::section :icon="$this->overdueIcon" icon-color="danger">
<x-slot name="heading">{{ $this->overdueHeading }}</x-slot> <x-slot name="heading">Penerimaan Tugas Ditutup</x-slot>
<x-slot name="description">{{ $this->overdueDescription }}</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 items-center gap-3 mb-5">
<div <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"> 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" /> <x-filament::icon icon="heroicon-o-lock-closed" class="h-5 w-5" />
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<x-filament::badge :color="$this->submissionStatus->badge_color" size="lg"> Tugas sudah dikirim ke dosen pada status final.
{{ $this->submissionStatus->badge_label }}
</x-filament::badge>
</div> </div>
</div> </div>
@if ($url = $this->getSubmissionFileUrl()) @if ($url = $this->getSubmissionFileUrl())
<div> <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 <div
class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"> 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> class="block border-0"></iframe>
</div> </div>
</div> </div>
@ -96,6 +95,16 @@ class="block border-0"></iframe>
@endif @endif
</x-filament::section> </x-filament::section>
@else @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-filament::section :icon="$this->submissionIcon" icon-color="primary">
<x-slot name="heading">{{ $this->submissionHeading }}</x-slot> <x-slot name="heading">{{ $this->submissionHeading }}</x-slot>
<x-slot name="description">{{ $this->submissionDescription }}</x-slot> <x-slot name="description">{{ $this->submissionDescription }}</x-slot>

View File

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

View File

@ -366,3 +366,71 @@
->assertSee($assignment->title); ->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"');
});
});