Andrew Wallo пре 10 месеци
родитељ
комит
781eb1b7c9
26 измењених фајлова са 1283 додато и 197 уклоњено
  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 Прегледај датотеку

@@ -27,4 +27,12 @@ enum BillStatus: string implements HasColor, HasLabel
27 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 Прегледај датотеку

@@ -2,20 +2,31 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Purchases;
4 4
 
5
+use App\Enums\Accounting\BillStatus;
6
+use App\Enums\Accounting\PaymentMethod;
5 7
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
8
+use App\Filament\Tables\Actions\ReplicateBulkAction;
9
+use App\Filament\Tables\Filters\DateRangeFilter;
6 10
 use App\Models\Accounting\Adjustment;
7 11
 use App\Models\Accounting\Bill;
8 12
 use App\Models\Accounting\DocumentLineItem;
13
+use App\Models\Banking\BankAccount;
9 14
 use App\Models\Common\Offering;
10
-use App\Utilities\Currency\CurrencyAccessor;
15
+use App\Utilities\Currency\CurrencyConverter;
11 16
 use Awcodes\TableRepeater\Components\TableRepeater;
12 17
 use Awcodes\TableRepeater\Header;
13 18
 use Carbon\CarbonInterface;
19
+use Closure;
14 20
 use Filament\Forms;
15 21
 use Filament\Forms\Form;
22
+use Filament\Notifications\Notification;
16 23
 use Filament\Resources\Resource;
24
+use Filament\Support\Enums\Alignment;
25
+use Filament\Support\Enums\MaxWidth;
17 26
 use Filament\Tables;
18 27
 use Filament\Tables\Table;
28
+use Illuminate\Database\Eloquent\Builder;
29
+use Illuminate\Database\Eloquent\Collection;
19 30
 use Illuminate\Database\Eloquent\Model;
20 31
 use Illuminate\Support\Carbon;
21 32
 use Illuminate\Support\Facades\Auth;
@@ -180,7 +191,7 @@ class BillResource extends Resource
180 191
                                     ->live()
181 192
                                     ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
182 193
                                         $offeringId = $state;
183
-                                        $offeringRecord = Offering::with('purchaseTaxes')->find($offeringId);
194
+                                        $offeringRecord = Offering::with(['purchaseTaxes', 'purchaseDiscounts'])->find($offeringId);
184 195
 
185 196
                                         if ($offeringRecord) {
186 197
                                             $set('description', $offeringRecord->description);
@@ -239,7 +250,7 @@ class BillResource extends Resource
239 250
                                         // Final total
240 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 256
                         Forms\Components\Grid::make(6)
@@ -295,6 +306,248 @@ class BillResource extends Resource
295 306
                 Tables\Columns\TextColumn::make('amount_due')
296 307
                     ->label('Amount Due')
297 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,4 +566,11 @@ class BillResource extends Resource
313 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 Прегледај датотеку

@@ -2,13 +2,20 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4 4
 
5
+use App\Enums\Accounting\BillStatus;
5 6
 use App\Filament\Company\Resources\Purchases\BillResource;
7
+use App\Models\Accounting\Bill;
6 8
 use Filament\Actions;
9
+use Filament\Pages\Concerns\ExposesTableToWidgets;
10
+use Filament\Resources\Components\Tab;
7 11
 use Filament\Resources\Pages\ListRecords;
8 12
 use Filament\Support\Enums\MaxWidth;
13
+use Illuminate\Database\Eloquent\Builder;
9 14
 
10 15
 class ListBills extends ListRecords
11 16
 {
17
+    use ExposesTableToWidgets;
18
+
12 19
     protected static string $resource = BillResource::class;
13 20
 
14 21
     protected function getHeaderActions(): array
@@ -18,8 +25,37 @@ class ListBills extends ListRecords
18 25
         ];
19 26
     }
20 27
 
28
+    protected function getHeaderWidgets(): array
29
+    {
30
+        return [
31
+            BillResource\Widgets\BillOverview::class,
32
+        ];
33
+    }
34
+
21 35
     public function getMaxContentWidth(): MaxWidth | string | null
