Andrew Wallo 10 mesi fa
parent
commit
781eb1b7c9
26 ha cambiato i file con 1283 aggiunte e 197 eliminazioni
  1. 8
    0
      app/Enums/Accounting/BillStatus.php
  2. 263
    3
      app/Filament/Company/Resources/Purchases/BillResource.php
  3. 36
    0
      app/Filament/Company/Resources/Purchases/BillResource/Pages/ListBills.php
  4. 69
    0
      app/Filament/Company/Resources/Purchases/BillResource/Widgets/BillOverview.php
  5. 1
    2
      app/Filament/Company/Resources/Sales/InvoiceResource.php
  6. 2
    12
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ListInvoices.php
  7. 20
    27
      app/Filament/Company/Resources/Sales/InvoiceResource/Widgets/InvoiceOverview.php
  8. 12
    0
      app/Filament/Widgets/EnhancedStatsOverviewWidget.php
  9. 47
    0
      app/Filament/Widgets/EnhancedStatsOverviewWidget/EnhancedStat.php
  10. 5
    0
      app/Models/Accounting/Account.php
  11. 169
    1
      app/Models/Accounting/Bill.php
  12. 26
    1
      app/Models/Accounting/Invoice.php
  13. 40
    0
      app/Observers/BillObserver.php
  14. 0
    37
      app/Observers/InvoiceObserver.php
  15. 47
    10
      app/Observers/TransactionObserver.php
  16. 0
    4
      app/Providers/MacroServiceProvider.php
  17. 8
    2
      app/Utilities/Currency/CurrencyConverter.php
  18. 6
    6
      composer.lock
  19. 159
    2
      database/factories/Accounting/BillFactory.php
  20. 46
    9
      database/factories/Accounting/DocumentLineItemFactory.php
  21. 2
    2
      database/factories/Accounting/InvoiceFactory.php
  22. 46
    0
      database/factories/CompanyFactory.php
  23. 2
    0
      database/migrations/2024_11_27_221657_create_bills_table.php
  24. 2
    1
      database/seeders/DatabaseSeeder.php
  25. 93
    78
      package-lock.json
  26. 174
    0
      resources/views/filament/widgets/enhanced-stats-overview-widget/enhanced-stat.blade.php

+ 8
- 0
app/Enums/Accounting/BillStatus.php Vedi File

27
             self::Void => 'gray',
27
             self::Void => 'gray',
28
         };
28
         };
29
     }
29
     }
30
+
31
+    public static function canBeOverdue(): array
32
+    {
33
+        return [
34
+            self::Partial,
35
+            self::Unpaid,
36
+        ];
37
+    }
30
 }
38
 }

+ 263
- 3
app/Filament/Company/Resources/Purchases/BillResource.php Vedi File

2
 
2
 
3
 namespace App\Filament\Company\Resources\Purchases;
3
 namespace App\Filament\Company\Resources\Purchases;
4
 
4
 
5
+use App\Enums\Accounting\BillStatus;
6
+use App\Enums\Accounting\PaymentMethod;
5
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
7
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
8
+use App\Filament\Tables\Actions\ReplicateBulkAction;
9
+use App\Filament\Tables\Filters\DateRangeFilter;
6
 use App\Models\Accounting\Adjustment;
10
 use App\Models\Accounting\Adjustment;
7
 use App\Models\Accounting\Bill;
11
 use App\Models\Accounting\Bill;
8
 use App\Models\Accounting\DocumentLineItem;
12
 use App\Models\Accounting\DocumentLineItem;
13
+use App\Models\Banking\BankAccount;
9
 use App\Models\Common\Offering;
14
 use App\Models\Common\Offering;
10
-use App\Utilities\Currency\CurrencyAccessor;
15
+use App\Utilities\Currency\CurrencyConverter;
11
 use Awcodes\TableRepeater\Components\TableRepeater;
16
 use Awcodes\TableRepeater\Components\TableRepeater;
12
 use Awcodes\TableRepeater\Header;
17
 use Awcodes\TableRepeater\Header;
13
 use Carbon\CarbonInterface;
18
 use Carbon\CarbonInterface;
19
+use Closure;
14
 use Filament\Forms;
20
 use Filament\Forms;
15
 use Filament\Forms\Form;
21
 use Filament\Forms\Form;
22
+use Filament\Notifications\Notification;
16
 use Filament\Resources\Resource;
23
 use Filament\Resources\Resource;
24
+use Filament\Support\Enums\Alignment;
25
+use Filament\Support\Enums\MaxWidth;
17
 use Filament\Tables;
26
 use Filament\Tables;
18
 use Filament\Tables\Table;
27
 use Filament\Tables\Table;
28
+use Illuminate\Database\Eloquent\Builder;
29
+use Illuminate\Database\Eloquent\Collection;
19
 use Illuminate\Database\Eloquent\Model;
30
 use Illuminate\Database\Eloquent\Model;
20
 use Illuminate\Support\Carbon;
31
 use Illuminate\Support\Carbon;
21
 use Illuminate\Support\Facades\Auth;
32
 use Illuminate\Support\Facades\Auth;
180
                                     ->live()
191
                                     ->live()
181
                                     ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
192
                                     ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
182
                                         $offeringId = $state;
193
                                         $offeringId = $state;
183
-                                        $offeringRecord = Offering::with('purchaseTaxes')->find($offeringId);
194
+                                        $offeringRecord = Offering::with(['purchaseTaxes', 'purchaseDiscounts'])->find($offeringId);
184
 
195
 
185
                                         if ($offeringRecord) {
196
                                         if ($offeringRecord) {
186
                                             $set('description', $offeringRecord->description);
197
                                             $set('description', $offeringRecord->description);
239
                                         // Final total
250
                                         // Final total
240
                                         $total = $subtotal + ($taxAmount - $discountAmount);
251
                                         $total = $subtotal + ($taxAmount - $discountAmount);
241
 
252
 
242
-                                        return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
253
+                                        return CurrencyConverter::formatToMoney($total);
243
                                     }),
254
                                     }),
244
                             ]),
255
                             ]),
245
                         Forms\Components\Grid::make(6)
256
                         Forms\Components\Grid::make(6)
295
                 Tables\Columns\TextColumn::make('amount_due')
306
                 Tables\Columns\TextColumn::make('amount_due')
296
                     ->label('Amount Due')
307
                     ->label('Amount Due')
297
                     ->currency(),
308
                     ->currency(),
