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\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
|
||||
{
|
||||
|
||||
@ -137,6 +137,7 @@ class="w-5 h-5 text-gray-400 transition duration-300"
|
||||
|
||||
@canAny(['Update:Changelog', 'Delete:Changelog'])
|
||||
<div class="flex items-center gap-2">
|
||||
{{ ($this->viewReadersAction)(['record' => $log->id]) }}
|
||||
{{ ($this->editChangelogAction)(['record' => $log->id]) }}
|
||||
{{ ($this->deleteChangelogAction)(['record' => $log->id]) }}
|
||||
</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\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();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user