Skip to content

Commit

Permalink
Implement Expense Approval Workflow with Notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
sweep-ai[bot] authored Dec 24, 2024
1 parent 319ec95 commit aa5aeda
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 0 deletions.
128 changes: 128 additions & 0 deletions app/Filament/Resources/ExpenseResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@


<?php

namespace App\Filament\Resources;

use App\Filament\Resources\ExpenseResource\Pages;
use App\Models\Expense;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Tables;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class ExpenseResource extends Resource
{
protected static ?string $model = Expense::class;

protected static ?string $navigationIcon = 'heroicon-o-currency-dollar';
protected static ?string $navigationGroup = 'Finance';

public static function form(Forms\Form $form): Forms\Form
{
return $form
->schema([
Forms\Components\TextInput::make('amount')
->required()
->numeric()
->prefix('$')
->minValue(0.01)
->step(0.01),
Forms\Components\TextInput::make('description')
->required()
->maxLength(255),
Forms\Components\DatePicker::make('date')
->required()
->maxDate(now()),
Forms\Components\Select::make('approval_status')
->options([
'pending' => 'Pending',
'approved' => 'Approved',
'rejected' => 'Rejected',
])
->disabled()
->dehydrated(false),
Forms\Components\Textarea::make('rejection_reason')
->visible(fn (?Model $record) => $record?->approval_status === 'rejected')
->maxLength(1000)
->columnSpanFull(),
]);
}

public static function table(Tables\Table $table): Tables\Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('amount')
->money('USD')
->sortable(),
Tables\Columns\TextColumn::make('description')
->searchable(),
Tables\Columns\TextColumn::make('date')
->date()
->sortable(),
Tables\Columns\TextColumn::make('user.name')
->label('Submitted By')
->searchable(),
Tables\Columns\TextColumn::make('approval_status')
->badge()
->color(fn (string $state): string => match ($state) {
'approved' => 'success',
'rejected' => 'danger',
default => 'warning',
}),
Tables\Columns\TextColumn::make('approver.name')
->label('Approved By')
->visible(fn (Model $record): bool => $record->approved_by !== null),
])
->filters([
Tables\Filters\SelectFilter::make('approval_status')
->options([
'pending' => 'Pending',
'approved' => 'Approved',
'rejected' => 'Rejected',
]),
])
->actions([
Tables\Actions\ViewAction::make(),
Tables\Actions\Action::make('approve')
->action(fn (Expense $record) => $record->approve())
->requiresConfirmation()
->visible(fn (Expense $record) => $record->isPending())
->color('success')
->icon('heroicon-o-check'),
Tables\Actions\Action::make('reject')
->form([
Forms\Components\Textarea::make('reason')
->required()
->maxLength(1000)
->label('Rejection Reason'),
])
->action(fn (Expense $record, array $data) => $record->reject($data['reason']))
->visible(fn (Expense $record) => $record->isPending())
->color('danger')
->icon('heroicon-o-x-mark'),
])
->defaultSort('created_at', 'desc');
}

public static function getRelations(): array
{
return [];
}

public static function getPages(): array
{
return [
'index' => Pages\ListExpenses::route('/'),
'create' => Pages\CreateExpense::route('/create'),
'edit' => Pages\EditExpense::route('/{record}/edit'),
];
}

public static function getNavigationBadge(): ?string
{
return static::getModel()::where('approval_status', 'pending')->count() ?: null;
}
}
67 changes: 67 additions & 0 deletions app/Models/Expense.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Auth;
use App\Notifications\ExpenseApprovalNotification;

class Expense extends Model
{
protected $fillable = [
'amount',
'description',
'date',
'approval_status',
'rejection_reason',
'approved_by',
'approved_at'
];

protected $casts = [
'date' => 'date',
'approved_at' => 'datetime',
'amount' => 'decimal:2'
];

public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

public function approver(): BelongsTo
{
return $this->belongsTo(User::class, 'approved_by');
}

public function approve()
{
$this->update([
'approval_status' => 'approved',
'approved_by' => Auth::id(),
'approved_at' => now(),
]);

$this->user->notify(new ExpenseApprovalNotification($this, 'approved'));
}

public function reject($reason)
{
$this->update([
'approval_status' => 'rejected',
'rejection_reason' => $reason,
'approved_by' => Auth::id(),
'approved_at' => now(),
]);

$this->user->notify(new ExpenseApprovalNotification($this, 'rejected'));
}

public function isPending(): bool
{
return $this->approval_status === 'pending';
}
}
56 changes: 56 additions & 0 deletions app/Notifications/ExpenseApprovalNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@


<?php

namespace App\Notifications;

use App\Models\Expense;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;

class ExpenseApprovalNotification extends Notification implements ShouldQueue
{
use Queueable;

public function __construct(
protected Expense $expense,
protected string $status
) {}

public function via($notifiable): array
{
return ['mail', 'database'];
}

public function toMail($notifiable): MailMessage
{
$message = $this->status === 'approved'
? 'Your expense has been approved.'
: 'Your expense has been rejected.';

return (new MailMessage)
->subject("Expense {$this->status}")
->greeting("Hello {$notifiable->name}")
->line($message)
->line("Amount: {$this->expense->amount}")
->line("Description: {$this->expense->description}")
->when($this->status === 'rejected', function($mail) {
return $mail->line("Reason: {$this->expense->rejection_reason}");
})
->line("Date: {$this->expense->date->format('Y-m-d')}");
}

public function toArray($notifiable): array
{
return [
'expense_id' => $this->expense->id,
'status' => $this->status,
'amount' => $this->expense->amount,
'reason' => $this->expense->rejection_reason,
'approved_by' => $this->expense->approved_by,
'approved_at' => $this->expense->approved_at,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::table('expenses', function (Blueprint $table) {
$table->enum('approval_status', ['pending', 'approved', 'rejected'])->default('pending');
$table->text('rejection_reason')->nullable();
$table->foreignId('approved_by')->nullable()->constrained('users');
$table->timestamp('approved_at')->nullable();
});
}

public function down()
{
Schema::table('expenses', function (Blueprint $table) {
$table->dropColumn(['approval_status', 'rejection_reason', 'approved_by', 'approved_at']);
});
}
};

0 comments on commit aa5aeda

Please sign in to comment.