309
+            ])
310
+            ->filters([
311
+                Tables\Filters\SelectFilter::make('vendor')
312
+                    ->relationship('vendor', 'name')
313
+                    ->searchable()
314
+                    ->preload(),
315
+                Tables\Filters\SelectFilter::make('status')
316
+                    ->options(BillStatus::class)
317
+                    ->native(false),
318
+                Tables\Filters\TernaryFilter::make('has_payments')
319
+                    ->label('Has Payments')
320
+                    ->queries(
321
+                        true: fn (Builder $query) => $query->whereHas('payments'),
322
+                        false: fn (Builder $query) => $query->whereDoesntHave('payments'),
323
+                    ),
324
+                DateRangeFilter::make('date')
325
+                    ->fromLabel('From Date')
326
+                    ->untilLabel('To Date')
327
+                    ->indicatorLabel('Date'),
328
+                DateRangeFilter::make('due_date')
329
+                    ->fromLabel('From Due Date')
330
+                    ->untilLabel('To Due Date')
331
+                    ->indicatorLabel('Due'),
332
+            ])
333
+            ->actions([
334
+                Tables\Actions\ActionGroup::make([
335
+                    Tables\Actions\EditAction::make(),
336
+                    Tables\Actions\ViewAction::make(),
337
+                    Tables\Actions\DeleteAction::make(),
338
+                    Bill::getReplicateAction(Tables\Actions\ReplicateAction::class),
339
+                    Tables\Actions\Action::make('recordPayment')
340
+                        ->label('Record Payment')
341
+                        ->stickyModalHeader()
342
+                        ->stickyModalFooter()
343
+                        ->modalFooterActionsAlignment(Alignment::End)
344
+                        ->modalWidth(MaxWidth::TwoExtraLarge)
345
+                        ->icon('heroicon-o-credit-card')
346
+                        ->visible(function (Bill $record) {
347
+                            return $record->canRecordPayment();
348
+                        })
349
+                        ->mountUsing(function (Bill $record, Form $form) {
350
+                            $form->fill([
351
+                                'posted_at' => now(),
352
+                                'amount' => $record->amount_due,
353
+                            ]);
354
+                        })
355
+                        ->databaseTransaction()
356
+                        ->successNotificationTitle('Payment Recorded')
357
+                        ->form([
358
+                            Forms\Components\DatePicker::make('posted_at')
359
+                                ->label('Date'),
360
+                            Forms\Components\TextInput::make('amount')
361
+                                ->label('Amount')
362
+                                ->required()
363
+                                ->money()
364
+                                ->live(onBlur: true)
365
+                                ->helperText(function (Bill $record, $state) {
366
+                                    if (! CurrencyConverter::isValidAmount($state)) {
367
+                                        return null;
368
+                                    }
369
+
370
+                                    $amountDue = $record->getRawOriginal('amount_due');
371
+                                    $amount = CurrencyConverter::convertToCents($state);
372
+
373
+                                    if ($amount <= 0) {
374
+                                        return 'Please enter a valid positive amount';
375
+                                    }
376
+
377
+                                    $newAmountDue = $amountDue - $amount;
378
+
379
+                                    return match (true) {
380
+                                        $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue),
381
+                                        $newAmountDue === 0 => 'Bill will be fully paid',
382
+                                        default => 'Amount exceeds bill total by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue)),
383
+                                    };
384
+                                })
385
+                                ->rules([
386
+                                    static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
387
+                                        if (! CurrencyConverter::isValidAmount($value)) {
388
+                                            $fail('Please enter a valid amount');
389
+                                        }
390
+                                    },
391
+                                ]),
392
+                            Forms\Components\Select::make('payment_method')
393
+                                ->label('Payment Method')
394
+                                ->required()
395
+                                ->options(PaymentMethod::class),
396
+                            Forms\Components\Select::make('bank_account_id')
397
+                                ->label('Account')
398
+                                ->required()
399
+                                ->options(BankAccount::query()
400
+                                    ->get()
401
+                                    ->pluck('account.name', 'id'))
402
+                                ->searchable(),
403
+                            Forms\Components\Textarea::make('notes')
404
+                                ->label('Notes'),
405
+                        ])
406
+                        ->action(function (Bill $record, Tables\Actions\Action $action, array $data) {
407
+                            $record->recordPayment($data);
408
+
409
+                            $action->success();
410
+                        }),
411
+                ]),
412
+            ])
413
+            ->bulkActions([
414
+                Tables\Actions\BulkActionGroup::make([
415
+                    Tables\Actions\DeleteBulkAction::make(),
416
+                    ReplicateBulkAction::make()
417
+                        ->label('Replicate')
418
+                        ->modalWidth(MaxWidth::Large)
419
+                        ->modalDescription('Replicating bills will also replicate their line items. Are you sure you want to proceed?')
420
+                        ->successNotificationTitle('Bills Replicated Successfully')
421
+                        ->failureNotificationTitle('Failed to Replicate Bills')
422
+                        ->databaseTransaction()
423
+                        ->deselectRecordsAfterCompletion()
424
+                        ->excludeAttributes([
425
+                            'status',
426
+                            'amount_paid',
427
+                            'amount_due',
428
+                            'created_by',
429
+                            'updated_by',
430
+                            'created_at',
431
+                            'updated_at',
432
+                            'bill_number',
433
+                            'date',
434
+                            'due_date',
435
+                            'paid_at',
436
+                        ])
437
+                        ->beforeReplicaSaved(function (Bill $replica) {
438
+                            $replica->status = BillStatus::Unpaid;
439
+                            $replica->bill_number = Bill::getNextDocumentNumber();
440
+                            $replica->date = now();
441
+                            $replica->due_date = now()->addDays($replica->company->defaultBill->payment_terms->getDays());
442
+                        })
443
+                        ->withReplicatedRelationships(['lineItems'])
444
+                        ->withExcludedRelationshipAttributes('lineItems', [
445
+                            'subtotal',
446
+                            'total',
447
+                            'created_by',
448
+                            'updated_by',
449
+                            'created_at',
450
+                            'updated_at',
451
+                        ]),
452
+                    Tables\Actions\BulkAction::make('recordPayments')
453
+                        ->label('Record Payments')
454
+                        ->icon('heroicon-o-credit-card')
455
+                        ->stickyModalHeader()
456
+                        ->stickyModalFooter()
457
+                        ->modalFooterActionsAlignment(Alignment::End)
458
+                        ->modalWidth(MaxWidth::TwoExtraLarge)
459
+                        ->databaseTransaction()
460
+                        ->successNotificationTitle('Payments Recorded')
461
+                        ->failureNotificationTitle('Failed to Record Payments')
462
+                        ->deselectRecordsAfterCompletion()
463
+                        ->beforeFormFilled(function (Collection $records, Tables\Actions\BulkAction $action) {
464
+                            $cantRecordPayments = $records->contains(fn (Bill $bill) => ! $bill->canRecordPayment());
465
+
466
+                            if ($cantRecordPayments) {
467
+                                Notification::make()
468
+                                    ->title('Payment Recording Failed')
469
+                                    ->body('Bills that are either paid or voided cannot be processed through bulk payments. Please adjust your selection and try again.')
470
+                                    ->persistent()
471
+                                    ->danger()
472
+                                    ->send();
473
+
474
+                                $action->cancel(true);
475
+                            }
476
+                        })
477
+                        ->mountUsing(function (Collection $records, Form $form) {
478
+                            $totalAmountDue = $records->sum(fn (Bill $bill) => $bill->getRawOriginal('amount_due'));
479
+
480
+                            $form->fill([
481
+                                'posted_at' => now(),
482
+                                'amount' => CurrencyConverter::convertCentsToFormatSimple($totalAmountDue),
483
+                            ]);
484
+                        })
485
+                        ->form([
486
+                            Forms\Components\DatePicker::make('posted_at')
487
+                                ->label('Date'),
488
+                            Forms\Components\TextInput::make('amount')
489
+                                ->label('Amount')
490
+                                ->required()
491
+                                ->money()
492
+                                ->rules([
493
+                                    static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
494
+                                        if (! CurrencyConverter::isValidAmount($value)) {
495
+                                            $fail('Please enter a valid amount');
496
+                                        }
497
+                                    },
498
+                                ]),
499
+                            Forms\Components\Select::make('payment_method')
500
+                                ->label('Payment Method')
501
+                                ->required()
502
+                                ->options(PaymentMethod::class),
503
+                            Forms\Components\Select::make('bank_account_id')
504
+                                ->label('Account')
505
+                                ->required()
506
+                                ->options(BankAccount::query()
507
+                                    ->get()
508
+                                    ->pluck('account.name', 'id'))
509
+                                ->searchable(),
510
+                            Forms\Components\Textarea::make('notes')
511
+                                ->label('Notes'),
512
+                        ])
513
+                        ->before(function (Collection $records, Tables\Actions\BulkAction $action, array $data) {
514
+                            $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
515
+                            $totalAmountDue = $records->sum(fn (Bill $bill) => $bill->getRawOriginal('amount_due'));
516
+
517
+                            if ($totalPaymentAmount > $totalAmountDue) {
518
+                                $formattedTotalAmountDue = CurrencyConverter::formatCentsToMoney($totalAmountDue);
519
+
520
+                                Notification::make()
521
+                                    ->title('Excess Payment Amount')
522
+                                    ->body("The payment amount exceeds the total amount due of {$formattedTotalAmountDue}. Please adjust the payment amount and try again.")
523
+                                    ->persistent()
524
+                                    ->warning()
525
+                                    ->send();
526
+
527
+                                $action->halt(true);
528
+                            }
529
+                        })
530
+                        ->action(function (Collection $records, Tables\Actions\BulkAction $action, array $data) {
531
+                            $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
532
+                            $remainingAmount = $totalPaymentAmount;
533
+
534
+                            $records->each(function (Bill $record) use (&$remainingAmount, $data) {
535
+                                $amountDue = $record->getRawOriginal('amount_due');
536
+
537
+                                if ($amountDue <= 0 || $remainingAmount <= 0) {
538
+                                    return;
539
+                                }
540
+
541
+                                $paymentAmount = min($amountDue, $remainingAmount);
542
+                                $data['amount'] = CurrencyConverter::convertCentsToFormatSimple($paymentAmount);
543
+
544
+                                $record->recordPayment($data);
545
+                                $remainingAmount -= $paymentAmount;
546
+                            });
547
+
548
+                            $action->success();
549
+                        }),
550
+                ]),
298
             ]);
551
             ]);
299
     }
552
     }
300
 
553
 
313
             'edit' => Pages\EditBill::route('/{record}/edit'),
566
             'edit' => Pages\EditBill::route('/{record}/edit'),
314
         ];
567
         ];
315
     }
568
     }
569
+
570
+    public static function getWidgets(): array
571
+    {
572
+        return [
573
+            BillResource\Widgets\BillOverview::class,
574
+        ];
575
+    }
316
 }
576
 }

+ 36
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/ListBills.php Vedi File

2
 
2
 
3
 namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
3
 namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4
 
4
 
5
+use App\Enums\Accounting\BillStatus;
5
 use App\Filament\Company\Resources\Purchases\BillResource;
6
 use App\Filament\Company\Resources\Purchases\BillResource;
7
+use App\Models\Accounting\Bill;
6
 use Filament\Actions;
8
 use Filament\Actions;
9
+use Filament\Pages\Concerns\ExposesTableToWidgets;
10
+use Filament\Resources\Components\Tab;
7
 use Filament\Resources\Pages\ListRecords;
11
 use Filament\Resources\Pages\ListRecords;
8
 use Filament\Support\Enums\MaxWidth;
12
 use Filament\Support\Enums\MaxWidth;
13
+use Illuminate\Database\Eloquent\Builder;
9
 
14
 
10
 class ListBills extends ListRecords
15
 class ListBills extends ListRecords
11
 {
16
 {
17
+    use ExposesTableToWidgets;
18
+
12
     protected static string $resource = BillResource::class;
19
     protected static string $resource = BillResource::class;
13
 
20
 
14
     protected function getHeaderActions(): array
21
     protected function getHeaderActions(): array
18
         ];
25
         ];
19
     }
26
     }
20
 
27
 
28
+    protected function getHeaderWidgets(): array
29
+    {
30
+        return [
31
+            BillResource\Widgets\BillOverview::class,
32
+        ];
33
+    }
34
+
21
     public function getMaxContentWidth(): MaxWidth | string | null
35
     public function getMaxContentWidth(): MaxWidth | string | null
22
     {
36
     {
23
         return 'max-w-8xl';
37
         return 'max-w-8xl';
24
     }
38
     }
39
+
40
+    public function getTabs(): array
41
+    {
42
+        return [
43
+            'all' => Tab::make()
44
+                ->label('All'),
45
+
46
+            'outstanding' => Tab::make()
47
+                ->label('Outstanding')
48
+                ->modifyQueryUsing(function (Builder $query) {
49
+                    $query->outstanding();
50
+                })
51
+                ->badge(Bill::outstanding()->count()),
52
+
53
+            'paid' => Tab::make()
54
+                ->label('Paid')
55
+                ->modifyQueryUsing(function (Builder $query) {
56
+                    $query->where('status', BillStatus::Paid);
57
+                })
58
+                ->badge(Bill::where('status', BillStatus::Paid)->count()),
59
+        ];
60
+    }
25
 }
61
 }

+ 69
- 0
app/Filament/Company/Resources/Purchases/BillResource/Widgets/BillOverview.php Vedi File

1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Purchases\BillResource\Widgets;
4
+
5
+use App\Enums\Accounting\BillStatus;
6
+use App\Filament\Company\Resources\Purchases\BillResource\Pages\ListBills;
7
+use App\Filament\Widgets\EnhancedStatsOverviewWidget;
8
+use App\Utilities\Currency\CurrencyAccessor;
9
+use App\Utilities\Currency\CurrencyConverter;
10
+use Filament\Widgets\Concerns\InteractsWithPageTable;
11
+use Illuminate\Support\Number;
12
+
13
+class BillOverview extends EnhancedStatsOverviewWidget
14
+{
15
+    use InteractsWithPageTable;
16
+
17
+    protected function getTablePage(): string
18
+    {
19
+        return ListBills::class;
20
+    }
21
+
22
+    protected function getStats(): array
23
+    {
24
+        $unpaidBills = $this->getPageTableQuery()
25
+            ->whereIn('status', [BillStatus::Unpaid, BillStatus::Partial, BillStatus::Overdue]);
26
+
27
+        $amountToPay = $unpaidBills->sum('amount_due');
28
+
29
+        $amountOverdue = $unpaidBills
30
+            ->clone()
31
+            ->where('status', BillStatus::Overdue)
32
+            ->sum('amount_due');
33
+
34
+        $amountDueWithin7Days = $unpaidBills
35
+            ->clone()
36
+            ->whereBetween('due_date', [today(), today()->addWeek()])
37
+            ->sum('amount_due');
38
+
39
+        $averagePaymentTime = $this->getPageTableQuery()
40
+            ->whereNotNull('paid_at')
41
+            ->selectRaw('AVG(TIMESTAMPDIFF(DAY, date, paid_at)) as avg_days')
42
+            ->value('avg_days');
43
+
44
+        $averagePaymentTimeFormatted = Number::format($averagePaymentTime ?? 0, maxPrecision: 1);
45
+
46
+        $lastMonthTotal = $this->getPageTableQuery()
47
+            ->where('status', BillStatus::Paid)
48
+            ->whereBetween('date', [
49
+                today()->subMonth()->startOfMonth(),
50
+                today()->subMonth()->endOfMonth(),
51
+            ])
52
+            ->sum('amount_paid');
53
+
54
+        return [
55
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Total To Pay', CurrencyConverter::formatCentsToMoney($amountToPay))
56
+                ->suffix(CurrencyAccessor::getDefaultCurrency())
57
+                ->description('Includes ' . CurrencyConverter::formatCentsToMoney($amountOverdue) . ' overdue'),
58
+
59
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Due Within 7 Days', CurrencyConverter::formatCentsToMoney($amountDueWithin7Days))
60
+                ->suffix(CurrencyAccessor::getDefaultCurrency()),
61
+
62
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Average Payment Time', $averagePaymentTimeFormatted)
63
+                ->suffix('days'),
64
+
65
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Paid Last Month', CurrencyConverter::formatCentsToMoney($lastMonthTotal))
66
+                ->suffix(CurrencyAccessor::getDefaultCurrency()),
67
+        ];
68
+    }
69
+}

