feat: implement assignment delivery status workflow with lecturer notification lock and UI updates
This commit is contained in:
parent
3ee0e49723
commit
2d36e961ba
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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()),
|
||||||
|
|||||||
@ -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')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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' => [
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -26,11 +26,18 @@
|
|||||||
style="width: {{ $this->percentage }}%"></div>
|
style="width: {{ $this->percentage }}%"></div>
|
||||||
</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'] }}
|
||||||
·
|
·
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -102,7 +102,7 @@ class="text-[10px] font-bold text-gray-400 uppercase tracking-widest leading-non
|
|||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<x-filament::empty-state icon="heroicon-o-finger-print"
|
<x-filament::empty-state icon="heroicon-o-finger-print"
|
||||||
heading="{{ $this->emptyHeading }}"
|
heading="{{ $this->emptyHeading }}"
|
||||||
description="{{ $this->emptyDescription }}"
|
description="{{ $this->emptyDescription }}"
|
||||||
iconColor="gray">
|
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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@ -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"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user