feat: add 'view readers' action to changelog with modal displaying student read status
This commit is contained in:
parent
927c9a440a
commit
400a0e3001
@ -9,8 +9,10 @@
|
|||||||
use App\Filament\Pages\System\Changelog\Actions\MarkAsReadAction;
|
use App\Filament\Pages\System\Changelog\Actions\MarkAsReadAction;
|
||||||
use App\Filament\Support\SystemNotification;
|
use App\Filament\Support\SystemNotification;
|
||||||
use App\Models\Changelog;
|
use App\Models\Changelog;
|
||||||
|
use App\Models\Student;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
|
use BezhanSalleh\FilamentShield\Traits\HasPageShield;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\Concerns\InteractsWithActions;
|
use Filament\Actions\Concerns\InteractsWithActions;
|
||||||
use Filament\Actions\Contracts\HasActions;
|
use Filament\Actions\Contracts\HasActions;
|
||||||
@ -23,6 +25,7 @@
|
|||||||
use Filament\Forms\Contracts\HasForms;
|
use Filament\Forms\Contracts\HasForms;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Filament\Schemas\Components\Grid;
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Support\Enums\Width;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\Computed;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
@ -80,10 +83,10 @@ public function changelogs(): Collection
|
|||||||
return Changelog::with([
|
return Changelog::with([
|
||||||
'users' => fn ($query) => $query->where('user_id', auth()->id()),
|
'users' => fn ($query) => $query->where('user_id', auth()->id()),
|
||||||
])
|
])
|
||||||
|
->orderBy('version', 'desc')
|
||||||
->get()
|
->get()
|
||||||
->sortBy([
|
->sortBy([
|
||||||
fn ($a, $b) => $a->is_read <=> $b->is_read,
|
fn ($a, $b) => $a->is_read <=> $b->is_read,
|
||||||
['release_date', 'desc'],
|
|
||||||
])
|
])
|
||||||
->values();
|
->values();
|
||||||
}
|
}
|
||||||
@ -113,6 +116,44 @@ public function markAsReadAction(): Action
|
|||||||
return MarkAsReadAction::make();
|
return MarkAsReadAction::make();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function viewReadersAction(): Action
|
||||||
|
{
|
||||||
|
return Action::make('viewReaders')
|
||||||
|
->label('Lihat')
|
||||||
|
->icon('heroicon-o-eye')
|
||||||
|
->color('info')
|
||||||
|
->link()
|
||||||
|
->tooltip('Daftar Mahasiswa')
|
||||||
|
->modalHeading('Daftar Mahasiswa yang Melihat')
|
||||||
|
->modalWidth(Width::TwoExtraLarge)
|
||||||
|
->modalSubmitAction(false)
|
||||||
|
->modalCancelActionLabel('Tutup')
|
||||||
|
->visible(fn () => auth()->user()?->can('Update:Changelog'))
|
||||||
|
->modalContent(function (array $arguments) {
|
||||||
|
$record = Changelog::find($arguments['record'] ?? null);
|
||||||
|
|
||||||
|
if (! $record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$students = Student::with('user')->get();
|
||||||
|
$readUsersMap = $record->users()->withPivot('read_at')->get()->keyBy('id');
|
||||||
|
|
||||||
|
$records = $students->map(fn ($student) => (object) [
|
||||||
|
'student' => $student,
|
||||||
|
'is_read' => $readUsersMap->has($student->user_id),
|
||||||
|
'read_at' => $readUsersMap->get($student->user_id)?->pivot?->read_at
|
||||||
|
? Carbon::parse($readUsersMap->get($student->user_id)->pivot->read_at)->translatedFormat('d F Y H:i')
|
||||||
|
: null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return view('filament.pages.system.changelog.view-readers', [
|
||||||
|
'records' => $records,
|
||||||
|
'readCount' => $readUsersMap->count(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[Computed]
|
#[Computed]
|
||||||
public function emptyHeading(): string
|
public function emptyHeading(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@ -137,6 +137,7 @@ class="w-5 h-5 text-gray-400 transition duration-300"
|
|||||||
|
|
||||||
@canAny(['Update:Changelog', 'Delete:Changelog'])
|
@canAny(['Update:Changelog', 'Delete:Changelog'])
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
{{ ($this->viewReadersAction)(['record' => $log->id]) }}
|
||||||
{{ ($this->editChangelogAction)(['record' => $log->id]) }}
|
{{ ($this->editChangelogAction)(['record' => $log->id]) }}
|
||||||
{{ ($this->deleteChangelogAction)(['record' => $log->id]) }}
|
{{ ($this->deleteChangelogAction)(['record' => $log->id]) }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
<div class="space-y-3">
|
||||||
|
<div class="fi-ta-content overflow-x-auto overflow-y-auto max-h-150 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
|
||||||
|
<table class="w-full text-sm text-left divider-y dark:divider-white/5 sticky-header-table">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 font-bold text-gray-900 dark:text-white">Mahasiswa</th>
|
||||||
|
<th class="px-4 py-3 font-bold text-gray-900 dark:text-white text-center">Status</th>
|
||||||
|
<th class="px-4 py-3 font-bold text-gray-900 dark:text-white text-right">Waktu Lihat</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-white/5">
|
||||||
|
@foreach ($records as $item)
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-white/5 transition-colors">
|
||||||
|
<td class="px-4 py-3 text-gray-950 dark:text-white font-medium">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>{{ $item->student->full_name }}</span>
|
||||||
|
<span class="text-[10px] text-gray-500 dark:text-gray-400 font-normal uppercase tabular-nums">
|
||||||
|
NIM: {{ $item->student->student_number }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-center">
|
||||||
|
@if ($item->is_read)
|
||||||
|
<span class="inline-flex items-center rounded-md bg-success-50 dark:bg-success-400/10 px-2 py-1 text-xs font-bold text-success-700 dark:text-success-400 ring-1 ring-inset ring-success-600/20">
|
||||||
|
Sudah Melihat
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="inline-flex items-center rounded-md bg-gray-50 dark:bg-gray-400/10 px-2 py-1 text-xs font-bold text-gray-600 dark:text-gray-400 ring-1 ring-inset ring-gray-500/20">
|
||||||
|
Belum Melihat
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right tabular-nums">
|
||||||
|
@if ($item->is_read)
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">
|
||||||
|
{{ $item->read_at }}
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="text-gray-400 dark:text-gray-600 italic">
|
||||||
|
-
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center justify-between px-2">
|
||||||
|
<p class="text-xs text-gray-500 font-medium">
|
||||||
|
Total Mahasiswa: <span class="text-primary-600 dark:text-primary-400 font-bold font-mono">{{ $records->count() }}</span>
|
||||||
|
<span class="mx-2 text-gray-300">|</span>
|
||||||
|
Sudah Melihat: <span class="text-success-600 dark:text-success-400 font-bold font-mono">{{ $readCount }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -4,6 +4,7 @@
|
|||||||
use App\Enums\RoleEnum;
|
use App\Enums\RoleEnum;
|
||||||
use App\Filament\Pages\System\Changelog\Index as ChangelogPage;
|
use App\Filament\Pages\System\Changelog\Index as ChangelogPage;
|
||||||
use App\Models\Changelog;
|
use App\Models\Changelog;
|
||||||
|
use App\Models\Student;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@ -203,16 +204,16 @@
|
|||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns changelogs sorted by latest release date', function () {
|
it('returns changelogs sorted by version descending', function () {
|
||||||
$oldest = Changelog::factory()->create(['release_date' => now()->subYear()]);
|
$oldVersion = Changelog::factory()->create(['version' => 'v1.0.0', 'release_date' => now()]);
|
||||||
$newest = Changelog::factory()->create(['release_date' => now()]);
|
$newVersion = Changelog::factory()->create(['version' => 'v2.0.0', 'release_date' => now()->subYear()]);
|
||||||
|
|
||||||
$component = Livewire::test(ChangelogPage::class);
|
$component = Livewire::test(ChangelogPage::class);
|
||||||
|
$changelogs = $component->get('changelogs');
|
||||||
|
|
||||||
// changelogs() computed prop returns latest first
|
// Newest version should be first even if release_date is older
|
||||||
$ids = Changelog::latest('release_date')->pluck('id')->toArray();
|
expect($changelogs->first()->version)->toBe('v2.0.0');
|
||||||
expect($ids[0])->toBe($newest->id);
|
expect($changelogs->last()->version)->toBe('v1.0.0');
|
||||||
expect($ids[1])->toBe($oldest->id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reflects latest version in techStack', function () {
|
it('reflects latest version in techStack', function () {
|
||||||
@ -308,3 +309,43 @@
|
|||||||
expect(ChangelogPage::getNavigationBadge())->toBe('3');
|
expect(ChangelogPage::getNavigationBadge())->toBe('3');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Changelog Action: View Readers', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$user->assignRole(RoleEnum::Developer);
|
||||||
|
$user->givePermissionTo(['View:Changelog', 'Update:Changelog']);
|
||||||
|
$this->actingAs($user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is visible to users with Update:Changelog permission', function () {
|
||||||
|
$changelog = Changelog::factory()->create();
|
||||||
|
|
||||||
|
Livewire::test(ChangelogPage::class)
|
||||||
|
->assertActionVisible('viewReaders');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is hidden from users without Update:Changelog permission', function () {
|
||||||
|
$kosma = User::factory()->create();
|
||||||
|
$kosma->assignRole(RoleEnum::Kosma);
|
||||||
|
$kosma->givePermissionTo(['View:Changelog']);
|
||||||
|
|
||||||
|
$this->actingAs($kosma);
|
||||||
|
Livewire::test(ChangelogPage::class)
|
||||||
|
->assertActionHidden('viewReaders');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows all students and their read status in the modal', function () {
|
||||||
|
$changelog = Changelog::factory()->create();
|
||||||
|
$student = Student::factory()->create(['full_name' => 'Budi Sudarsono']);
|
||||||
|
$otherStudent = Student::factory()->create(['full_name' => 'Anomali Student']);
|
||||||
|
|
||||||
|
// Mark student as read
|
||||||
|
$changelog->users()->attach($student->user_id, ['read_at' => now()]);
|
||||||
|
|
||||||
|
Livewire::test(ChangelogPage::class)
|
||||||
|
->mountAction('viewReaders', ['record' => $changelog->id])
|
||||||
|
->assertActionMounted('viewReaders')
|
||||||
|
->assertHasNoErrors();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user