+ 1
- 2
app/Filament/Company/Resources/Sales/InvoiceResource.php Vedi File

15
 use App\Models\Accounting\Invoice;
15
 use App\Models\Accounting\Invoice;
16
 use App\Models\Banking\BankAccount;
16
 use App\Models\Banking\BankAccount;
17
 use App\Models\Common\Offering;
17
 use App\Models\Common\Offering;
18
-use App\Utilities\Currency\CurrencyAccessor;
19
 use App\Utilities\Currency\CurrencyConverter;
18
 use App\Utilities\Currency\CurrencyConverter;
20
 use Awcodes\TableRepeater\Components\TableRepeater;
19
 use Awcodes\TableRepeater\Components\TableRepeater;
21
 use Awcodes\TableRepeater\Header;
20
 use Awcodes\TableRepeater\Header;
311
                                         // Final total
310
                                         // Final total
312
                                         $total = $subtotal + ($taxAmount - $discountAmount);
311
                                         $total = $subtotal + ($taxAmount - $discountAmount);
313
 
312
 
314
-                                        return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
313
+                                        return CurrencyConverter::formatToMoney($total);
315
                                     }),
314
                                     }),
316
                             ]),
315
                             ]),
317
                         Forms\Components\Grid::make(6)
316
                         Forms\Components\Grid::make(6)

+ 2
- 12
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ListInvoices.php Vedi File

47
             'unpaid' => Tab::make()
47
             'unpaid' => Tab::make()
48
                 ->label('Unpaid')
48
                 ->label('Unpaid')
49
                 ->modifyQueryUsing(function (Builder $query) {
49
                 ->modifyQueryUsing(function (Builder $query) {
50
-                    $query->whereIn('status', [
51
-                        InvoiceStatus::Unsent,
52
-                        InvoiceStatus::Sent,
53
-                        InvoiceStatus::Partial,
54
-                        InvoiceStatus::Overdue,
55
-                    ]);
50
+                    $query->unpaid();
56
                 })
51
                 })
57
-                ->badge(Invoice::whereIn('status', [
58
-                    InvoiceStatus::Unsent,
59
-                    InvoiceStatus::Sent,
60
-                    InvoiceStatus::Partial,
61
-                    InvoiceStatus::Overdue,
62
-                ])->count()),
52
+                ->badge(Invoice::unpaid()->count()),
63
 
53
 
64
             'draft' => Tab::make()
54
             'draft' => Tab::make()
65
                 ->label('Draft')
55
                 ->label('Draft')

+ 20
- 27
app/Filament/Company/Resources/Sales/InvoiceResource/Widgets/InvoiceOverview.php Vedi File

4
 
4
 
5
 use App\Enums\Accounting\InvoiceStatus;
5
 use App\Enums\Accounting\InvoiceStatus;
6
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ListInvoices;
6
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ListInvoices;
7
+use App\Filament\Widgets\EnhancedStatsOverviewWidget;
8
+use App\Utilities\Currency\CurrencyAccessor;
7
 use App\Utilities\Currency\CurrencyConverter;
9
 use App\Utilities\Currency\CurrencyConverter;
8
 use Filament\Widgets\Concerns\InteractsWithPageTable;
10
 use Filament\Widgets\Concerns\InteractsWithPageTable;
9
-use Filament\Widgets\StatsOverviewWidget as BaseWidget;
10
-use Filament\Widgets\StatsOverviewWidget\Stat;
11
 use Illuminate\Support\Number;
11
 use Illuminate\Support\Number;
12
 
12
 
13
-class InvoiceOverview extends BaseWidget
13
+class InvoiceOverview extends EnhancedStatsOverviewWidget
14
 {
14
 {
15
     use InteractsWithPageTable;
15
     use InteractsWithPageTable;
16
 
16
 
17
-    protected static ?string $pollingInterval = null;
18
-
19
     protected function getTablePage(): string
17
     protected function getTablePage(): string
20
     {
18
     {
21
         return ListInvoices::class;
19
         return ListInvoices::class;
23
 
21
 
24
     protected function getStats(): array
22
     protected function getStats(): array
25
     {
23
     {
26
-        $outstandingInvoices = $this->getPageTableQuery()
27
-            ->whereNotIn('status', [
28
-                InvoiceStatus::Paid,
29
-                InvoiceStatus::Void,
30
-                InvoiceStatus::Draft,
31
-                InvoiceStatus::Overpaid,
32
-            ]);
24
+        $unpaidInvoices = $this->getPageTableQuery()->unpaid();
33
 
25
 
34
-        $amountOutstanding = $outstandingInvoices
35
-            ->clone()
36
-            ->sum('amount_due');
26
+        $amountUnpaid = $unpaidInvoices->sum('amount_due');
37
 
27
 
38
-        $amountOverdue = $outstandingInvoices
28
+        $amountOverdue = $unpaidInvoices
39
             ->clone()
29
             ->clone()
40
             ->where('status', InvoiceStatus::Overdue)
30
             ->where('status', InvoiceStatus::Overdue)
41
             ->sum('amount_due');
31
             ->sum('amount_due');
42
 
32
 
43
-        $amountDueWithin30Days = $outstandingInvoices
33
+        $amountDueWithin30Days = $unpaidInvoices
44
             ->clone()
34
             ->clone()
45
-            ->where('due_date', '>=', today())
46
-            ->where('due_date', '<=', today()->addDays(30))
35
+            ->whereBetween('due_date', [today(), today()->addMonth()])
47
             ->sum('amount_due');
36
             ->sum('amount_due');
48
 
37
 
49
         $validInvoices = $this->getPageTableQuery()
38
         $validInvoices = $this->getPageTableQuery()
52
                 InvoiceStatus::Draft,
41
                 InvoiceStatus::Draft,
53
             ]);
42
             ]);
54
 
43
 
55
-        $totalValidInvoiceAmount = $validInvoices->clone()->sum('amount_due');
44
+        $totalValidInvoiceAmount = $validInvoices->sum('total');
56
 
45
 
57
-        $totalValidInvoiceCount = $validInvoices->clone()->count();
46
+        $totalValidInvoiceCount = $validInvoices->count();
58
 
47
 
59
         $averageInvoiceTotal = $totalValidInvoiceCount > 0 ? $totalValidInvoiceAmount / $totalValidInvoiceCount : 0;
48
         $averageInvoiceTotal = $totalValidInvoiceCount > 0 ? $totalValidInvoiceAmount / $totalValidInvoiceCount : 0;
60
 
49
 
63
             ->selectRaw('AVG(TIMESTAMPDIFF(DAY, date, paid_at)) as avg_days')
52
             ->selectRaw('AVG(TIMESTAMPDIFF(DAY, date, paid_at)) as avg_days')
64
             ->value('avg_days');
53
             ->value('avg_days');
65
 
54
 
55
+        $averagePaymentTimeFormatted = Number::format($averagePaymentTime ?? 0, maxPrecision: 1);
56
+
66
         return [
57
         return [
67
-            Stat::make('Total Outstanding', CurrencyConverter::formatCentsToMoney($amountOutstanding))
58
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Total Unpaid', CurrencyConverter::formatCentsToMoney($amountUnpaid))
59
+                ->suffix(CurrencyAccessor::getDefaultCurrency())
68
                 ->description('Includes ' . CurrencyConverter::formatCentsToMoney($amountOverdue) . ' overdue'),
60
                 ->description('Includes ' . CurrencyConverter::formatCentsToMoney($amountOverdue) . ' overdue'),
69
-            Stat::make('Due Within 30 Days', CurrencyConverter::formatCentsToMoney($amountDueWithin30Days)),
70
-            Stat::make('Average Payment Time', Number::format($averagePaymentTime ?? 0, maxPrecision: 1) . ' days')
71
-                ->description('From invoice date to payment received'),
72
-            Stat::make('Average Invoice Total', CurrencyConverter::formatCentsToMoney($averageInvoiceTotal))
73
-                ->description('Excludes void and draft invoices'),
61
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Due Within 30 Days', CurrencyConverter::formatCentsToMoney($amountDueWithin30Days))
62
+                ->suffix(CurrencyAccessor::getDefaultCurrency()),
63
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Average Payment Time', $averagePaymentTimeFormatted)
64
+                ->suffix('days'),
65
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Average Invoice Total', CurrencyConverter::formatCentsToMoney($averageInvoiceTotal))
66
+                ->suffix(CurrencyAccessor::getDefaultCurrency()),
74
         ];
67
         ];
75
     }
68
     }
76
 }
69
 }

+ 12
- 0
app/Filament/Widgets/EnhancedStatsOverviewWidget.php Vedi File

1
+<?php
2
+
3
+namespace App\Filament\Widgets;
4
+
5
+use Filament\Widgets\StatsOverviewWidget;
6
+
7
+class EnhancedStatsOverviewWidget extends StatsOverviewWidget
8
+{
9
+    protected static ?string $pollingInterval = null;
10
+
11
+    protected static bool $isLazy = false;
12
+}

+ 47
- 0
app/Filament/Widgets/EnhancedStatsOverviewWidget/EnhancedStat.php Vedi File

1
+<?php
2
+
3
+namespace App\Filament\Widgets\EnhancedStatsOverviewWidget;
4
+
5
+use Closure;
6
+use Filament\Support\Concerns\EvaluatesClosures;
7
+use Filament\Widgets\StatsOverviewWidget\Stat;
8
+use Illuminate\Contracts\Support\Htmlable;
9
+use Illuminate\Contracts\View\View;
10
+
11
+class EnhancedStat extends Stat
12
+{
13
+    use EvaluatesClosures;
14
+
15
+    protected string | Htmlable | Closure | null $prefixLabel = null;
16
+
17
+    protected string | Htmlable | Closure | null $suffixLabel = null;
18
+
19
+    public function prefix(string | Htmlable | Closure | null $label): static
20
+    {
21
+        $this->prefixLabel = $label;
22
+
23
+        return $this;
24
+    }
25
+
26
+    public function suffix(string | Htmlable | Closure | null $label): static
27
+    {
28
+        $this->suffixLabel = $label;
29
+
30
+        return $this;
31
+    }
32
+
33
+    public function getPrefixLabel(): string | Htmlable | null
34
+    {
35
+        return $this->evaluate($this->prefixLabel);
36
+    }
37
+
38
+    public function getSuffixLabel(): string | Htmlable | null
39
+    {
40
+        return $this->evaluate($this->suffixLabel);
41
+    }
42
+
43
+    public function render(): View
44
+    {
45
+        return view('filament.widgets.enhanced-stats-overview-widget.enhanced-stat', $this->data());
46
+    }
47
+}

+ 5
- 0
app/Models/Accounting/Account.php Vedi File

128
         return self::where('name', 'Accounts Receivable')->firstOrFail();
128
         return self::where('name', 'Accounts Receivable')->firstOrFail();
129
     }
129
     }
130
 
130
 
131
+    public static function getAccountsPayableAccount(): self
132
+    {
133
+        return self::where('name', 'Accounts Payable')->firstOrFail();
134
+    }
135
+
131
     protected static function newFactory(): Factory
136
     protected static function newFactory(): Factory
