diff --git a/app/Filament/Resources/Votes/Pages/ViewVote.php b/app/Filament/Resources/Votes/Pages/ViewVote.php new file mode 100644 index 0000000..d2c4efd --- /dev/null +++ b/app/Filament/Resources/Votes/Pages/ViewVote.php @@ -0,0 +1,85 @@ +label('Beri Suara') + ->icon('heroicon-o-hand-raised') + ->color('info') + ->visible(fn () => $this->record->status === 'open' + && ! $this->record->items()->where('user_id', auth()->id())->exists() + && (! $this->record->deadline || $this->record->deadline->isFuture())) + ->form([ + \Filament\Forms\Components\Radio::make('choice') + ->label('Pilihan Anda') + ->options([ + 'approve' => '✓ Setuju', + 'reject' => '✗ Tidak Setuju', + 'abstain' => '○ Abstain', + ]) + ->required(), + ]) + ->action(function (array $data): void { + VoteItem::create([ + 'vote_id' => $this->record->id, + 'user_id' => auth()->id(), + 'choice' => $data['choice'], + ]); + $this->refreshFormData([]); + }), + + EditAction::make(), + ]; + } + + public function infolist(Schema $infolist): Schema + { + $record = $this->record; + $total = $record->items()->count(); + $approve = $record->items()->where('choice', 'approve')->count(); + $reject = $record->items()->where('choice', 'reject')->count(); + $abstain = $record->items()->where('choice', 'abstain')->count(); + $quorum = $total > 0 && ($approve / $total) > 0.5 ? '✓ Mayoritas Setuju' : '✗ Belum Mayoritas'; + + return $infolist->schema([ + Section::make('Detail Voting')->schema([ + TextEntry::make('title')->label('Judul'), + TextEntry::make('description')->label('Deskripsi'), + TextEntry::make('type')->badge(), + TextEntry::make('status')->badge() + ->color(fn ($state) => $state === 'open' ? 'success' : 'gray'), + TextEntry::make('deadline')->label('Batas Waktu')->dateTime('d M Y H:i'), + TextEntry::make('creator.name')->label('Dibuat Oleh'), + ])->columns(2), + + Section::make('Rekapitulasi Suara')->schema([ + TextEntry::make('total')->label('Total Suara')->state($total), + TextEntry::make('approve')->label('Setuju')->state($approve) + ->color('success'), + TextEntry::make('reject')->label('Tidak Setuju')->state($reject) + ->color('danger'), + TextEntry::make('abstain')->label('Abstain')->state($abstain) + ->color('gray'), + TextEntry::make('result')->label('Hasil')->state($total > 0 ? $quorum : '-') + ->color($total > 0 && ($approve / max($total, 1)) > 0.5 ? 'success' : 'warning'), + ])->columns(5), + ]); + } +} diff --git a/app/Filament/Resources/Votes/Tables/VotesTable.php b/app/Filament/Resources/Votes/Tables/VotesTable.php index f80dde2..69d1ac5 100644 --- a/app/Filament/Resources/Votes/Tables/VotesTable.php +++ b/app/Filament/Resources/Votes/Tables/VotesTable.php @@ -2,9 +2,13 @@ namespace App\Filament\Resources\Votes\Tables; +use App\Models\Vote; +use App\Models\VoteItem; +use Filament\Actions\Action; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; +use Filament\Actions\ViewAction; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; @@ -21,6 +25,18 @@ class VotesTable ->color(fn ($state) => $state === 'open' ? 'success' : 'gray'), TextColumn::make('deadline')->label('Batas Waktu')->dateTime('d M Y H:i'), TextColumn::make('items_count')->counts('items')->label('Suara'), + TextColumn::make('result') + ->label('Hasil') + ->state(function (Vote $record): string { + $total = $record->items()->count(); + $approve = $record->items()->where('choice', 'approve')->count(); + $reject = $record->items()->where('choice', 'reject')->count(); + $abstain = $record->items()->where('choice', 'abstain')->count(); + + if ($total === 0) return '-'; + + return "✓{$approve} ✗{$reject} ○{$abstain}"; + }), ]) ->filters([ SelectFilter::make('status')->options(['open' => 'Buka', 'closed' => 'Tutup']), @@ -30,7 +46,61 @@ class VotesTable 'general' => 'Umum', ]), ]) - ->recordActions([EditAction::make()]) + ->recordActions([ + Action::make('vote') + ->label('Beri Suara') + ->icon('heroicon-o-hand-raised') + ->color('info') + ->visible(fn (Vote $record) => $record->status === 'open' + && ! $record->items()->where('user_id', auth()->id())->exists() + && (! $record->deadline || $record->deadline->isFuture())) + ->form([ + \Filament\Forms\Components\Radio::make('choice') + ->label('Pilihan Anda') + ->options([ + 'approve' => '✓ Setuju', + 'reject' => '✗ Tidak Setuju', + 'abstain' => '○ Abstain', + ]) + ->required(), + ]) + ->action(function (Vote $record, array $data): void { + VoteItem::create([ + 'vote_id' => $record->id, + 'user_id' => auth()->id(), + 'choice' => $data['choice'], + ]); + + \App\Models\ActivityLog::create([ + 'user_id' => auth()->id(), + 'action' => 'voted', + 'model_type' => Vote::class, + 'model_id' => $record->id, + 'description' => auth()->user()->name . " memilih {$data['choice']} pada voting '{$record->title}'", + ]); + }), + + Action::make('close') + ->label('Tutup Voting') + ->icon('heroicon-o-lock-closed') + ->color('danger') + ->requiresConfirmation() + ->visible(fn (Vote $record) => $record->status === 'open') + ->action(function (Vote $record): void { + $record->update(['status' => 'closed']); + + \App\Models\ActivityLog::create([ + 'user_id' => auth()->id(), + 'action' => 'vote_closed', + 'model_type' => Vote::class, + 'model_id' => $record->id, + 'description' => "Voting '{$record->title}' ditutup", + ]); + }), + + EditAction::make(), + ViewAction::make()->label('Detail'), + ]) ->toolbarActions([BulkActionGroup::make([DeleteBulkAction::make()])]); } } diff --git a/app/Filament/Resources/Votes/VoteResource.php b/app/Filament/Resources/Votes/VoteResource.php index 2196266..a8bdcb9 100644 --- a/app/Filament/Resources/Votes/VoteResource.php +++ b/app/Filament/Resources/Votes/VoteResource.php @@ -5,6 +5,7 @@ namespace App\Filament\Resources\Votes; use App\Filament\Resources\Votes\Pages\CreateVote; use App\Filament\Resources\Votes\Pages\EditVote; use App\Filament\Resources\Votes\Pages\ListVotes; +use App\Filament\Resources\Votes\Pages\ViewVote; use App\Filament\Resources\Votes\Schemas\VoteForm; use App\Filament\Resources\Votes\Tables\VotesTable; use App\Models\Vote; @@ -34,6 +35,7 @@ class VoteResource extends Resource return [ 'index' => ListVotes::route('/'), 'create' => CreateVote::route('/create'), + 'view' => ViewVote::route('/{record}'), 'edit' => EditVote::route('/{record}/edit'), ]; } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 411a6ba..421a5c8 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -14,6 +14,7 @@ class DatabaseSeeder extends Seeder UserSeeder::class, ActivitySeeder::class, CashSeeder::class, + VoteSeeder::class, ]); } } diff --git a/database/seeders/VoteSeeder.php b/database/seeders/VoteSeeder.php new file mode 100644 index 0000000..2183669 --- /dev/null +++ b/database/seeders/VoteSeeder.php @@ -0,0 +1,71 @@ +first(); + $members = User::whereHas('roles')->get(); + + $votes = [ + [ + 'title' => 'Persetujuan Program Kerja 2026', + 'description' => 'Voting untuk menyetujui program kerja organisasi tahun 2026', + 'type' => 'general', + 'status' => 'open', + 'deadline' => now()->addDays(7), + 'created_by' => $ketua?->id, + ], + [ + 'title' => 'Pengadaan Peralatan Olahraga', + 'description' => 'Pembelian peralatan olahraga senilai Rp 2.500.000', + 'type' => 'finance', + 'status' => 'closed', + 'deadline' => now()->subDays(3), + 'created_by' => $ketua?->id, + ], + [ + 'title' => 'Kegiatan Bakti Sosial Ramadan', + 'description' => 'Persetujuan kegiatan bakti sosial pembagian sembako', + 'type' => 'activity', + 'status' => 'open', + 'deadline' => now()->addDays(2), + 'created_by' => $ketua?->id, + ], + ]; + + foreach ($votes as $data) { + $vote = Vote::create($data); + + // Isi suara untuk voting yang sudah closed + if ($vote->status === 'closed') { + foreach ($members as $member) { + VoteItem::create([ + 'vote_id' => $vote->id, + 'user_id' => $member->id, + 'choice' => collect(['approve', 'approve', 'approve', 'reject', 'abstain'])->random(), + ]); + } + } + + // Isi sebagian suara untuk voting yang masih open + if ($vote->status === 'open') { + foreach ($members->take(3) as $member) { + VoteItem::create([ + 'vote_id' => $vote->id, + 'user_id' => $member->id, + 'choice' => collect(['approve', 'approve', 'reject'])->random(), + ]); + } + } + } + } +}