'integer', 'source_type' => BudgetSourceType::class, 'start_date' => 'date', 'end_date' => 'date', 'status' => BudgetStatus::class, 'interval_type' => BudgetIntervalType::class, 'approved_at' => 'datetime', 'closed_at' => 'datetime', ]; public function sourceBudget(): BelongsTo { return $this->belongsTo(self::class, 'source_budget_id'); } public function derivedBudgets(): HasMany { return $this->hasMany(self::class, 'source_budget_id'); } public function approvedBy(): BelongsTo { return $this->belongsTo(User::class, 'approved_by_id'); } public function budgetItems(): HasMany { return $this->hasMany(BudgetItem::class); } public function allocations(): HasManyThrough { return $this->hasManyThrough(BudgetAllocation::class, BudgetItem::class); } /** * Get all periods for this budget in chronological order. */ public function getPeriods(): array { return $this->budgetItems() ->with('allocations') ->get() ->flatMap(fn ($item) => $item->allocations) ->sortBy('start_date') ->pluck('period') ->unique() ->values() ->toArray(); } public function isDraft(): bool { return $this->status === BudgetStatus::Draft; } public function isActive(): bool { return $this->status === BudgetStatus::Active; } public function isClosed(): bool { return $this->status === BudgetStatus::Closed; } public function wasApproved(): bool { return $this->approved_at !== null; } public function wasClosed(): bool { return $this->closed_at !== null; } public function canBeApproved(): bool { return $this->isDraft() && ! $this->wasApproved(); } public function canBeClosed(): bool { return $this->isActive() && ! $this->wasClosed(); } public function hasItems(): bool { return $this->budgetItems()->exists(); } public function hasAllocations(): bool { return $this->allocations()->exists(); } public function scopeDraft(Builder $query): Builder { return $query->where('status', BudgetStatus::Draft); } public function scopeActive(Builder $query): Builder { return $query->where('status', BudgetStatus::Active); } public function scopeClosed(Builder $query): Builder { return $query->where('status', BudgetStatus::Closed); } public function scopeCurrentlyActive(Builder $query): Builder { return $query->active() ->where('start_date', '<=', now()) ->where('end_date', '>=', now()); } protected function isCurrentlyInPeriod(): Attribute { return Attribute::get(function () { return now()->between($this->start_date, $this->end_date); }); } /** * Approve a draft budget */ public function approveDraft(?Carbon $approvedAt = null): void { if (! $this->canBeApproved()) { throw new \RuntimeException('Budget cannot be approved.'); } $approvedAt ??= now(); $this->update([ 'status' => BudgetStatus::Active, 'approved_at' => $approvedAt, ]); } /** * Close an active budget */ public function close(?Carbon $closedAt = null): void { if (! $this->canBeClosed()) { throw new \RuntimeException('Budget cannot be closed.'); } $closedAt ??= now(); $this->update([ 'status' => BudgetStatus::Closed, 'closed_at' => $closedAt, ]); } /** * Reopen a closed budget */ public function reopen(): void { if (! $this->isClosed()) { throw new \RuntimeException('Only closed budgets can be reopened.'); } $this->update([ 'status' => BudgetStatus::Active, 'closed_at' => null, ]); } /** * Get Action for approving a draft budget */ public static function getApproveDraftAction(string $action = Action::class): MountableAction { return $action::make('approveDraft') ->label('Approve') ->icon('heroicon-m-check-circle') ->visible(function (self $record) { return $record->canBeApproved(); }) ->databaseTransaction() ->successNotificationTitle('Budget approved') ->action(function (self $record, MountableAction $action) { $record->approveDraft(); $action->success(); }); } /** * Get Action for closing an active budget */ public static function getCloseAction(string $action = Action::class): MountableAction { return $action::make('close') ->label('Close') ->icon('heroicon-m-lock-closed') ->color('warning') ->visible(function (self $record) { return $record->canBeClosed(); }) ->requiresConfirmation() ->databaseTransaction() ->successNotificationTitle('Budget closed') ->action(function (self $record, MountableAction $action) { $record->close(); $action->success(); }); } /** * Get Action for reopening a closed budget */ public static function getReopenAction(string $action = Action::class): MountableAction { return $action::make('reopen') ->label('Reopen') ->icon('heroicon-m-lock-open') ->visible(function (self $record) { return $record->isClosed(); }) ->requiresConfirmation() ->databaseTransaction() ->successNotificationTitle('Budget reopened') ->action(function (self $record, MountableAction $action) { $record->reopen(); $action->success(); }); } /** * Get Action for duplicating a budget */ public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction { return $action::make() ->excludeAttributes([ 'status', 'approved_at', 'closed_at', 'created_by', 'updated_by', 'created_at', 'updated_at', ]) ->modal(false) ->beforeReplicaSaved(function (self $original, self $replica) { $replica->status = BudgetStatus::Draft; $replica->name = $replica->name . ' (Copy)'; }) ->databaseTransaction() ->after(function (self $original, self $replica) { // Clone budget items and their allocations $original->budgetItems->each(function (BudgetItem $item) use ($replica) { $newItem = $item->replicate([ 'budget_id', 'created_by', 'updated_by', 'created_at', 'updated_at', ]); $newItem->budget_id = $replica->id; $newItem->save(); // Clone the allocations for this budget item $item->allocations->each(function (BudgetAllocation $allocation) use ($newItem) { $newAllocation = $allocation->replicate([ 'budget_item_id', 'created_at', 'updated_at', ]); $newAllocation->budget_item_id = $newItem->id; $newAllocation->save(); }); }); }) ->successRedirectUrl(static function (self $replica) { return BudgetResource::getUrl('edit', ['record' => $replica]); }); } }