132
     {
137
     {
133
         return AccountFactory::new();
138
         return AccountFactory::new();

+ 169
- 1
app/Models/Accounting/Bill.php Vedi File

6
 use App\Concerns\Blamable;
6
 use App\Concerns\Blamable;
7
 use App\Concerns\CompanyOwned;
7
 use App\Concerns\CompanyOwned;
8
 use App\Enums\Accounting\BillStatus;
8
 use App\Enums\Accounting\BillStatus;
9
+use App\Enums\Accounting\JournalEntryType;
9
 use App\Enums\Accounting\TransactionType;
10
 use App\Enums\Accounting\TransactionType;
11
+use App\Filament\Company\Resources\Purchases\BillResource;
10
 use App\Models\Common\Vendor;
12
 use App\Models\Common\Vendor;
13
+use App\Observers\BillObserver;
14
+use Filament\Actions\MountableAction;
15
+use Filament\Actions\ReplicateAction;
16
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
17
+use Illuminate\Database\Eloquent\Builder;
18
+use Illuminate\Database\Eloquent\Casts\Attribute;
11
 use Illuminate\Database\Eloquent\Factories\HasFactory;
19
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12
 use Illuminate\Database\Eloquent\Model;
20
 use Illuminate\Database\Eloquent\Model;
13
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
21
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
 use Illuminate\Database\Eloquent\Relations\MorphMany;
22
 use Illuminate\Database\Eloquent\Relations\MorphMany;
15
 use Illuminate\Database\Eloquent\Relations\MorphOne;
23
 use Illuminate\Database\Eloquent\Relations\MorphOne;
24
+use Illuminate\Support\Carbon;
16
 
25
 
26
+#[ObservedBy(BillObserver::class)]
17
 class Bill extends Model
27
 class Bill extends Model
18
 {
28
 {
19
     use Blamable;
29
     use Blamable;
29
         'order_number',
39
         'order_number',
30
         'date',
40
         'date',
31
         'due_date',
41
         'due_date',
42
+        'paid_at',
32
         'status',
43
         'status',
33
         'currency_code',
44
         'currency_code',
34
         'subtotal',
45
         'subtotal',
36
         'discount_total',
47
         'discount_total',
37
         'total',
48
         'total',
38
         'amount_paid',
49
         'amount_paid',
50
+        'notes',
39
         'created_by',
51
         'created_by',
40
         'updated_by',
52
         'updated_by',
41
     ];
53
     ];
43
     protected $casts = [
55
     protected $casts = [
44
         'date' => 'date',
56
         'date' => 'date',
45
         'due_date' => 'date',
57
         'due_date' => 'date',
58
+        'paid_at' => 'datetime',
46
         'status' => BillStatus::class,
59
         'status' => BillStatus::class,
47
         'subtotal' => MoneyCast::class,
60
         'subtotal' => MoneyCast::class,
48
         'tax_total' => MoneyCast::class,
61
         'tax_total' => MoneyCast::class,
82
         return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
95
         return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
83
     }
96
     }
84
 
97
 
85
-    public function approvalTransaction(): MorphOne
98
+    public function initialTransaction(): MorphOne
86
     {
99
     {
87
         return $this->morphOne(Transaction::class, 'transactionable')
100
         return $this->morphOne(Transaction::class, 'transactionable')
88
             ->where('type', TransactionType::Journal);
101
             ->where('type', TransactionType::Journal);
89
     }
102
     }
90
 
103
 
104
+    protected function isCurrentlyOverdue(): Attribute
105
+    {
106
+        return Attribute::get(function () {
107
+            return $this->due_date->isBefore(today()) && $this->canBeOverdue();
108
+        });
109
+    }
110
+
111
+    public function canBeOverdue(): bool
112
+    {
113
+        return in_array($this->status, BillStatus::canBeOverdue());
114
+    }
115
+
116
+    public function canRecordPayment(): bool
117
+    {
118
+        return ! in_array($this->status, [
119
+            BillStatus::Paid,
120
+            BillStatus::Void,
121
+        ]);
122
+    }
123
+
91
     public static function getNextDocumentNumber(): string
124
     public static function getNextDocumentNumber(): string
92
     {
125
     {
93
         $company = auth()->user()->currentCompany;
126
         $company = auth()->user()->currentCompany;
120
             next: $numberNext
153
             next: $numberNext
121
         );
154
         );
122
     }
155
     }
156
+
157
+    public function hasInitialTransaction(): bool
158
+    {
159
+        return $this->initialTransaction()->exists();
160
+    }
161
+
162
+    public function scopeOutstanding(Builder $query): Builder
163
+    {
164
+        return $query->whereIn('status', [
165
+            BillStatus::Unpaid,
166
+            BillStatus::Partial,
167
+            BillStatus::Overdue,
168
+        ]);
169
+    }
170
+
171
+    public function recordPayment(array $data): void
172
+    {
173
+        $transactionType = TransactionType::Withdrawal;
174
+        $transactionDescription = 'Payment for Bill #' . $this->bill_number;
175
+
176
+        // Create transaction
177
+        $this->transactions()->create([
178
+            'company_id' => $this->company_id,
179
+            'type' => $transactionType,
180
+            'is_payment' => true,
181
+            'posted_at' => $data['posted_at'],
182
+            'amount' => $data['amount'],
183
+            'payment_method' => $data['payment_method'],
184
+            'bank_account_id' => $data['bank_account_id'],
185
+            'account_id' => Account::getAccountsPayableAccount()->id,
186
+            'description' => $transactionDescription,
187
+            'notes' => $data['notes'] ?? null,
188
+        ]);
189
+    }
190
+
191
+    public function createInitialTransaction(?Carbon $postedAt = null): void
192
+    {
193
+        $postedAt ??= now();
194
+
195
+        $transaction = $this->transactions()->create([
196
+            'company_id' => $this->company_id,
197
+            'type' => TransactionType::Journal,
198
+            'posted_at' => $postedAt,
199
+            'amount' => $this->total,
200
+            'description' => 'Bill Creation for Bill #' . $this->bill_number,
201
+        ]);
202
+
203
+        $transaction->journalEntries()->create([
204
+            'company_id' => $this->company_id,
205
+            'type' => JournalEntryType::Credit,
206
+            'account_id' => Account::getAccountsPayableAccount()->id,
207
+            'amount' => $this->total,
208
+            'description' => $transaction->description,
209
+        ]);
210
+
211
+        foreach ($this->lineItems as $lineItem) {
212
+            $transaction->journalEntries()->create([
213
+                'company_id' => $this->company_id,
214
+                'type' => JournalEntryType::Debit,
215
+                'account_id' => $lineItem->offering->expense_account_id,
216
+                'amount' => $lineItem->subtotal,
217
+                'description' => $transaction->description,
218
+            ]);
219
+
220
+            foreach ($lineItem->adjustments as $adjustment) {
221
+                if ($adjustment->isNonRecoverablePurchaseTax()) {
222
+                    $transaction->journalEntries()->create([
223
+                        'company_id' => $this->company_id,
224
+                        'type' => JournalEntryType::Debit,
225
+                        'account_id' => $lineItem->offering->expense_account_id,
226
+                        'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
227
+                        'description' => $transaction->description . " ($adjustment->name)",
228
+                    ]);
229
+                } elseif ($adjustment->account_id) {
230
+                    $transaction->journalEntries()->create([
231
+                        'company_id' => $this->company_id,
232
+                        'type' => $adjustment->category->isDiscount() ? JournalEntryType::Credit : JournalEntryType::Debit,
233
+                        'account_id' => $adjustment->account_id,
234
+                        'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
235
+                        'description' => $transaction->description,
236
+                    ]);
237
+                }
238
+            }
239
+        }
240
+    }
241
+
242
+    public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
243
+    {
244
+        return $action::make()
245
+            ->excludeAttributes([
246
+                'status',
247
+                'amount_paid',
248
+                'amount_due',
249
+                'created_by',
250
+                'updated_by',
251
+                'created_at',
252
+                'updated_at',
253
+                'bill_number',
254
+                'date',
255
+                'due_date',
256
+                'paid_at',
257
+            ])
258
+            ->modal(false)
259
+            ->beforeReplicaSaved(function (self $original, self $replica) {
260
+                $replica->status = BillStatus::Unpaid;
261
+                $replica->bill_number = self::getNextDocumentNumber();
262
+                $replica->date = now();
263
+                $replica->due_date = now()->addDays($original->company->defaultBill->payment_terms->getDays());
264
+            })
265
+            ->databaseTransaction()
266
+            ->after(function (self $original, self $replica) {
267
+                $original->lineItems->each(function (DocumentLineItem $lineItem) use ($replica) {
268
+                    $replicaLineItem = $lineItem->replicate([
269
+                        'documentable_id',
270
+                        'documentable_type',
271
+                        'subtotal',
272
+                        'total',
273
+                        'created_by',
274
+                        'updated_by',
275
+                        'created_at',
276
+                        'updated_at',
277
+                    ]);
278
+
279
+                    $replicaLineItem->documentable_id = $replica->id;
280
+                    $replicaLineItem->documentable_type = $replica->getMorphClass();
281
+
282
+                    $replicaLineItem->save();
283
+
284
+                    $replicaLineItem->adjustments()->sync($lineItem->adjustments->pluck('id'));
285
+                });
286
+            })
287
+            ->successRedirectUrl(static function (self $replica) {
288
+                return BillResource::getUrl('edit', ['record' => $replica]);
289
+            });
290
+    }
123
 }
291
 }

+ 26
- 1
app/Models/Accounting/Invoice.php Vedi File

17
 use Filament\Actions\ReplicateAction;
17
 use Filament\Actions\ReplicateAction;
18
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
18
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
19
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
19
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
20
+use Illuminate\Database\Eloquent\Builder;
20
 use Illuminate\Database\Eloquent\Casts\Attribute;
21
 use Illuminate\Database\Eloquent\Casts\Attribute;
21
 use Illuminate\Database\Eloquent\Factories\HasFactory;
22
 use Illuminate\Database\Eloquent\Factories\HasFactory;
22
 use Illuminate\Database\Eloquent\Model;
23
 use Illuminate\Database\Eloquent\Model;
112
             ->where('type', TransactionType::Journal);
113
             ->where('type', TransactionType::Journal);
113
     }
114
     }
114
 
115
 
116
+    public function scopeUnpaid(Builder $query): Builder
117
+    {
118
+        return $query->whereNotIn('status', [
119
+            InvoiceStatus::Paid,
120
+            InvoiceStatus::Void,
121
+            InvoiceStatus::Draft,
122
+            InvoiceStatus::Overpaid,
123
+        ]);
124
+    }
125
+
115
     protected function isCurrentlyOverdue(): Attribute
126
     protected function isCurrentlyOverdue(): Attribute
