From 400a0e300181dbb111171b49eed589af82350c06 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 7 Apr 2026 13:07:46 +0700 Subject: [PATCH] feat: add 'view readers' action to changelog with modal displaying student read status --- app/Filament/Pages/System/Changelog/Index.php | 43 +++++++++++++- .../pages/system/changelog/index.blade.php | 1 + .../system/changelog/view-readers.blade.php | 57 +++++++++++++++++++ tests/Feature/ChangelogTest.php | 55 +++++++++++++++--- 4 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 resources/views/filament/pages/system/changelog/view-readers.blade.php diff --git a/app/Filament/Pages/System/Changelog/Index.php b/app/Filament/Pages/System/Changelog/Index.php index 3cd87a1..f1e30f2 100644 --- a/app/Filament/Pages/System/Changelog/Index.php +++ b/app/Filament/Pages/System/Changelog/Index.php @@ -9,8 +9,10 @@ use App\Filament\Pages\System\Changelog\Actions\MarkAsReadAction; use App\Filament\Support\SystemNotification; use App\Models\Changelog; +use App\Models\Student; use BackedEnum; use BezhanSalleh\FilamentShield\Traits\HasPageShield; +use Carbon\Carbon; use Filament\Actions\Action; use Filament\Actions\Concerns\InteractsWithActions; use Filament\Actions\Contracts\HasActions; @@ -23,6 +25,7 @@ use Filament\Forms\Contracts\HasForms; use Filament\Pages\Page; use Filament\Schemas\Components\Grid; +use Filament\Support\Enums\Width; use Illuminate\Support\Collection; use Livewire\Attributes\Computed; use UnitEnum; @@ -80,10 +83,10 @@ public function changelogs(): Collection return Changelog::with([ 'users' => fn ($query) => $query->where('user_id', auth()->id()), ]) + ->orderBy('version', 'desc') ->get() ->sortBy([ fn ($a, $b) => $a->is_read <=> $b->is_read, - ['release_date', 'desc'], ]) ->values(); } @@ -113,6 +116,44 @@ public function markAsReadAction(): Action 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] public function emptyHeading(): string { diff --git a/resources/views/filament/pages/system/changelog/index.blade.php b/resources/views/filament/pages/system/changelog/index.blade.php index 33b8618..7c1bbb8 100644 --- a/resources/views/filament/pages/system/changelog/index.blade.php +++ b/resources/views/filament/pages/system/changelog/index.blade.php @@ -137,6 +137,7 @@ class="w-5 h-5 text-gray-400 transition duration-300" @canAny(['Update:Changelog', 'Delete:Changelog'])
+ {{ ($this->viewReadersAction)(['record' => $log->id]) }} {{ ($this->editChangelogAction)(['record' => $log->id]) }} {{ ($this->deleteChangelogAction)(['record' => $log->id]) }}
diff --git a/resources/views/filament/pages/system/changelog/view-readers.blade.php b/resources/views/filament/pages/system/changelog/view-readers.blade.php new file mode 100644 index 0000000..9b9d301 --- /dev/null +++ b/resources/views/filament/pages/system/changelog/view-readers.blade.php @@ -0,0 +1,57 @@ +
+
+ + + + + + + + + + @foreach ($records as $item) + + + + + + @endforeach + +
MahasiswaStatusWaktu Lihat
+
+ {{ $item->student->full_name }} + + NIM: {{ $item->student->student_number }} + +
+
+ @if ($item->is_read) + + Sudah Melihat + + @else + + Belum Melihat + + @endif + + @if ($item->is_read) + + {{ $item->read_at }} + + @else + + - + + @endif +
+
+ +
+

+ Total Mahasiswa: {{ $records->count() }} + | + Sudah Melihat: {{ $readCount }} +

+
+
diff --git a/tests/Feature/ChangelogTest.php b/tests/Feature/ChangelogTest.php index 2152b0a..a191f13 100644 --- a/tests/Feature/ChangelogTest.php +++ b/tests/Feature/ChangelogTest.php @@ -4,6 +4,7 @@ use App\Enums\RoleEnum; use App\Filament\Pages\System\Changelog\Index as ChangelogPage; use App\Models\Changelog; +use App\Models\Student; use App\Models\User; use Illuminate\Database\QueryException; use Illuminate\Support\Carbon; @@ -203,16 +204,16 @@ $this->actingAs($user); }); - it('returns changelogs sorted by latest release date', function () { - $oldest = Changelog::factory()->create(['release_date' => now()->subYear()]); - $newest = Changelog::factory()->create(['release_date' => now()]); + it('returns changelogs sorted by version descending', function () { + $oldVersion = Changelog::factory()->create(['version' => 'v1.0.0', 'release_date' => now()]); + $newVersion = Changelog::factory()->create(['version' => 'v2.0.0', 'release_date' => now()->subYear()]); $component = Livewire::test(ChangelogPage::class); + $changelogs = $component->get('changelogs'); - // changelogs() computed prop returns latest first - $ids = Changelog::latest('release_date')->pluck('id')->toArray(); - expect($ids[0])->toBe($newest->id); - expect($ids[1])->toBe($oldest->id); + // Newest version should be first even if release_date is older + expect($changelogs->first()->version)->toBe('v2.0.0'); + expect($changelogs->last()->version)->toBe('v1.0.0'); }); it('reflects latest version in techStack', function () { @@ -308,3 +309,43 @@ 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(); + }); +});