22 36
     {
23 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 Прегледај датотеку

@@ -0,0 +1,69 @@
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 Прегледај датотеку

@@ -15,7 +15,6 @@ use App\Models\Accounting\DocumentLineItem;
15 15
 use App\Models\Accounting\Invoice;
16 16
 use App\Models\Banking\BankAccount;
17 17
 use App\Models\Common\Offering;
18
-use App\Utilities\Currency\CurrencyAccessor;
19 18
 use App\Utilities\Currency\CurrencyConverter;
20 19
 use Awcodes\TableRepeater\Components\TableRepeater;
21 20
 use Awcodes\TableRepeater\Header;
@@ -311,7 +310,7 @@ class InvoiceResource extends Resource
311 310
                                         // Final total
312 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 316
                         Forms\Components\Grid::make(6)

+ 2
- 12
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ListInvoices.php Прегледај датотеку

@@ -47,19 +47,9 @@ class ListInvoices extends ListRecords
47 47
             'unpaid' => Tab::make()
48 48
                 ->label('Unpaid')
49 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 54
             'draft' => Tab::make()
65 55
                 ->label('Draft')

+ 20
- 27
app/Filament/Company/Resources/Sales/InvoiceResource/Widgets/InvoiceOverview.php Прегледај датотеку

@@ -4,18 +4,16 @@ namespace App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
4 4
 
5 5
 use App\Enums\Accounting\InvoiceStatus;
6 6
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ListInvoices;
7
+use App\Filament\Widgets\EnhancedStatsOverviewWidget;
8
+use App\Utilities\Currency\CurrencyAccessor;
7 9
 use App\Utilities\Currency\CurrencyConverter;
8 10
 use Filament\Widgets\Concerns\InteractsWithPageTable;
9
-use Filament\Widgets\StatsOverviewWidget as BaseWidget;
10
-use Filament\Widgets\StatsOverviewWidget\Stat;
11 11
 use Illuminate\Support\Number;
12 12
 
13
-class InvoiceOverview extends BaseWidget
13
+class InvoiceOverview extends EnhancedStatsOverviewWidget
14 14
 {
15 15
     use InteractsWithPageTable;
16 16
 
17
-    protected static ?string $pollingInterval = null;
18
-
19 17
     protected function getTablePage(): string
20 18
     {
21 19
         return ListInvoices::class;
@@ -23,27 +21,18 @@ class InvoiceOverview extends BaseWidget
23 21
 
24 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 29
             ->clone()
40 30
             ->where('status', InvoiceStatus::Overdue)
41 31
             ->sum('amount_due');
42 32
 
43
-        $amountDueWithin30Days = $outstandingInvoices
33
+        $amountDueWithin30Days = $unpaidInvoices
44 34
             ->clone()
45
-            ->where('due_date', '>=', today())
46
-            ->where('due_date', '<=', today()->addDays(30))
35
+            ->whereBetween('due_date', [today(), today()->addMonth()])
47 36
             ->sum('amount_due');
48 37
 
49 38
         $validInvoices = $this->getPageTableQuery()
@@ -52,9 +41,9 @@ class InvoiceOverview extends BaseWidget
52 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 48
         $averageInvoiceTotal = $totalValidInvoiceCount > 0 ? $totalValidInvoiceAmount / $totalValidInvoiceCount : 0;
60 49
 
@@ -63,14 +52,18 @@ class InvoiceOverview extends BaseWidget
63 52
             ->selectRaw('AVG(TIMESTAMPDIFF(DAY, date, paid_at)) as avg_days')
64 53
             ->value('avg_days');
65 54
 
55
+        $averagePaymentTimeFormatted = Number::format($averagePaymentTime ?? 0, maxPrecision: 1);
56
+
66 57
         return [
67
-            Stat::make('Total Outstanding', CurrencyConverter::formatCentsToMoney($amountOutstanding))
58
+            EnhancedStatsOverviewWidget\EnhancedStat::make('Total Unpaid', CurrencyConverter::formatCentsToMoney($amountUnpaid))
59
+                ->suffix(CurrencyAccessor::getDefaultCurrency())
68 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 Прегледај датотеку

@@ -0,0 +1,12 @@
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 Прегледај датотеку

@@ -0,0 +1,47 @@
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 Прегледај датотеку

@@ -128,6 +128,11 @@ class Account extends Model
128 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 136
     protected static function newFactory(): Factory
132 137
     {
133 138
         return AccountFactory::new();

+ 169
- 1
app/Models/Accounting/Bill.php Прегледај датотеку

@@ -6,14 +6,24 @@ use App\Casts\MoneyCast;
6 6
 use App\Concerns\Blamable;
7 7
 use App\Concerns\CompanyOwned;
8 8
 use App\Enums\Accounting\BillStatus;
9
+use App\Enums\Accounting\JournalEntryType;
9 10
 use App\Enums\Accounting\TransactionType;
11
+use App\Filament\Company\Resources\Purchases\BillResource;
10 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 19
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12 20
 use Illuminate\Database\Eloquent\Model;
13 21
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
14 22
 use Illuminate\Database\Eloquent\Relations\MorphMany;
15 23
 use Illuminate\Database\Eloquent\Relations\MorphOne;
24
+use Illuminate\Support\Carbon;
16 25
 
26
+#[ObservedBy(BillObserver::class)]
17 27
 class Bill extends Model
18 28
 {
19 29
     use Blamable;
@@ -29,6 +39,7 @@ class Bill extends Model
29 39
         'order_number',
30 40
         'date',
31 41
         'due_date',
42
+        'paid_at',
32 43
         'status',
33 44
         'currency_code',
34 45
         'subtotal',
@@ -36,6 +47,7 @@ class Bill extends Model
36 47
         'discount_total',
37 48
         'total',
38 49
         'amount_paid',
50
+        'notes',
39 51
         'created_by',
40 52
         'updated_by',
41 53
     ];
@@ -43,6 +55,7 @@ class Bill extends Model
43 55
     protected $casts = [
44 56
         'date' => 'date',
45 57
         'due_date' => 'date',
58
+        'paid_at' => 'datetime',
46 59
         'status' => BillStatus::class,
47 60
         'subtotal' => MoneyCast::class,
48 61
         'tax_total' => MoneyCast::class,
@@ -82,12 +95,32 @@ class Bill extends Model
82 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 100
         return $this->morphOne(Transaction::class, 'transactionable')
88 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 124
     public static function getNextDocumentNumber(): string
92 125
     {
93 126
         $company = auth()->user()->currentCompany;
@@ -120,4 +153,139 @@ class Bill extends Model
120 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 Прегледај датотеку

@@ -17,6 +17,7 @@ use Filament\Actions\MountableAction;
17 17
 use Filament\Actions\ReplicateAction;
18 18
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
19 19
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
20
+use Illuminate\Database\Eloquent\Builder;
20 21
 use Illuminate\Database\Eloquent\Casts\Attribute;
21 22
 use Illuminate\Database\Eloquent\Factories\HasFactory;
22 23
 use Illuminate\Database\Eloquent\Model;
@@ -112,6 +113,16 @@ class Invoice extends Model
112 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 126
     protected function isCurrentlyOverdue(): Attribute
116 127
     {
117 128
         return Attribute::get(function () {
@@ -297,7 +308,21 @@ class Invoice extends Model
297 308
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
298 309
     {
299 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 326
             ->modal(false)
302 327
             ->beforeReplicaSaved(function (self $original, self $replica) {
303 328
                 $replica->status = InvoiceStatus::Draft;

+ 40
- 0
app/Observers/BillObserver.php Прегледај датотеку

@@ -0,0 +1,40 @@
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 Прегледај датотеку

@@ -10,27 +10,6 @@ use Illuminate\Support\Facades\DB;
10 10
 
11 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 13
     public function saving(Invoice $invoice): void
35 14
     {
36 15
         if ($invoice->approved_at && $invoice->is_currently_overdue) {
@@ -53,20 +32,4 @@ class InvoiceObserver
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 Прегледај датотеку

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace App\Observers;
4 4
 
5
+use App\Enums\Accounting\BillStatus;
5 6
 use App\Enums\Accounting\InvoiceStatus;
7
+use App\Models\Accounting\Bill;
6 8
 use App\Models\Accounting\Invoice;
7 9
 use App\Models\Accounting\Transaction;
8 10
 use App\Services\TransactionService;
@@ -37,10 +39,12 @@ class TransactionObserver
37 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,10 +61,12 @@ class TransactionObserver
57 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,14 +82,16 @@ class TransactionObserver
76 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 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,4 +135,33 @@ class TransactionObserver
127 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 Прегледај датотеку

@@ -204,10 +204,6 @@ class MacroServiceProvider extends ServiceProvider
204 204
 
205 205
             $currencyCode = $this->currency->getCurrency();
206 206
 
207
-            if ($currencyCode === CurrencyAccessor::getDefaultCurrency()) {
208
-                return $formatted;
209
-            }
210
-
211 207
             if ($codeBefore) {
212 208
                 return $currencyCode . ' ' . $formatted;
213 209
             }

+ 8
- 2
app/Utilities/Currency/CurrencyConverter.php Прегледај датотеку

@@ -49,11 +49,17 @@ class CurrencyConverter
49 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 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 65
     public static function formatToMoney(string | float $amount, ?string $currency = null): string

+ 6
- 6
composer.lock Прегледај датотеку

@@ -5064,16 +5064,16 @@
5064 5064
         },
5065 5065
         {
5066 5066
             "name": "openspout/openspout",
5067
-            "version": "v4.28.1",
5067
+            "version": "v4.28.2",
5068 5068
             "source": {
5069 5069
                 "type": "git",
5070 5070
                 "url": "https://github.com/openspout/openspout.git",
5071
-                "reference": "229a9c837bd768e8767660671f9fdf429f343a74"
5071
+                "reference": "d6dd654b5db502f28c5773edfa785b516745a142"
5072 5072
             },
5073 5073
             "dist": {
5074 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 5077
                 "shasum": ""
5078 5078
             },
5079 5079
             "require": {
@@ -5141,7 +5141,7 @@
5141 5141
             ],
5142 5142
             "support": {
5143 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 5146
             "funding": [
5147 5147
                 {
@@ -5153,7 +5153,7 @@
5153 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 5159
             "name": "paragonie/constant_time_encoding",

+ 159
- 2
database/factories/Accounting/BillFactory.php Прегледај датотеку

@@ -2,13 +2,26 @@
2 2
 
3 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 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 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 26
      * Define the model's default state.
14 27
      *
@@ -16,8 +29,152 @@ class BillFactory extends Factory
16 29
      */
17 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 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 Прегледај датотеку

@@ -23,30 +23,67 @@ class DocumentLineItemFactory extends Factory
23 23
      */
24 24
     public function definition(): array
25 25
     {
26
-        $offering = Offering::with(['salesTaxes', 'salesDiscounts'])->inRandomOrder()->first();
27
-
28 26
         $quantity = $this->faker->numberBetween(1, 10);
29
-        $unitPrice = $offering->price;
30 27
 
31 28
         return [
32 29
             'company_id' => 1,
33
-            'offering_id' => $offering->id,
34 30
             'description' => $this->faker->sentence,
35 31
             'quantity' => $quantity,
36
-            'unit_price' => $unitPrice,
37 32
             'created_by' => 1,
38 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 82
             $offering = $lineItem->offering;
46 83
 
47 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 89
             $lineItem->refresh();

+ 2
- 2
database/factories/Accounting/InvoiceFactory.php Прегледај датотеку

@@ -51,7 +51,7 @@ class InvoiceFactory extends Factory
51 51
 
52 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 57
     public function approved(): static
@@ -126,7 +126,7 @@ class InvoiceFactory extends Factory
126 126
             if ($invoiceStatus === InvoiceStatus::Paid) {
127 127
                 $latestPaymentDate = max($paymentDates);
128 128
                 $invoice->updateQuietly([
129
-                    'status' => InvoiceStatus::Paid,
129
+                    'status' => $invoiceStatus,
130 130
                     'paid_at' => $latestPaymentDate,
131 131
                 ]);
132 132
             }

+ 46
- 0
database/factories/CompanyFactory.php Прегледај датотеку

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace Database\Factories;
4 4
 
5
+use App\Enums\Accounting\BillStatus;
5 6
 use App\Enums\Accounting\InvoiceStatus;
7
+use App\Models\Accounting\Bill;
6 8
 use App\Models\Accounting\Invoice;
7 9
 use App\Models\Accounting\Transaction;
8 10
 use App\Models\Common\Client;
@@ -160,4 +162,48 @@ class CompanyFactory extends Factory
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 Прегледај датотеку

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

+ 2
- 1
database/seeders/DatabaseSeeder.php Прегледај датотеку

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

+ 93
- 78
package-lock.json Прегледај датотеку

@@ -558,9 +558,9 @@
558 558
             }
559 559
         },
560 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 564
             "cpu": [
565 565
                 "arm"
566 566
             ],
@@ -572,9 +572,9 @@
572 572
             ]
573 573
         },
574 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 578
             "cpu": [
579 579
                 "arm64"
580 580
             ],
@@ -586,9 +586,9 @@
586 586
             ]
587 587
         },
588 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 592
             "cpu": [
593 593
                 "arm64"
594 594
             ],
@@ -600,9 +600,9 @@
600 600
             ]
601 601
         },
602 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 606
             "cpu": [
607 607
                 "x64"
608 608
             ],
@@ -614,9 +614,9 @@
614 614
             ]
615 615
         },
616 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 620
             "cpu": [
621 621
                 "arm64"
622 622
             ],
@@ -628,9 +628,9 @@
628 628
             ]
629 629
         },
630 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 634
             "cpu": [
635 635
                 "x64"
636 636
             ],
@@ -642,9 +642,9 @@
642 642
             ]
643 643
         },
644 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 648
             "cpu": [
649 649
                 "arm"
650 650
             ],
@@ -656,9 +656,9 @@
656 656
             ]
657 657
         },
658 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 662
             "cpu": [
663 663
                 "arm"
664 664
             ],
@@ -670,9 +670,9 @@
670 670
             ]
671 671
         },
672 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 676
             "cpu": [
677 677
                 "arm64"
678 678
             ],
@@ -684,9 +684,9 @@
684 684
             ]
685 685
         },