116
     {
127
     {
117
         return Attribute::get(function () {
128
         return Attribute::get(function () {
297
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
308
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
298
     {
309
     {
299
         return $action::make()
310
         return $action::make()
300
-            ->excludeAttributes(['status', 'amount_paid', 'amount_due', 'created_by', 'updated_by', 'created_at', 'updated_at', 'invoice_number', 'date', 'due_date'])
311
+            ->excludeAttributes([
312
+                'status',
313
+                'amount_paid',
314
+                'amount_due',
315
+                'created_by',
316
+                'updated_by',
317
+                'created_at',
318
+                'updated_at',
319
+                'invoice_number',
320
+                'date',
321
+                'due_date',
322
+                'approved_at',
323
+                'paid_at',
324
+                'last_sent',
325
+            ])
301
             ->modal(false)
326
             ->modal(false)
302
             ->beforeReplicaSaved(function (self $original, self $replica) {
327
             ->beforeReplicaSaved(function (self $original, self $replica) {
303
                 $replica->status = InvoiceStatus::Draft;
328
                 $replica->status = InvoiceStatus::Draft;

+ 40
- 0
app/Observers/BillObserver.php Vedi File

1
+<?php
2
+
3
+namespace App\Observers;
4
+
5
+use App\Enums\Accounting\BillStatus;
6
+use App\Models\Accounting\Bill;
7
+use App\Models\Accounting\DocumentLineItem;
8
+use App\Models\Accounting\Transaction;
9
+use Illuminate\Support\Facades\DB;
10
+
11
+class BillObserver
12
+{
13
+    public function created(Bill $bill): void
14
+    {
15
+        // $bill->createInitialTransaction();
16
+    }
17
+
18
+    public function saving(Bill $bill): void
19
+    {
20
+        if ($bill->is_currently_overdue) {
21
+            $bill->status = BillStatus::Overdue;
22
+        }
23
+    }
24
+
25
+    /**
26
+     * Handle the Bill "deleted" event.
27
+     */
28
+    public function deleted(Bill $bill): void
29
+    {
30
+        DB::transaction(function () use ($bill) {
31
+            $bill->lineItems()->each(function (DocumentLineItem $lineItem) {
32
+                $lineItem->delete();
33
+            });
34
+
35
+            $bill->transactions()->each(function (Transaction $transaction) {
36
+                $transaction->delete();
37
+            });
38
+        });
39
+    }
40
+}

+ 0
- 37
app/Observers/InvoiceObserver.php Vedi File

10
 
10
 
11
 class InvoiceObserver
11
 class InvoiceObserver
12
 {
12
 {
13
-    /**
14
-     * Handle the Invoice "created" event.
15
-     */
16
-    public function created(Invoice $invoice): void
17
-    {
18
-        //
19
-    }
20
-
21
-    /**
22
-     * Handle the Invoice "updated" event.
23
-     */
24
-    public function updated(Invoice $invoice): void
25
-    {
26
-        //
27
-    }
28
-
29
-    public function deleting(Invoice $invoice): void
30
-    {
31
-        //
32
-    }
33
-
34
     public function saving(Invoice $invoice): void
13
     public function saving(Invoice $invoice): void
35
     {
14
     {
36
         if ($invoice->approved_at && $invoice->is_currently_overdue) {
15
         if ($invoice->approved_at && $invoice->is_currently_overdue) {
53
             });
32
             });
54
         });
33
         });
55
     }
34
     }
56
-
57
-    /**
58
-     * Handle the Invoice "restored" event.
59
-     */
60
-    public function restored(Invoice $invoice): void
61
-    {
62
-        //
63
-    }
64
-
65
-    /**
66
-     * Handle the Invoice "force deleted" event.
67
-     */
68
-    public function forceDeleted(Invoice $invoice): void
69
-    {
70
-        //
71
-    }
72
 }
35
 }

+ 47
- 10
app/Observers/TransactionObserver.php Vedi File

2
 
2
 
3
 namespace App\Observers;
3
 namespace App\Observers;
4
 
4
 
5
+use App\Enums\Accounting\BillStatus;
5
 use App\Enums\Accounting\InvoiceStatus;
6
 use App\Enums\Accounting\InvoiceStatus;
7
+use App\Models\Accounting\Bill;
6
 use App\Models\Accounting\Invoice;
8
 use App\Models\Accounting\Invoice;
7
 use App\Models\Accounting\Transaction;
9
 use App\Models\Accounting\Transaction;
8
 use App\Services\TransactionService;
10
 use App\Services\TransactionService;
37
             return;
39
             return;
38
         }
40
         }
39
 
41
 
40
-        $invoice = $transaction->transactionable;
42
+        $document = $transaction->transactionable;
41
 
43
 
42
-        if ($invoice instanceof Invoice) {
43
-            $this->updateInvoiceTotals($invoice);
44
+        if ($document instanceof Invoice) {
45
+            $this->updateInvoiceTotals($document);
46
+        } elseif ($document instanceof Bill) {
47
+            $this->updateBillTotals($document);
44
         }
48
         }
45
     }
49
     }
46
 
50
 
57
             return;
61
             return;
58
         }
62
         }
59
 
63
 
60
-        $invoice = $transaction->transactionable;
64
+        $document = $transaction->transactionable;
61
 
65
 
62
-        if ($invoice instanceof Invoice) {
63
-            $this->updateInvoiceTotals($invoice);
66
+        if ($document instanceof Invoice) {
67
+            $this->updateInvoiceTotals($document);
68
+        } elseif ($document instanceof Bill) {
69
+            $this->updateBillTotals($document);
64
         }
70
         }
65
     }
71
     }
66
 
72
 
76
                 return;
82
                 return;
77
             }
83
             }
78
 
84
 
79
-            $invoice = $transaction->transactionable;
85
+            $document = $transaction->transactionable;
80
 
86
 
81
-            if ($invoice instanceof Invoice && ! $invoice->exists) {
87
+            if (($document instanceof Invoice || $document instanceof Bill) && ! $document->exists) {
82
                 return;
88
                 return;
83
             }
89
             }
84
 
90
 
85
-            if ($invoice instanceof Invoice) {
86
-                $this->updateInvoiceTotals($invoice, $transaction);
91
+            if ($document instanceof Invoice) {
92
+                $this->updateInvoiceTotals($document, $transaction);
93
+            } elseif ($document instanceof Bill) {
94
+                $this->updateBillTotals($document, $transaction);
87
             }
95
             }
88
         });
96
         });
89
     }
97
     }
127
             'paid_at' => $paidAt,
135
             'paid_at' => $paidAt,
128
         ]);
136
         ]);
129
     }
137
     }
138
+
139
+    protected function updateBillTotals(Bill $bill, ?Transaction $excludedTransaction = null): void
140
+    {
141
+        $withdrawalTotal = (int) $bill->withdrawals()
142
+            ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
143
+            ->sum('amount');
144
+
145
+        $totalPaid = $withdrawalTotal;
146
+        $billTotal = (int) $bill->getRawOriginal('total');
147
+
148
+        $newStatus = match (true) {
149
+            $totalPaid >= $billTotal => BillStatus::Paid,
150
+            default => BillStatus::Partial,
151
+        };
152
+
153
+        $paidAt = $bill->paid_at;
154
+
155
+        if ($newStatus === BillStatus::Paid && ! $paidAt) {
156
+            $paidAt = $bill->withdrawals()
157
+                ->latest('posted_at')
158
+                ->value('posted_at');
159
+        }
160
+
161
+        $bill->update([
162
+            'amount_paid' => CurrencyConverter::convertCentsToFloat($totalPaid),
163
+            'status' => $newStatus,
164
+            'paid_at' => $paidAt,
165
+        ]);
166
+    }
130
 }
167
 }

+ 0
- 4
app/Providers/MacroServiceProvider.php Vedi File

204
 
204
 
205
             $currencyCode = $this->currency->getCurrency();
205
             $currencyCode = $this->currency->getCurrency();
206
 
206
 
207
-            if ($currencyCode === CurrencyAccessor::getDefaultCurrency()) {
208
-                return $formatted;
209
-            }
210
-
211
             if ($codeBefore) {
207
             if ($codeBefore) {
212
                 return $currencyCode . ' ' . $formatted;
208
                 return $currencyCode . ' ' . $formatted;
213
             }
209
             }

+ 8
- 2
app/Utilities/Currency/CurrencyConverter.php Vedi File

49
         return money($amount, $currency, true)->getAmount();
49
         return money($amount, $currency, true)->getAmount();
50
     }
50
     }
51
 
51
 
52
-    public static function formatCentsToMoney(int $amount, ?string $currency = null): string
52
+    public static function formatCentsToMoney(int $amount, ?string $currency = null, bool $withCode = false): string
53
     {
53
     {
54
         $currency ??= CurrencyAccessor::getDefaultCurrency();
54
         $currency ??= CurrencyAccessor::getDefaultCurrency();
55
 
55
 
56
-        return money($amount, $currency)->format();
56
+        $money = money($amount, $currency);
57
+
58
+        if ($withCode) {
59
+            return $money->formatWithCode();
60
+        }
61
+
62
+        return $money->format();
57
     }
63
     }
58
 
64
 
59
     public static function formatToMoney(string | float $amount, ?string $currency = null): string
65
     public static function formatToMoney(string | float $amount, ?string $currency = null): string

+ 6
- 6
composer.lock Vedi File

5064
         },
5064
         },
5065
         {
5065
         {
5066
             "name": "openspout/openspout",
5066
             "name": "openspout/openspout",
5067
-            "version": "v4.28.1",
5067
+            "version": "v4.28.2",
5068
             "source": {
5068
             "source": {
5069
                 "type": "git",
5069
                 "type": "git",
5070
                 "url": "https://github.com/openspout/openspout.git",
5070
                 "url": "https://github.com/openspout/openspout.git",
5071
-                "reference": "229a9c837bd768e8767660671f9fdf429f343a74"
5071
+                "reference": "d6dd654b5db502f28c5773edfa785b516745a142"
5072
             },
5072
             },
5073
             "dist": {
5073
             "dist": {
5074
                 "type": "zip",
5074
                 "type": "zip",
5075
-                "url": "https://api.github.com/repos/openspout/openspout/zipball/229a9c837bd768e8767660671f9fdf429f343a74",
5076
-                "reference": "229a9c837bd768e8767660671f9fdf429f343a74",
5075
+                "url": "https://api.github.com/repos/openspout/openspout/zipball/d6dd654b5db502f28c5773edfa785b516745a142",
5076
+                "reference": "d6dd654b5db502f28c5773edfa785b516745a142",
5077
                 "shasum": ""
5077
                 "shasum": ""
5078
             },
5078
             },
5079
             "require": {
5079
             "require": {
5141
             ],
5141
             ],
5142
             "support": {
5142
             "support": {
5143
                 "issues": "https://github.com/openspout/openspout/issues",
5143
                 "issues": "https://github.com/openspout/openspout/issues",
5144
-                "source": "https://github.com/openspout/openspout/tree/v4.28.1"
5144
+                "source": "https://github.com/openspout/openspout/tree/v4.28.2"
5145
             },
5145
             },
5146
             "funding": [
5146
             "funding": [
5147
                 {
5147
                 {
5153
                     "type": "github"
5153
                     "type": "github"
5154
                 }
5154
                 }
5155
             ],
5155
             ],
5156
-            "time": "2024-12-05T13:34:00+00:00"
5156
+            "time": "2024-12-06T06:17:37+00:00"
5157
         },
5157
         },
