diff --git a/app/Filament/App/Resources/InvoiceResource.php b/app/Filament/App/Resources/InvoiceResource.php index ab6da993..9ada9625 100644 --- a/app/Filament/App/Resources/InvoiceResource.php +++ b/app/Filament/App/Resources/InvoiceResource.php @@ -12,83 +12,66 @@ use Filament\Tables\Columns\TextColumn; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\DatePicker; +use Filament\Forms\Components\Textarea; use Illuminate\Database\Eloquent\Builder; use Filament\Forms\Components\BelongsToSelect; -use Illuminate\Database\Eloquent\SoftDeletingScope; +use Filament\Tables\Actions\Action; use App\Filament\App\Resources\InvoiceResource\Pages; -use App\Filament\App\Resources\InvoiceResource\RelationManagers; class InvoiceResource extends Resource { protected static ?string $model = Invoice::class; - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + protected static ?string $navigationIcon = 'heroicon-o-document-text'; public static function form(Form $form): Form { return $form ->schema([ - Grid::make(2)->schema([ - BelongsToSelect::make('customer_id') - ->relationship('customer', 'customer_name') - ->label('Customer') - ->columnSpan(1), - DatePicker::make('invoice_date') - ->columnSpan(1), - DatePicker::make('due_date') - ->columnSpan(1), - TextInput::make('total_amount') - ->numeric() - ->reactive() - ->afterStateUpdated(function ($state, callable $set, $get) { - if ($get('tax_rate_id')) { - $taxRate = TaxRate::find($get('tax_rate_id')); - $taxAmount = $state * ($taxRate->rate / 100); - $set('tax_amount', $taxAmount); - } - }) - ->columnSpan(1), - BelongsToSelect::make('tax_rate_id') - ->relationship('taxRate', 'name') - ->label('Tax Rate') - ->reactive() - ->afterStateUpdated(function ($state, callable $set, $get) { - if ($state && $get('total_amount')) { - $taxRate = TaxRate::find($state); - $taxAmount = $get('total_amount') * ($taxRate->rate / 100); - $set('tax_amount', $taxAmount); - } - }) - ->columnSpan(1), - TextInput::make('tax_amount') - ->numeric() - ->disabled() - ->label('Tax Amount') - ->columnSpan(1), - TextInput::make('late_fee_percentage') - ->numeric() - ->label('Late Fee (%)') - ->default(0) - ->columnSpan(1), - TextInput::make('grace_period_days') - ->numeric() - ->label('Grace Period (Days)') - ->default(0) - ->columnSpan(1), - TextInput::make('late_fee_amount') - ->disabled() - ->numeric() - ->label('Late Fee Amount') - ->columnSpan(1), - Select::make('payment_status') - ->options([ - 'pending' => 'Pending', - 'paid' => 'Paid', - 'failed' => 'Failed', - ]) - ->label('Payment Status') - ->columnSpan(1), - ]), + BelongsToSelect::make('customer_id') + ->relationship('customer', 'customer_name') + ->required() + ->searchable(), + TextInput::make('invoice_number') + ->disabled() + ->dehydrated(false) + ->visible(fn ($record) => $record !== null), + DatePicker::make('invoice_date') + ->required(), + DatePicker::make('due_date'), + TextInput::make('total_amount') + ->numeric() + ->required() + ->reactive() + ->afterStateUpdated(function ($state, callable $set, $get) { + if ($get('tax_rate_id')) { + $taxRate = \App\Models\TaxRate::find($get('tax_rate_id')); + $taxAmount = $state * ($taxRate->rate / 100); + $set('tax_amount', $taxAmount); + } + }), + BelongsToSelect::make('tax_rate_id') + ->relationship('taxRate', 'name') + ->reactive() + ->afterStateUpdated(function ($state, callable $set, $get) { + if ($state && $get('total_amount')) { + $taxRate = \App\Models\TaxRate::find($state); + $taxAmount = $get('total_amount') * ($taxRate->rate / 100); + $set('tax_amount', $taxAmount); + } + }), + TextInput::make('tax_amount') + ->numeric() + ->disabled(), + Select::make('payment_status') + ->options([ + 'pending' => 'Pending', + 'paid' => 'Paid', + 'failed' => 'Failed', + ]) + ->required(), + Textarea::make('notes') + ->rows(3), ]); } @@ -96,38 +79,40 @@ public static function table(Table $table): Table { return $table ->columns([ - TextColumn::make('customer_id') - ->label('Customer') + TextColumn::make('invoice_number') ->searchable() ->sortable(), - TextColumn::make('invoice_date') - ->label('Invoice Date') + TextColumn::make('customer.customer_name') ->searchable() ->sortable(), + TextColumn::make('invoice_date') + ->date() + ->sortable(), TextColumn::make('total_amount') - ->label('Total Amount') - ->searchable() + ->money('USD') ->sortable(), - TextColumn::make('payment_status'), - ]) - ->filters([ - // + TextColumn::make('payment_status') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'paid' => 'success', + 'failed' => 'danger', + default => 'warning', + }), ]) ->actions([ Tables\Actions\EditAction::make(), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DeleteBulkAction::make(), - ]), + Action::make('download') + ->icon('heroicon-o-document-download') + ->action(fn (Invoice $record) => response()->streamDownload( + fn () => print($record->generatePDF()), + "invoice_{$record->invoice_number}.pdf" + )), ]); } public static function getRelations(): array { - return [ - // - ]; + return []; } public static function getPages(): array diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index ee9d798d..a530f5fb 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -2,9 +2,9 @@ namespace App\Models; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Barryvdh\DomPDF\Facade\Pdf; class Invoice extends Model { @@ -14,23 +14,20 @@ class Invoice extends Model protected $fillable = [ "customer_id", + "invoice_number", "invoice_date", "due_date", "total_amount", "tax_amount", "tax_rate_id", "payment_status", - "late_fee_percentage", - "grace_period_days", - "late_fee_amount" + "notes" ]; protected $casts = [ 'total_amount' => 'decimal:2', 'tax_amount' => 'decimal:2', - 'late_fee_amount' => 'decimal:2', - 'late_fee_percentage' => 'decimal:2', - 'invoice_date' => 'datetime', + 'invoice_date' => 'date', 'due_date' => 'date', ]; @@ -60,48 +57,37 @@ public function calculateTax() return $taxAmount; } - public function calculateLateFee() - { - if ($this->payment_status === 'paid' || !$this->due_date) { - return 0; - } - - $dueDate = Carbon::parse($this->due_date)->addDays($this->grace_period_days); - $today = Carbon::now(); - - if ($today->lte($dueDate)) { - return 0; - } - - $lateFee = $this->total_amount * ($this->late_fee_percentage / 100); - $this->late_fee_amount = $lateFee; - $this->save(); - - return $lateFee; - } - - public function isOverdue() + public function getTotalWithTax() { - if (!$this->due_date || $this->payment_status === 'paid') { - return false; - } - - return Carbon::now()->gt(Carbon::parse($this->due_date)->addDays($this->grace_period_days)); + return $this->total_amount + $this->tax_amount; } - public function getTotalWithTax() + public function calculateTotalFromTimeEntries() { - return $this->total_amount + $this->tax_amount; + $this->total_amount = $this->timeEntries->sum('total_amount'); + return $this->total_amount; } - public function getTotalWithTaxAndLateFees() + public function generatePDF() { - return $this->getTotalWithTax() + $this->late_fee_amount; + $data = [ + 'invoice' => $this, + 'customer' => $this->customer, + 'tax_rate' => $this->taxRate, + ]; + + $pdf = PDF::loadView('invoices.template', $data); + return $pdf->download('invoice_' . $this->invoice_number . '.pdf'); } - public function calculateTotalFromTimeEntries() + protected static function boot() { - $this->total_amount = $this->timeEntries->sum('total_amount'); - return $this->total_amount; + parent::boot(); + + static::creating(function ($invoice) { + if (empty($invoice->invoice_number)) { + $invoice->invoice_number = 'INV-' . str_pad(static::max('invoice_id') + 1, 6, '0', STR_PAD_LEFT); + } + }); } } diff --git a/database/migrations/2024_02_20_create_invoices_table.php b/database/migrations/2024_02_20_create_invoices_table.php new file mode 100644 index 00000000..ec12e1cb --- /dev/null +++ b/database/migrations/2024_02_20_create_invoices_table.php @@ -0,0 +1,32 @@ + + +id('invoice_id'); + $table->foreignId('customer_id')->constrained('customers'); + $table->string('invoice_number')->unique(); + $table->date('invoice_date'); + $table->date('due_date')->nullable(); + $table->decimal('total_amount', 10, 2)->default(0); + $table->decimal('tax_amount', 10, 2)->default(0); + $table->foreignId('tax_rate_id')->nullable()->constrained('tax_rates'); + $table->enum('payment_status', ['pending', 'paid', 'failed'])->default('pending'); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('invoices'); + } +}; \ No newline at end of file diff --git a/resources/views/invoices/template.blade.php b/resources/views/invoices/template.blade.php new file mode 100644 index 00000000..2d6c259f --- /dev/null +++ b/resources/views/invoices/template.blade.php @@ -0,0 +1,49 @@ + + + + +
+ +Invoice Number: {{ $invoice->invoice_number }}
+Date: {{ $invoice->invoice_date->format('Y-m-d') }}
+Due Date: {{ $invoice->due_date ? $invoice->due_date->format('Y-m-d') : 'N/A' }}
+{{ $customer->customer_name }}
+{{ $customer->address ?? '' }}
+{{ $customer->email }}
+Subtotal: ${{ number_format($invoice->total_amount, 2) }}
+Tax Rate: {{ $tax_rate->rate ?? 0 }}%
+Tax Amount: ${{ number_format($invoice->tax_amount, 2) }}
+Total Amount: ${{ number_format($invoice->getTotalWithTax(), 2) }}
+{{ $invoice->notes }}
+