686 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 690
             "cpu": [
691 691
                 "arm64"
692 692
             ],
@@ -697,10 +697,24 @@
697 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 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 718
             "cpu": [
705 719
                 "ppc64"
706 720
             ],
@@ -712,9 +726,9 @@
712 726
             ]
713 727
         },
714 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 732
             "cpu": [
719 733
                 "riscv64"
720 734
             ],
@@ -726,9 +740,9 @@
726 740
             ]
727 741
         },
728 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 746
             "cpu": [
733 747
                 "s390x"
734 748
             ],
@@ -740,9 +754,9 @@
740 754
             ]
741 755
         },
742 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 760
             "cpu": [
747 761
                 "x64"
748 762
             ],
@@ -754,9 +768,9 @@
754 768
             ]
755 769
         },
756 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 774
             "cpu": [
761 775
                 "x64"
762 776
             ],
@@ -768,9 +782,9 @@
768 782
             ]
769 783
         },
770 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 788
             "cpu": [
775 789
                 "arm64"
776 790
             ],
@@ -782,9 +796,9 @@
782 796
             ]
783 797
         },
784 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 802
             "cpu": [
789 803
                 "ia32"
790 804
             ],
@@ -796,9 +810,9 @@
796 810
             ]
797 811
         },
798 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 816
             "cpu": [
803 817
                 "x64"
804 818
             ],
@@ -1043,9 +1057,9 @@
1043 1057
             }
1044 1058
         },
1045 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 1063
             "dev": true,
1050 1064
             "funding": [
1051 1065
                 {
@@ -2207,9 +2221,9 @@
2207 2221
             }
2208 2222
         },
2209 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 2227
             "dev": true,
2214 2228
             "license": "MIT",
2215 2229
             "dependencies": {
@@ -2223,24 +2237,25 @@
2223 2237
                 "npm": ">=8.0.0"
2224 2238
             },
2225 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 2259
                 "fsevents": "~2.3.2"
2245 2260
             }
2246 2261
         },

+ 174
- 0
resources/views/filament/widgets/enhanced-stats-overview-widget/enhanced-stat.blade.php Прегледај датотеку

@@ -0,0 +1,174 @@
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…
Откажи
Сачувај