5158
         {
5158
         {
5159
             "name": "paragonie/constant_time_encoding",
5159
             "name": "paragonie/constant_time_encoding",

+ 159
- 2
database/factories/Accounting/BillFactory.php Vedi File

2
 
2
 
3
 namespace Database\Factories\Accounting;
3
 namespace Database\Factories\Accounting;
4
 
4
 
5
+use App\Enums\Accounting\BillStatus;
6
+use App\Enums\Accounting\PaymentMethod;
7
+use App\Models\Accounting\Bill;
8
+use App\Models\Accounting\DocumentLineItem;
9
+use App\Models\Banking\BankAccount;
10
+use App\Models\Common\Vendor;
11
+use App\Utilities\Currency\CurrencyConverter;
5
 use Illuminate\Database\Eloquent\Factories\Factory;
12
 use Illuminate\Database\Eloquent\Factories\Factory;
13
+use Illuminate\Support\Carbon;
6
 
14
 
7
 /**
15
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\Bill>
16
+ * @extends Factory<Bill>
9
  */
17
  */
10
 class BillFactory extends Factory
18
 class BillFactory extends Factory
11
 {
19
 {
20
+    /**
21
+     * The name of the factory's corresponding model.
22
+     */
23
+    protected $model = Bill::class;
24
+
12
     /**
25
     /**
13
      * Define the model's default state.
26
      * Define the model's default state.
14
      *
27
      *
16
      */
29
      */
17
     public function definition(): array
30
     public function definition(): array
18
     {
31
     {
32
+        // 50% chance of being a future bill
33
+        $isFutureBill = $this->faker->boolean();
34
+
35
+        if ($isFutureBill) {
36
+            // For future bills, date is recent and due date is in near future
37
+            $billDate = $this->faker->dateTimeBetween('-10 days', '+10 days');
38
+        } else {
39
+            // For past bills, both date and due date are in the past
40
+            $billDate = $this->faker->dateTimeBetween('-1 year', '-30 days');
41
+        }
42
+
43
+        $dueDays = $this->faker->numberBetween(14, 60);
44
+
19
         return [
45
         return [
20
-            //
46
+            'company_id' => 1,
47
+            'vendor_id' => Vendor::inRandomOrder()->value('id'),
48
+            'bill_number' => $this->faker->unique()->numerify('BILL-#####'),
49
+            'order_number' => $this->faker->unique()->numerify('PO-#####'),
50
+            'date' => $billDate,
51
+            'due_date' => Carbon::parse($billDate)->addDays($dueDays),
52
+            'status' => BillStatus::Unpaid,
53
+            'currency_code' => 'USD',
54
+            'notes' => $this->faker->sentence,
55
+            'created_by' => 1,
56
+            'updated_by' => 1,
21
         ];
57
         ];
22
     }
58
     }
59
+
60
+    public function withLineItems(int $count = 3): self
61
+    {
62
+        return $this->has(DocumentLineItem::factory()->forBill()->count($count), 'lineItems');
63
+    }
64
+
65
+    public function initialized(): static
66
+    {
67
+        return $this->afterCreating(function (Bill $bill) {
68
+            if ($bill->hasInitialTransaction()) {
69
+                return;
70
+            }
71
+
72
+            $this->recalculateTotals($bill);
73
+
74
+            $postedAt = Carbon::parse($bill->date)->addHours($this->faker->numberBetween(1, 24));
75
+
76
+            $bill->createInitialTransaction($postedAt);
77
+        });
78
+    }
79
+
80
+    public function withPayments(?int $min = 1, ?int $max = null, BillStatus $billStatus = BillStatus::Paid): static
81
+    {
82
+        return $this->afterCreating(function (Bill $bill) use ($billStatus, $max, $min) {
83
+            if (! $bill->hasInitialTransaction()) {
84
+                $this->recalculateTotals($bill);
85
+
86
+                $postedAt = Carbon::parse($bill->date)->addHours($this->faker->numberBetween(1, 24));
87
+
88
+                $bill->createInitialTransaction($postedAt);
89
+            }
90
+
91
+            $bill->refresh();
92
+
93
+            $totalAmountDue = $bill->getRawOriginal('amount_due');
94
+
95
+            if ($billStatus === BillStatus::Partial) {
96
+                $totalAmountDue = (int) floor($totalAmountDue * 0.5);
97
+            }
98
+
99
+            if ($totalAmountDue <= 0 || empty($totalAmountDue)) {
100
+                return;
101
+            }
102
+
103
+            $paymentCount = $max && $min ? $this->faker->numberBetween($min, $max) : $min;
104
+            $paymentAmount = (int) floor($totalAmountDue / $paymentCount);
105
+            $remainingAmount = $totalAmountDue;
106
+
107
+            $paymentDate = Carbon::parse($bill->initialTransaction->posted_at);
108
+            $paymentDates = [];
109
+
110
+            for ($i = 0; $i < $paymentCount; $i++) {
111
+                $amount = $i === $paymentCount - 1 ? $remainingAmount : $paymentAmount;
112
+
113
+                if ($amount <= 0) {
114
+                    break;
115
+                }
116
+
117
+                $postedAt = $paymentDate->copy()->addDays($this->faker->numberBetween(1, 30));
118
+                $paymentDates[] = $postedAt;
119
+
120
+                $data = [
121
+                    'posted_at' => $postedAt,
122
+                    'amount' => CurrencyConverter::convertCentsToFormatSimple($amount, $bill->currency_code),
123
+                    'payment_method' => $this->faker->randomElement(PaymentMethod::class),
124
+                    'bank_account_id' => BankAccount::inRandomOrder()->value('id'),
125
+                    'notes' => $this->faker->sentence,
126
+                ];
127
+
128
+                $bill->recordPayment($data);
129
+                $remainingAmount -= $amount;
130
+            }
131
+
132
+            if ($billStatus === BillStatus::Paid) {
133
+                $latestPaymentDate = max($paymentDates);
134
+                $bill->updateQuietly([
135
+                    'status' => $billStatus,
136
+                    'paid_at' => $latestPaymentDate,
137
+                ]);
138
+            }
139
+        });
140
+    }
141
+
142
+    public function configure(): static
143
+    {
144
+        return $this->afterCreating(function (Bill $bill) {
145
+            $paddedId = str_pad((string) $bill->id, 5, '0', STR_PAD_LEFT);
146
+
147
+            $bill->updateQuietly([
148
+                'bill_number' => "BILL-{$paddedId}",
149
+                'order_number' => "PO-{$paddedId}",
150
+            ]);
151
+
152
+            $this->recalculateTotals($bill);
153
+
154
+            // Check for overdue status
155
+            if ($bill->due_date < today() && $bill->status !== BillStatus::Paid) {
156
+                $bill->updateQuietly([
157
+                    'status' => BillStatus::Overdue,
158
+                ]);
159
+            }
160
+        });
161
+    }
162
+
163
+    protected function recalculateTotals(Bill $bill): void
164
+    {
165
+        if ($bill->lineItems()->exists()) {
166
+            $bill->refresh();
167
+            $subtotal = $bill->lineItems()->sum('subtotal') / 100;
168
+            $taxTotal = $bill->lineItems()->sum('tax_total') / 100;
169
+            $discountTotal = $bill->lineItems()->sum('discount_total') / 100;
170
+            $grandTotal = $subtotal + $taxTotal - $discountTotal;
171
+
172
+            $bill->update([
173
+                'subtotal' => $subtotal,
174
+                'tax_total' => $taxTotal,
175
+                'discount_total' => $discountTotal,
176
+                'total' => $grandTotal,
177
+            ]);
178
+        }
179
+    }
23
 }
180
 }

+ 46
- 9
database/factories/Accounting/DocumentLineItemFactory.php Vedi File

23
      */
23
      */
24
     public function definition(): array
24
     public function definition(): array
25
     {
25
     {
26
-        $offering = Offering::with(['salesTaxes', 'salesDiscounts'])->inRandomOrder()->first();
27
-
28
         $quantity = $this->faker->numberBetween(1, 10);
26
         $quantity = $this->faker->numberBetween(1, 10);
29
-        $unitPrice = $offering->price;
30
 
27
 
31
         return [
28
         return [
32
             'company_id' => 1,
29
             'company_id' => 1,
33
-            'offering_id' => $offering->id,
34
             'description' => $this->faker->sentence,
30
             'description' => $this->faker->sentence,
35
             'quantity' => $quantity,
31
             'quantity' => $quantity,
36
-            'unit_price' => $unitPrice,
37
             'created_by' => 1,
32
             'created_by' => 1,
38
             'updated_by' => 1,
33
             'updated_by' => 1,
39
         ];
34
         ];
40
     }
35
     }
41
 
36
 
42
-    public function configure(): static
37
+    public function forInvoice(): static
43
     {
38
     {
44
-        return $this->afterCreating(function (DocumentLineItem $lineItem) {
39
+        return $this->state(function (array $attributes) {
40
+            $offering = Offering::with(['salesTaxes', 'salesDiscounts'])
41
+                ->where('sellable', true)
42
+                ->inRandomOrder()
43
+                ->first();
44
+
45
+            return [
46
+                'offering_id' => $offering->id,
47
+                'unit_price' => $offering->price,
48
+            ];
49
+        })->afterCreating(function (DocumentLineItem $lineItem) {
50
+            $offering = $lineItem->offering;
51
+
52
+            if ($offering) {
53
+                $lineItem->adjustments()->syncWithoutDetaching($offering->salesTaxes->pluck('id')->toArray());
54
+                $lineItem->adjustments()->syncWithoutDetaching($offering->salesDiscounts->pluck('id')->toArray());
55
+            }
56
+
57
+            $lineItem->refresh();
58
+
59
+            $taxTotal = $lineItem->calculateTaxTotal()->getAmount();
60
+            $discountTotal = $lineItem->calculateDiscountTotal()->getAmount();
61
+
62
+            $lineItem->updateQuietly([
63
+                'tax_total' => $taxTotal,
64
+                'discount_total' => $discountTotal,
65
+            ]);
66
+        });
67
+    }
68
+
69
+    public function forBill(): static
70
+    {
71
+        return $this->state(function (array $attributes) {
72
+            $offering = Offering::with(['purchaseTaxes', 'purchaseDiscounts'])
73
+                ->where('purchasable', true)
74
+                ->inRandomOrder()
75
+                ->first();
76
+
77
+            return [
78
+                'offering_id' => $offering->id,
79
+                'unit_price' => $offering->price,
80
+            ];
81
+        })->afterCreating(function (DocumentLineItem $lineItem) {
45
             $offering = $lineItem->offering;
82
             $offering = $lineItem->offering;
46
 
83
 
47
             if ($offering) {
84
             if ($offering) {
48
-                $lineItem->salesTaxes()->sync($offering->salesTaxes->pluck('id')->toArray());
49
-                $lineItem->salesDiscounts()->sync($offering->salesDiscounts->pluck('id')->toArray());
85
+                $lineItem->adjustments()->syncWithoutDetaching($offering->purchaseTaxes->pluck('id')->toArray());
86
+                $lineItem->adjustments()->syncWithoutDetaching($offering->purchaseDiscounts->pluck('id')->toArray());
50
             }
87
             }
51
 
88
 
52
             $lineItem->refresh();
89
             $lineItem->refresh();

+ 2
- 2
database/factories/Accounting/InvoiceFactory.php Vedi File

51
 
51
 
52
     public function withLineItems(int $count = 3): self
52
     public function withLineItems(int $count = 3): self
53
     {
53
     {
54
-        return $this->has(DocumentLineItem::factory()->count($count), 'lineItems');
54
+        return $this->has(DocumentLineItem::factory()->forInvoice()->count($count), 'lineItems');
55
     }
55
     }
56
 
56
 
57
     public function approved(): static
57
     public function approved(): static
126
             if ($invoiceStatus === InvoiceStatus::Paid) {
126
             if ($invoiceStatus === InvoiceStatus::Paid) {
127
                 $latestPaymentDate = max($paymentDates);
127
                 $latestPaymentDate = max($paymentDates);
128
                 $invoice->updateQuietly([
128
                 $invoice->updateQuietly([
129
-                    'status' => InvoiceStatus::Paid,
129
+                    'status' => $invoiceStatus,
130
                     'paid_at' => $latestPaymentDate,
130
                     'paid_at' => $latestPaymentDate,
131
                 ]);
131
                 ]);
132
             }
132
             }

+ 46
- 0
database/factories/CompanyFactory.php Vedi File

2
 
2
 
3
 namespace Database\Factories;
3
 namespace Database\Factories;
4
 
4
 
5
+use App\Enums\Accounting\BillStatus;
5
 use App\Enums\Accounting\InvoiceStatus;
6
 use App\Enums\Accounting\InvoiceStatus;
7
+use App\Models\Accounting\Bill;
6
 use App\Models\Accounting\Invoice;
8
 use App\Models\Accounting\Invoice;
7
 use App\Models\Accounting\Transaction;
9
 use App\Models\Accounting\Transaction;
8
 use App\Models\Common\Client;
10
 use App\Models\Common\Client;
160
                 ]);
162
                 ]);
161
         });
163
         });
