From 4106eae5cfec303f61d1518a14b067b2c9379b1d Mon Sep 17 00:00:00 2001 From: tuxarmy Date: Sun, 5 Apr 2026 23:18:49 +0700 Subject: [PATCH] feat: tambah budget kegiatan dengan alur threshold approval/voting --- .../Activities/Schemas/ActivityForm.php | 2 ++ .../CashRecords/Schemas/CashRecordForm.php | 4 +++ app/Models/Activity.php | 7 ++++- app/Models/CashRecord.php | 8 ++++- app/Observers/ActivityObserver.php | 23 ++++++++++++++ ...vities_and_activity_id_to_cash_records.php | 31 +++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_04_05_161652_add_budget_to_activities_and_activity_id_to_cash_records.php diff --git a/app/Filament/Resources/Activities/Schemas/ActivityForm.php b/app/Filament/Resources/Activities/Schemas/ActivityForm.php index ed9321e..53e7b52 100644 --- a/app/Filament/Resources/Activities/Schemas/ActivityForm.php +++ b/app/Filament/Resources/Activities/Schemas/ActivityForm.php @@ -17,6 +17,8 @@ class ActivityForm Textarea::make('description')->label('Deskripsi')->rows(3)->columnSpanFull(), DatePicker::make('start_date')->label('Mulai')->required(), DatePicker::make('end_date')->label('Selesai')->required(), + TextInput::make('budget')->label('Estimasi Budget (Rp)')->numeric() + ->helperText('Kosongkan jika tidak ada budget. < Rp500.000: langsung | Rp500.000–2.000.000: approval ketua | > Rp2.000.000: voting'), DateTimePicker::make('executed_at')->label('Waktu Pelaksanaan') ->visible(fn ($record) => $record?->status === 'approved'), Textarea::make('execution_notes')->label('Catatan Pelaksanaan')->rows(3)->columnSpanFull() diff --git a/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php b/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php index 9efdb0a..7a56eca 100644 --- a/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php +++ b/app/Filament/Resources/CashRecords/Schemas/CashRecordForm.php @@ -2,6 +2,7 @@ namespace App\Filament\Resources\CashRecords\Schemas; +use App\Models\Activity; use App\Models\CashCategory; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Placeholder; @@ -27,6 +28,9 @@ class CashRecordForm ->live(), Textarea::make('description')->label('Keterangan')->required()->columnSpanFull(), DatePicker::make('date')->label('Tanggal')->required(), + Select::make('activity_id')->label('Kegiatan Terkait') + ->options(Activity::whereIn('status', ['approved'])->pluck('title', 'id')) + ->searchable()->nullable(), ]); } } diff --git a/app/Models/Activity.php b/app/Models/Activity.php index f4a37f3..96e5c22 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Activity extends Model { protected $fillable = [ - 'title', 'description', 'start_date', 'end_date', + 'title', 'description', 'budget', 'start_date', 'end_date', 'created_by', 'status', 'approved_by', 'approved_at', 'executed_at', 'execution_notes', ]; @@ -44,4 +44,9 @@ class Activity extends Model return $this->belongsToMany(User::class, 'activity_member') ->withPivot('status', 'notes'); } + + public function cashRecords(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(CashRecord::class); + } } diff --git a/app/Models/CashRecord.php b/app/Models/CashRecord.php index c53e270..ea1bceb 100644 --- a/app/Models/CashRecord.php +++ b/app/Models/CashRecord.php @@ -4,11 +4,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use App\Models\Activity; class CashRecord extends Model { protected $fillable = [ - 'amount', 'category_id', 'description', + 'amount', 'category_id', 'activity_id', 'description', 'date', 'created_by', 'verified_by', 'verified_at', ]; @@ -29,6 +30,11 @@ class CashRecord extends Model return $this->belongsTo(CashCategory::class, 'category_id'); } + public function activity(): BelongsTo + { + return $this->belongsTo(Activity::class); + } + public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); diff --git a/app/Observers/ActivityObserver.php b/app/Observers/ActivityObserver.php index c8d6b23..cc9ee93 100644 --- a/app/Observers/ActivityObserver.php +++ b/app/Observers/ActivityObserver.php @@ -4,6 +4,8 @@ namespace App\Observers; use App\Models\Activity; use App\Models\ActivityLog; +use App\Models\Approval; +use App\Models\Vote; use App\Services\NotificationService; use Filament\Notifications\Notification; use Illuminate\Support\Facades\Auth; @@ -42,6 +44,27 @@ class ActivityObserver NotificationService::toRole('ketua', 'Kegiatan Menunggu Persetujuan', "Kegiatan \"{$activity->title}\" diajukan untuk disetujui.", 'warning', route('filament.admin.resources.activities.edit', $activity)); + + // Threshold budget + $budget = $activity->budget; + if ($budget >= 500_000 && $budget <= 2_000_000) { + Approval::create([ + 'model_type' => Activity::class, + 'model_id' => $activity->id, + 'required_approvals' => 1, + 'status' => 'pending', + ]); + } elseif ($budget > 2_000_000) { + Vote::create([ + 'title' => "Persetujuan Budget Kegiatan: {$activity->title}", + 'description' => "Budget kegiatan senilai Rp " . number_format($budget, 0, ',', '.') . " memerlukan persetujuan voting.", + 'type' => 'finance', + 'related_id' => $activity->id, + 'status' => 'open', + 'deadline' => now()->addDays(3), + 'created_by' => Auth::id() ?? $activity->created_by, + ]); + } } if (in_array($new, ['approved', 'rejected']) && $activity->creator) { diff --git a/database/migrations/2026_04_05_161652_add_budget_to_activities_and_activity_id_to_cash_records.php b/database/migrations/2026_04_05_161652_add_budget_to_activities_and_activity_id_to_cash_records.php new file mode 100644 index 0000000..19b03d8 --- /dev/null +++ b/database/migrations/2026_04_05_161652_add_budget_to_activities_and_activity_id_to_cash_records.php @@ -0,0 +1,31 @@ +unsignedBigInteger('budget')->nullable()->after('description'); + }); + + Schema::table('cash_records', function (Blueprint $table) { + $table->foreignId('activity_id')->nullable()->constrained('activities')->nullOnDelete()->after('category_id'); + }); + } + + public function down(): void + { + Schema::table('cash_records', function (Blueprint $table) { + $table->dropForeignIdFor(\App\Models\Activity::class, 'activity_id'); + $table->dropColumn('activity_id'); + }); + + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn('budget'); + }); + } +};