From d42b23f604c094a6e5baab27cdfa3d29da09e186 Mon Sep 17 00:00:00 2001 From: tuxarmy Date: Fri, 3 Apr 2026 04:34:21 +0700 Subject: [PATCH] feat: pindah route ke /dashboard, tambah seeders, business rules via observers, action verifikasi & approval --- .../Activities/Tables/ActivitiesTable.php | 30 ++++++++++- .../CashRecords/Tables/CashRecordsTable.php | 19 ++++++- app/Observers/ActivityObserver.php | 52 +++++++++++++++++++ app/Observers/CashRecordObserver.php | 46 ++++++++++++++++ app/Observers/UserObserver.php | 44 ++++++++++++++++ app/Providers/AppServiceProvider.php | 21 ++++---- app/Providers/Filament/AdminPanelProvider.php | 2 +- database/seeders/ActivitySeeder.php | 52 +++++++++++++++++++ database/seeders/CashSeeder.php | 36 +++++++++++++ database/seeders/DatabaseSeeder.php | 4 ++ database/seeders/DivisionSeeder.php | 24 +++++++++ database/seeders/UserSeeder.php | 41 +++++++++++++++ 12 files changed, 355 insertions(+), 16 deletions(-) create mode 100644 app/Observers/ActivityObserver.php create mode 100644 app/Observers/CashRecordObserver.php create mode 100644 app/Observers/UserObserver.php create mode 100644 database/seeders/ActivitySeeder.php create mode 100644 database/seeders/CashSeeder.php create mode 100644 database/seeders/DivisionSeeder.php create mode 100644 database/seeders/UserSeeder.php diff --git a/app/Filament/Resources/Activities/Tables/ActivitiesTable.php b/app/Filament/Resources/Activities/Tables/ActivitiesTable.php index 077b15a..09e0e2e 100644 --- a/app/Filament/Resources/Activities/Tables/ActivitiesTable.php +++ b/app/Filament/Resources/Activities/Tables/ActivitiesTable.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources\Activities\Tables; +use Filament\Actions\Action; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; @@ -35,7 +36,34 @@ class ActivitiesTable 'rejected' => 'Ditolak', ]), ]) - ->recordActions([EditAction::make()]) + ->recordActions([ + EditAction::make(), + Action::make('submit') + ->label('Ajukan') + ->icon('heroicon-o-paper-airplane') + ->color('info') + ->requiresConfirmation() + ->visible(fn ($record) => $record->status === 'draft') + ->action(fn ($record) => $record->update(['status' => 'pending'])), + Action::make('approve') + ->label('Setujui') + ->icon('heroicon-o-check-circle') + ->color('success') + ->requiresConfirmation() + ->visible(fn ($record) => $record->status === 'pending') + ->action(fn ($record) => $record->update([ + 'status' => 'approved', + 'approved_by' => auth()->id(), + 'approved_at' => now(), + ])), + Action::make('reject') + ->label('Tolak') + ->icon('heroicon-o-x-circle') + ->color('danger') + ->requiresConfirmation() + ->visible(fn ($record) => $record->status === 'pending') + ->action(fn ($record) => $record->update(['status' => 'rejected'])), + ]) ->toolbarActions([BulkActionGroup::make([DeleteBulkAction::make()])]); } } diff --git a/app/Filament/Resources/CashRecords/Tables/CashRecordsTable.php b/app/Filament/Resources/CashRecords/Tables/CashRecordsTable.php index 5ffff84..10176a0 100644 --- a/app/Filament/Resources/CashRecords/Tables/CashRecordsTable.php +++ b/app/Filament/Resources/CashRecords/Tables/CashRecordsTable.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources\CashRecords\Tables; use App\Models\CashCategory; +use Filament\Actions\Action; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; @@ -26,9 +27,23 @@ class CashRecordsTable ]) ->filters([ SelectFilter::make('category_id')->label('Kategori') - ->options(CashCategory::pluck('name', 'id')), + ->options(\App\Models\CashCategory::pluck('name', 'id')), + ]) + ->recordActions([ + EditAction::make()->hidden(fn ($record) => $record->verified_at !== null), + Action::make('verify') + ->label('Verifikasi') + ->icon('heroicon-o-check-circle') + ->color('success') + ->requiresConfirmation() + ->hidden(fn ($record) => $record->verified_at !== null) + ->action(function ($record) { + $record->update([ + 'verified_by' => auth()->id(), + 'verified_at' => now(), + ]); + }), ]) - ->recordActions([EditAction::make()]) ->toolbarActions([BulkActionGroup::make([DeleteBulkAction::make()])]); } } diff --git a/app/Observers/ActivityObserver.php b/app/Observers/ActivityObserver.php new file mode 100644 index 0000000..9623487 --- /dev/null +++ b/app/Observers/ActivityObserver.php @@ -0,0 +1,52 @@ +wasChanged('status')) { + $old = $activity->getOriginal('status'); + $new = $activity->status; + + // Validasi workflow: draft→pending, pending→approved/rejected + $allowed = [ + 'draft' => ['pending'], + 'pending' => ['approved', 'rejected'], + ]; + + if (isset($allowed[$old]) && ! in_array($new, $allowed[$old])) { + throw new \Exception("Transisi status dari {$old} ke {$new} tidak diizinkan."); + } + + // Wajib isi executed_at & execution_notes jika sudah approved dan mau ditandai selesai + if ($new === 'approved' && $activity->wasChanged('executed_at') && empty($activity->execution_notes)) { + throw new \Exception('Catatan pelaksanaan wajib diisi.'); + } + + ActivityLog::create([ + 'user_id' => Auth::id(), + 'action' => 'status_changed', + 'model_type' => Activity::class, + 'model_id' => $activity->id, + 'description' => "Status kegiatan '{$activity->title}' diubah dari {$old} menjadi {$new}", + ]); + } + } + + public function created(Activity $activity): void + { + ActivityLog::create([ + 'user_id' => Auth::id(), + 'action' => 'created', + 'model_type' => Activity::class, + 'model_id' => $activity->id, + 'description' => "Kegiatan baru dibuat: {$activity->title}", + ]); + } +} diff --git a/app/Observers/CashRecordObserver.php b/app/Observers/CashRecordObserver.php new file mode 100644 index 0000000..540be1a --- /dev/null +++ b/app/Observers/CashRecordObserver.php @@ -0,0 +1,46 @@ + Auth::id(), + 'action' => 'created', + 'model_type' => CashRecord::class, + 'model_id' => $record->id, + 'description' => "Transaksi kas baru: {$record->description} sebesar Rp " . number_format($record->amount, 0, ',', '.'), + ]); + } + + public function updated(CashRecord $record): void + { + // Setelah diverifikasi, tidak bisa diubah lagi + if ($record->getOriginal('verified_at') !== null) { + throw new \Exception('Transaksi yang sudah diverifikasi tidak dapat diubah.'); + } + + if ($record->wasChanged('verified_by') && $record->verified_by !== null) { + ActivityLog::create([ + 'user_id' => Auth::id(), + 'action' => 'verified', + 'model_type' => CashRecord::class, + 'model_id' => $record->id, + 'description' => "Transaksi kas diverifikasi: {$record->description}", + ]); + } + } + + public function deleting(CashRecord $record): void + { + if ($record->verified_at !== null) { + throw new \Exception('Transaksi yang sudah diverifikasi tidak dapat dihapus.'); + } + } +} diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php new file mode 100644 index 0000000..e38cbd4 --- /dev/null +++ b/app/Observers/UserObserver.php @@ -0,0 +1,44 @@ +wasChanged('status')) { + MemberStatusLog::create([ + 'member_id' => $user->id, + 'changed_by' => Auth::id() ?? $user->id, + 'old_status' => $user->getOriginal('status'), + 'new_status' => $user->status, + 'reason' => $user->inactive_reason, + ]); + + ActivityLog::create([ + 'user_id' => Auth::id(), + 'action' => 'status_changed', + 'model_type' => User::class, + 'model_id' => $user->id, + 'description' => "Status anggota {$user->name} diubah dari {$user->getOriginal('status')} menjadi {$user->status}", + ]); + } + } + + public function created(User $user): void + { + ActivityLog::create([ + 'user_id' => Auth::id(), + 'action' => 'created', + 'model_type' => User::class, + 'model_id' => $user->id, + 'description' => "Anggota baru {$user->name} ditambahkan", + ]); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..5cb7c03 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,23 +2,20 @@ namespace App\Providers; +use App\Models\Activity; +use App\Models\CashRecord; +use App\Models\User; +use App\Observers\ActivityObserver; +use App\Observers\CashRecordObserver; +use App\Observers\UserObserver; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { - /** - * Register any application services. - */ - public function register(): void - { - // - } - - /** - * Bootstrap any application services. - */ public function boot(): void { - // + User::observe(UserObserver::class); + CashRecord::observe(CashRecordObserver::class); + Activity::observe(ActivityObserver::class); } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index ef98aab..eecdbd0 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -27,7 +27,7 @@ class AdminPanelProvider extends PanelProvider return $panel ->default() ->id('admin') - ->path('admin') + ->path('dashboard') ->login() ->colors([ 'primary' => Color::Amber, diff --git a/database/seeders/ActivitySeeder.php b/database/seeders/ActivitySeeder.php new file mode 100644 index 0000000..ab4ebd0 --- /dev/null +++ b/database/seeders/ActivitySeeder.php @@ -0,0 +1,52 @@ +first(); + $ketua = User::role('ketua')->first(); + $anggota = User::role('anggota')->get(); + + $activities = [ + [ + 'title' => 'Kerja Bakti Desa', + 'description' => 'Kegiatan bersih-bersih lingkungan desa', + 'start_date' => now()->addDays(7), + 'end_date' => now()->addDays(7), + 'status' => 'approved', + 'approved_by' => $ketua?->id, + 'approved_at' => now(), + ], + [ + 'title' => 'Pelatihan Kewirausahaan', + 'description' => 'Pelatihan usaha kecil untuk pemuda desa', + 'start_date' => now()->addDays(14), + 'end_date' => now()->addDays(15), + 'status' => 'pending', + ], + [ + 'title' => 'Turnamen Voli Antar RT', + 'description' => 'Kompetisi voli untuk mempererat silaturahmi', + 'start_date' => now()->subDays(10), + 'end_date' => now()->subDays(8), + 'status' => 'approved', + 'approved_by' => $ketua?->id, + 'approved_at' => now()->subDays(15), + 'executed_at' => now()->subDays(10), + 'execution_notes' => 'Kegiatan berjalan lancar, diikuti 8 tim.', + ], + ]; + + foreach ($activities as $data) { + $activity = Activity::create(array_merge($data, ['created_by' => $pengurus?->id])); + $activity->participants()->sync($anggota->pluck('id')); + } + } +} diff --git a/database/seeders/CashSeeder.php b/database/seeders/CashSeeder.php new file mode 100644 index 0000000..cb08603 --- /dev/null +++ b/database/seeders/CashSeeder.php @@ -0,0 +1,36 @@ + 'pemasukan']); + $pengeluaran = CashCategory::firstOrCreate(['name' => 'pengeluaran']); + + $bendahara = User::role('bendahara')->first(); + $ketua = User::role('ketua')->first(); + + $records = [ + ['category_id' => $pemasukan->id, 'amount' => 500000, 'description' => 'Iuran anggota bulan Januari', 'date' => now()->subMonths(2)], + ['category_id' => $pemasukan->id, 'amount' => 500000, 'description' => 'Iuran anggota bulan Februari', 'date' => now()->subMonth()], + ['category_id' => $pemasukan->id, 'amount' => 1000000, 'description' => 'Donasi kegiatan kerja bakti', 'date' => now()->subDays(20)], + ['category_id' => $pengeluaran->id, 'amount' => 250000, 'description' => 'Pembelian alat kebersihan', 'date' => now()->subDays(18)], + ['category_id' => $pengeluaran->id, 'amount' => 150000, 'description' => 'Konsumsi rapat bulanan', 'date' => now()->subDays(5)], + ]; + + foreach ($records as $data) { + CashRecord::create(array_merge($data, [ + 'created_by' => $bendahara?->id, + 'verified_by' => $ketua?->id, + 'verified_at' => now()->subDays(1), + ])); + } + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6ccb4cb..411a6ba 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -10,6 +10,10 @@ class DatabaseSeeder extends Seeder { $this->call([ RolesAndPermissionsSeeder::class, + DivisionSeeder::class, + UserSeeder::class, + ActivitySeeder::class, + CashSeeder::class, ]); } } diff --git a/database/seeders/DivisionSeeder.php b/database/seeders/DivisionSeeder.php new file mode 100644 index 0000000..84c02b6 --- /dev/null +++ b/database/seeders/DivisionSeeder.php @@ -0,0 +1,24 @@ + 'Humas', 'description' => 'Hubungan masyarakat dan komunikasi'], + ['name' => 'Pendidikan', 'description' => 'Bidang pendidikan dan pelatihan'], + ['name' => 'Olahraga', 'description' => 'Bidang olahraga dan kesehatan'], + ['name' => 'Seni & Budaya', 'description' => 'Bidang seni dan pelestarian budaya'], + ['name' => 'Lingkungan', 'description' => 'Bidang lingkungan hidup'], + ]; + + foreach ($divisions as $division) { + Division::firstOrCreate(['name' => $division['name']], $division); + } + } +} diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 0000000..cc8ef1a --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,41 @@ + 'Budi Santoso', 'email' => 'ketua@persegi.id', 'role' => 'ketua', 'division' => 'Humas'], + ['name' => 'Siti Rahayu', 'email' => 'bendahara@persegi.id','role' => 'bendahara', 'division' => 'Pendidikan'], + ['name' => 'Ahmad Fauzi', 'email' => 'pengurus@persegi.id', 'role' => 'pengurus', 'division' => 'Olahraga'], + ['name' => 'Dewi Lestari', 'email' => 'auditor@persegi.id', 'role' => 'auditor', 'division' => 'Seni & Budaya'], + ['name' => 'Rizky Pratama', 'email' => 'anggota1@persegi.id', 'role' => 'anggota', 'division' => 'Lingkungan'], + ['name' => 'Nur Hidayah', 'email' => 'anggota2@persegi.id', 'role' => 'anggota', 'division' => 'Humas'], + ]; + + foreach ($users as $data) { + $user = User::firstOrCreate( + ['email' => $data['email']], + [ + 'name' => $data['name'], + 'password' => bcrypt('password'), + 'phone' => '08' . rand(100000000, 999999999), + 'address' => 'Desa Karangdadap, Kalibagor, Banyumas', + 'division_id' => $divisions[$data['division']] ?? null, + 'status' => 'aktif', + ] + ); + + $user->syncRoles([$data['role']]); + } + } +}