162
     }
164
     }
165
+
166
+    public function withBills(int $count = 10): self
167
+    {
168
+        return $this->afterCreating(function (Company $company) use ($count) {
169
+            $unpaidCount = (int) floor($count * 0.4);
170
+            $paidCount = (int) floor($count * 0.4);
171
+            $partialCount = $count - ($unpaidCount + $paidCount);
172
+
173
+            // Create unpaid bills
174
+            Bill::factory()
175
+                ->count($unpaidCount)
176
+                ->withLineItems()
177
+                ->initialized()
178
+                ->create([
179
+                    'company_id' => $company->id,
180
+                    'created_by' => $company->user_id,
181
+                    'updated_by' => $company->user_id,
182
+                ]);
183
+
184
+            // Create paid bills
185
+            Bill::factory()
186
+                ->count($paidCount)
187
+                ->withLineItems()
188
+                ->initialized()
189
+                ->withPayments(max: 4)
190
+                ->create([
191
+                    'company_id' => $company->id,
192
+                    'created_by' => $company->user_id,
193
+                    'updated_by' => $company->user_id,
194
+                ]);
195
+
196
+            // Create partially paid bills
197
+            Bill::factory()
198
+                ->count($partialCount)
199
+                ->withLineItems()
200
+                ->initialized()
201
+                ->withPayments(max: 4, billStatus: BillStatus::Partial)
202
+                ->create([
203
+                    'company_id' => $company->id,
204
+                    'created_by' => $company->user_id,
205
+                    'updated_by' => $company->user_id,
206
+                ]);
207
+        });
208
+    }
163
 }
209
 }

+ 2
- 0
database/migrations/2024_11_27_221657_create_bills_table.php Vedi File

19
             $table->string('order_number')->nullable(); // PO, SO, etc.
19
             $table->string('order_number')->nullable(); // PO, SO, etc.
20
             $table->date('date')->nullable();
20
             $table->date('date')->nullable();
21
             $table->date('due_date')->nullable();
21
             $table->date('due_date')->nullable();
22
+            $table->timestamp('paid_at')->nullable();
22
             $table->string('status')->default('unpaid');
23
             $table->string('status')->default('unpaid');
23
             $table->string('currency_code')->nullable();
24
             $table->string('currency_code')->nullable();
24
             $table->integer('subtotal')->default(0);
25
             $table->integer('subtotal')->default(0);
27
             $table->integer('total')->default(0);
28
             $table->integer('total')->default(0);
28
             $table->integer('amount_paid')->default(0);
29
             $table->integer('amount_paid')->default(0);
29
             $table->integer('amount_due')->storedAs('total - amount_paid');
30
             $table->integer('amount_due')->storedAs('total - amount_paid');
31
+            $table->text('notes')->nullable();
30
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
32
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
31
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
33
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
32
             $table->timestamps();
34
             $table->timestamps();

+ 2
- 1
database/seeders/DatabaseSeeder.php Vedi File

24
                     ->withOfferings()
24
                     ->withOfferings()
25
                     ->withClients()
25
                     ->withClients()
26
                     ->withVendors()
26
                     ->withVendors()
27
-                    ->withInvoices(50);
27
+                    ->withInvoices(50)
28
+                    ->withBills(50);
28
             })
29
             })
29
             ->create([
30
             ->create([
30
                 'name' => 'Admin',
31
                 'name' => 'Admin',

+ 93
- 78
package-lock.json Vedi File

558
             }
558
             }
559
         },
559
         },
560
         "node_modules/@rollup/rollup-android-arm-eabi": {
560
         "node_modules/@rollup/rollup-android-arm-eabi": {
561
-            "version": "4.28.0",
562
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz",
563
-            "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==",
561
+            "version": "4.28.1",
562
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
563
+            "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
564
             "cpu": [
564
             "cpu": [
565
                 "arm"
565
                 "arm"
566
             ],
566
             ],
572
             ]
572
             ]
573
         },
573
         },
574
         "node_modules/@rollup/rollup-android-arm64": {
574
         "node_modules/@rollup/rollup-android-arm64": {
575
-            "version": "4.28.0",
576
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz",
577
-            "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==",
575
+            "version": "4.28.1",
576
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
577
+            "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
578
             "cpu": [
578
             "cpu": [
579
                 "arm64"
579
                 "arm64"
580
             ],
580
             ],
586
             ]
586
             ]
587
         },
587
         },
588
         "node_modules/@rollup/rollup-darwin-arm64": {
588
         "node_modules/@rollup/rollup-darwin-arm64": {
589
-            "version": "4.28.0",
590
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz",
591
-            "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==",
589
+            "version": "4.28.1",
590
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
591
+            "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
592
             "cpu": [
592
             "cpu": [
593
                 "arm64"
593
                 "arm64"
594
             ],
594
             ],
600
             ]
600
             ]
601
         },
601
         },
602
         "node_modules/@rollup/rollup-darwin-x64": {
602
         "node_modules/@rollup/rollup-darwin-x64": {
603
-            "version": "4.28.0",
604
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz",
605
-            "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==",
603
+            "version": "4.28.1",
604
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
605
+            "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
606
             "cpu": [
606
             "cpu": [
607
                 "x64"
607
                 "x64"
608
             ],
608
             ],
614
             ]
614
             ]
615
         },
615
         },
616
         "node_modules/@rollup/rollup-freebsd-arm64": {
616
         "node_modules/@rollup/rollup-freebsd-arm64": {
617
-            "version": "4.28.0",
618
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz",
619
-            "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==",
617
+            "version": "4.28.1",
618
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
619
+            "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
620
             "cpu": [
620
             "cpu": [
621
                 "arm64"
621
                 "arm64"
622
             ],
622
             ],
628
             ]
628
             ]
629
         },
629
         },
630
         "node_modules/@rollup/rollup-freebsd-x64": {
630
         "node_modules/@rollup/rollup-freebsd-x64": {
631
-            "version": "4.28.0",
632
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz",
633
-            "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==",
631
+            "version": "4.28.1",
632
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
633
+            "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
634
             "cpu": [
634
             "cpu": [
635
                 "x64"
635
                 "x64"
636
             ],
636
             ],
642
             ]
642
             ]
643
         },
643
         },
644
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
644
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
645
-            "version": "4.28.0",
646
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz",
647
-            "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==",
645
+            "version": "4.28.1",
646
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
647
+            "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
648
             "cpu": [
648
             "cpu": [
649
                 "arm"
649
                 "arm"
650
             ],
650
             ],
656
             ]
656
             ]
657
         },
657
         },
658
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
658
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
659
-            "version": "4.28.0",
660
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz",
661
-            "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==",
659
+            "version": "4.28.1",
660
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
661
+            "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
662
             "cpu": [
662
             "cpu": [
663
                 "arm"
663
                 "arm"
664
             ],
664
             ],
670
             ]
670
             ]
671
         },
671
         },
672
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
672
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
673
-            "version": "4.28.0",
674
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz",
675
-            "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==",
673
+            "version": "4.28.1",
674
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
675
+            "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
676
             "cpu": [
676
             "cpu": [
677
                 "arm64"
677
                 "arm64"
678
             ],
678
             ],
684
             ]
684
             ]
685
         },
685
         },
686
         "node_modules/@rollup/rollup-linux-arm64-musl": {
686
         "node_modules/@rollup/rollup-linux-arm64-musl": {
687
-            "version": "4.28.0",
688
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz",
689
-            "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==",
687
+            "version": "4.28.1",
688
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
689
+            "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
690
             "cpu": [
690
             "cpu": [
691
                 "arm64"
691
                 "arm64"
692
             ],
692
             ],
697
                 "linux"
697
                 "linux"
698
             ]
698
             ]
699
         },
699
         },
700
+        "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
701
+            "version": "4.28.1",
702
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
703
+            "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
704
+            "cpu": [
705
+                "loong64"
706
+            ],
707
+            "dev": true,
708
+            "license": "MIT",
709
+            "optional": true,
710
+            "os": [
711
+                "linux"
712
+            ]
713
+        },
700
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
714
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
701
-            "version": "4.28.0",
702
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz",
703
-            "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==",
715
+            "version": "4.28.1",
716
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
717
+            "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
704
             "cpu": [
718
             "cpu": [
705
                 "ppc64"
719
                 "ppc64"
706
             ],
720
             ],
712
             ]
726
             ]
713
         },
727
         },
714
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
728
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
715
-            "version": "4.28.0",
716
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz",
717
-            "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==",
729
+            "version": "4.28.1",
730
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
731
+            "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
718
             "cpu": [
732
             "cpu": [
719
                 "riscv64"
733
                 "riscv64"
720
             ],
734
             ],
726
             ]
740
             ]
727
         },
741
         },
728
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
742
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
729
-            "version": "4.28.0",
730
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz",
731
-            "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==",
743
+            "version": "4.28.1",
744
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
745
+            "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
732
             "cpu": [
746
             "cpu": [
733
                 "s390x"
747
                 "s390x"
734
             ],
748
             ],
740
             ]
754
             ]
741
         },
755
         },
742
         "node_modules/@rollup/rollup-linux-x64-gnu": {
756
         "node_modules/@rollup/rollup-linux-x64-gnu": {
743
-            "version": "4.28.0",
744
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz",
745
-            "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==",
757
+            "version": "4.28.1",
758
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
759
+            "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
746
             "cpu": [
760
             "cpu": [
747
                 "x64"
761
                 "x64"
748
             ],
762
             ],
754
             ]
768
             ]
755
         },
769
         },
756
         "node_modules/@rollup/rollup-linux-x64-musl": {
770
         "node_modules/@rollup/rollup-linux-x64-musl": {
757
-            "version": "4.28.0",
758
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz",
759
-            "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==",
771
+            "version": "4.28.1",
772
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
773
+            "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
760
             "cpu": [
774
             "cpu": [
761
                 "x64"
775
                 "x64"
762
             ],
776
             ],
768
             ]
782
             ]
769
         },
783
         },
770
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
784
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
771
-            "version": "4.28.0",
772
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz",
773
-            "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==",
785
+            "version": "4.28.1",
786
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
787
+            "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
774
             "cpu": [
788
             "cpu": [
775
                 "arm64"
789
                 "arm64"
776
             ],
790
             ],
782
             ]
796
             ]
783
         },
797
         },
784
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
798
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
785
-            "version": "4.28.0",
786
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz",
787
-            "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==",
799
+            "version": "4.28.1",
800
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
801
+            "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
788
             "cpu": [
802
             "cpu": [
789
                 "ia32"
803
                 "ia32"
790
             ],
804
             ],
796
             ]
810
             ]
797
         },
811
         },
798
         "node_modules/@rollup/rollup-win32-x64-msvc": {
812
         "node_modules/@rollup/rollup-win32-x64-msvc": {
799
-            "version": "4.28.0",
800
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz",
801
-            "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==",
813
+            "version": "4.28.1",
814
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
815
+            "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
802
             "cpu": [
816
             "cpu": [
803
                 "x64"
817
                 "x64"
804
             ],
818
             ],
1043
             }
1057
             }
1044
         },
1058
         },
1045
         "node_modules/caniuse-lite": {
1059
         "node_modules/caniuse-lite": {
1046
-            "version": "1.0.30001686",
1047
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz",
1048
-            "integrity": "sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA==",
1060
+            "version": "1.0.30001687",
1061
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz",
1062
+            "integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==",
1049
             "dev": true,
1063
             "dev": true,
1050
             "funding": [
1064
             "funding": [
1051
                 {
1065
                 {
2207
             }
2221
             }
2208
         },
2222
         },
2209
         "node_modules/rollup": {
2223
         "node_modules/rollup": {
2210
-            "version": "4.28.0",
2211
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz",
2212
-            "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==",
2224
+            "version": "4.28.1",
2225
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
2226
+            "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
2213
             "dev": true,
2227
             "dev": true,
2214
             "license": "MIT",
2228
             "license": "MIT",
2215
             "dependencies": {
2229
             "dependencies": {
2223
                 "npm": ">=8.0.0"
2237
                 "npm": ">=8.0.0"
2224
             },
2238
             },
2225
             "optionalDependencies": {
2239
             "optionalDependencies": {
2226
-                "@rollup/rollup-android-arm-eabi": "4.28.0",
2227
-                "@rollup/rollup-android-arm64": "4.28.0",
2228
-                "@rollup/rollup-darwin-arm64": "4.28.0",
2229
-                "@rollup/rollup-darwin-x64": "4.28.0",
2230
-                "@rollup/rollup-freebsd-arm64": "4.28.0",
2231
-                "@rollup/rollup-freebsd-x64": "4.28.0",
2232
-                "@rollup/rollup-linux-arm-gnueabihf": "4.28.0",
2233
-                "@rollup/rollup-linux-arm-musleabihf": "4.28.0",
2234
-                "@rollup/rollup-linux-arm64-gnu": "4.28.0",
2235
-                "@rollup/rollup-linux-arm64-musl": "4.28.0",
2236
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0",
2237
-                "@rollup/rollup-linux-riscv64-gnu": "4.28.0",
2238
-                "@rollup/rollup-linux-s390x-gnu": "4.28.0",
2239
-                "@rollup/rollup-linux-x64-gnu": "4.28.0",
2240
-                "@rollup/rollup-linux-x64-musl": "4.28.0",
2241
-                "@rollup/rollup-win32-arm64-msvc": "4.28.0",
2242
-                "@rollup/rollup-win32-ia32-msvc": "4.28.0",
2243
-                "@rollup/rollup-win32-x64-msvc": "4.28.0",
2240
+                "@rollup/rollup-android-arm-eabi": "4.28.1",
2241
+                "@rollup/rollup-android-arm64": "4.28.1",
2242
+                "@rollup/rollup-darwin-arm64": "4.28.1",
2243
+                "@rollup/rollup-darwin-x64": "4.28.1",
2244
+                "@rollup/rollup-freebsd-arm64": "4.28.1",
2245
+                "@rollup/rollup-freebsd-x64": "4.28.1",
2246
+                "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
2247
+                "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
2248
+                "@rollup/rollup-linux-arm64-gnu": "4.28.1",
2249
+                "@rollup/rollup-linux-arm64-musl": "4.28.1",
2250
+                "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
2251
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
2252
+                "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
2253
+                "@rollup/rollup-linux-s390x-gnu": "4.28.1",
2254
+                "@rollup/rollup-linux-x64-gnu": "4.28.1",
2255
+                "@rollup/rollup-linux-x64-musl": "4.28.1",
2256
+                "@rollup/rollup-win32-arm64-msvc": "4.28.1",
2257
+                "@rollup/rollup-win32-ia32-msvc": "4.28.1",
2258
+                "@rollup/rollup-win32-x64-msvc": "4.28.1",
2244
                 "fsevents": "~2.3.2"
2259
                 "fsevents": "~2.3.2"
2245
             }
2260
             }
2246
         },
2261
         },

+ 174
- 0
resources/views/filament/widgets/enhanced-stats-overview-widget/enhanced-stat.blade.php Vedi File

1
+@php
2
+    use Filament\Support\Enums\IconPosition;
3
+    use Filament\Support\Facades\FilamentView;
4
+
5
+    $chartColor = $getChartColor() ?? 'gray';
6
+    $descriptionColor = $getDescriptionColor() ?? 'gray';
7
+    $descriptionIcon = $getDescriptionIcon();
8
+    $descriptionIconPosition = $getDescriptionIconPosition();
9
+    $url = $getUrl();
10
+    $tag = $url ? 'a' : 'div';
11
+    $dataChecksum = $generateDataChecksum();
12
+
13
+    $descriptionIconClasses = \Illuminate\Support\Arr::toCssClasses([
14
+        'fi-wi-stats-overview-stat-description-icon h-5 w-5',
15
+        match ($descriptionColor) {
16
+            'gray' => 'text-gray-400 dark:text-gray-500',
17
+            default => 'text-custom-500',
18
+        },
19
+    ]);
20
+
21
+    $descriptionIconStyles = \Illuminate\Support\Arr::toCssStyles([
22
+        \Filament\Support\get_color_css_variables(
23
+            $descriptionColor,
24
+            shades: [500],
25
+            alias: 'widgets::stats-overview-widget.stat.description.icon',
26
+        ) => $descriptionColor !== 'gray',
27
+    ]);
28
+
29
+    $prefixLabel = $getPrefixLabel();
30
+    $suffixLabel = $getSuffixLabel();
31
+@endphp
32
+
33
+<{!! $tag !!}
34
+    @if ($url)
35
+        {{ \Filament\Support\generate_href_html($url, $shouldOpenUrlInNewTab()) }}
36
+    @endif
37
+    {{
38
+        $getExtraAttributeBag()
39
+            ->class([
40
+                'fi-wi-stats-overview-stat relative rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10',
41
+            ])
42
+    }}
43
+>
44
+    <div class="grid gap-y-2">
45
+        <div class="flex items-center gap-x-2">
46
+            @if ($icon = $getIcon())
47
+                <x-filament::icon
48
+                    :icon="$icon"
49
+                    class="fi-wi-stats-overview-stat-icon h-5 w-5 text-gray-400 dark:text-gray-500"
50
+                />
51
+            @endif
52
+
53
+            <span
54
+                class="fi-wi-stats-overview-stat-label text-sm font-medium text-gray-500 dark:text-gray-400"
55
+            >
56
+                {{ $getLabel() }}
57
+            </span>
58
+        </div>
59
+
60
+        <div
61
+            class="fi-wi-stats-overview-stat-value text-2xl font-semibold tracking-tight text-gray-950 dark:text-white"
62
+        >
63
+            @if ($prefixLabel)
64
+                <span class="prefix-label text-base font-medium text-gray-500 dark:text-gray-400">
65
+                    {{ $prefixLabel }}
66
+                </span>
67
+            @endif
68
+
69
+            {{ $getValue() }}
70
+
71
+            @if ($suffixLabel)
72
+                <span class="suffix-label text-base font-medium text-gray-500 dark:text-gray-400">
73
+                    {{ $suffixLabel }}
74
+                </span>
75
+            @endif
76
+        </div>
77
+
78
+        @if ($description = $getDescription())
79
+            <div class="flex items-center gap-x-1">
80
+                @if ($descriptionIcon && in_array($descriptionIconPosition, [IconPosition::Before, 'before']))
81
+                    <x-filament::icon
82
+                        :icon="$descriptionIcon"
83
+                        :class="$descriptionIconClasses"
84
+                        :style="$descriptionIconStyles"
85
+                    />
86
+                @endif
87
+
88
+                <span
89
+                    @class([
90
+                        'fi-wi-stats-overview-stat-description text-sm',
91
+                        match ($descriptionColor) {
92
+                            'gray' => 'text-gray-500 dark:text-gray-400',
93
+                            default => 'fi-color-custom text-custom-600 dark:text-custom-400',
94
+                        },
95
+                        is_string($descriptionColor) ? "fi-color-{$descriptionColor}" : null,
96
+                    ])
97
+                    @style([
98
+                        \Filament\Support\get_color_css_variables(
99
+                            $descriptionColor,
100
+                            shades: [400, 600],
101
+                            alias: 'widgets::stats-overview-widget.stat.description',
102
+                        ) => $descriptionColor !== 'gray',
103
+                    ])
104
+                >
105
+                    {{ $description }}
106
+                </span>
107
+
108
+                @if ($descriptionIcon && in_array($descriptionIconPosition, [IconPosition::After, 'after']))
109
+                    <x-filament::icon
110
+                        :icon="$descriptionIcon"
111
+                        :class="$descriptionIconClasses"
112
+                        :style="$descriptionIconStyles"
113
+                    />
114
+                @endif
115
+            </div>
116
+        @endif
117
+    </div>
118
+
119
+    @if ($chart = $getChart())
120
+        {{-- An empty function to initialize the Alpine component with until it's loaded with `ax-load`. This removes the need for `x-ignore`, allowing the chart to be updated via Livewire polling. --}}
121
+        <div x-data="{ statsOverviewStatChart: function () {} }">
122
+            <div
123
+                @if (FilamentView::hasSpaMode())
124
+                    ax-load="visible"
125
+                @else
126
+                    ax-load
127
+                @endif
128
+                ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('stats-overview/stat/chart', 'filament/widgets') }}"
129
+                x-data="statsOverviewStatChart({
130
+                            dataChecksum: @js($dataChecksum),
131
+                            labels: @js(array_keys($chart)),
132
+                            values: @js(array_values($chart)),
133
+                        })"
134
+                @class([
135
+                    'fi-wi-stats-overview-stat-chart absolute inset-x-0 bottom-0 overflow-hidden rounded-b-xl',
136
+                    match ($chartColor) {
137
+                        'gray' => null,
138
+                        default => 'fi-color-custom',
139
+                    },
140
+                    is_string($chartColor) ? "fi-color-{$chartColor}" : null,
141
+                ])
142
+                @style([
143
+                    \Filament\Support\get_color_css_variables(
144
+                        $chartColor,
145
+                        shades: [50, 400, 500],
146
+                        alias: 'widgets::stats-overview-widget.stat.chart',
147
+                    ) => $chartColor !== 'gray',
148
+                ])
149
+            >
150
+                <canvas x-ref="canvas" class="h-6"></canvas>
151
+
152
+                <span
153
+                    x-ref="backgroundColorElement"
154
+                    @class([
155
+                        match ($chartColor) {
156
+                            'gray' => 'text-gray-100 dark:text-gray-800',
157
+                            default => 'text-custom-50 dark:text-custom-400/10',
158
+                        },
159
+                    ])
160
+                ></span>
161
+
162
+                <span
163
+                    x-ref="borderColorElement"
164
+                    @class([
165
+                        match ($chartColor) {
166
+                            'gray' => 'text-gray-400',
167
+                            default => 'text-custom-500 dark:text-custom-400',
168
+                        },
169
+                    ])
170
+                ></span>
171
+            </div>
172
+        </div>
173
+    @endif
174
+</{!! $tag !!}>

Loading…
Annulla
Salva