Przeglądaj źródła

Merge pull request #169 from andrewdwallo/development-3.x

Development 3.x
3.x
Andrew Wallo 4 miesięcy temu
rodzic
commit
59a13df4f2
No account linked to committer's email address
38 zmienionych plików z 2053 dodań i 1352 usunięć
  1. 49
    26
      app/Concerns/HasJournalEntryActions.php
  2. 390
    0
      app/Concerns/HasTransactionAction.php
  3. 1
    1
      app/DTO/AccountTransactionDTO.php
  4. 89
    0
      app/Filament/Actions/CreateTransactionAction.php
  5. 81
    0
      app/Filament/Actions/EditTransactionAction.php
  6. 32
    36
      app/Filament/Company/Pages/Accounting/AccountChart.php
  7. 0
    880
      app/Filament/Company/Pages/Accounting/Transactions.php
  8. 8
    8
      app/Filament/Company/Pages/Reports/AccountTransactions.php
  9. 1
    3
      app/Filament/Company/Pages/Reports/BaseReportPage.php
  10. 324
    0
      app/Filament/Company/Resources/Accounting/TransactionResource.php
  11. 63
    0
      app/Filament/Company/Resources/Accounting/TransactionResource/Pages/ListTransactions.php
  12. 162
    0
      app/Filament/Company/Resources/Accounting/TransactionResource/Pages/ViewTransaction.php
  13. 62
    0
      app/Filament/Company/Resources/Accounting/TransactionResource/RelationManagers/JournalEntriesRelationManager.php
  14. 3
    7
      app/Filament/Company/Resources/Purchases/BillResource/Pages/ViewBill.php
  15. 3
    4
      app/Filament/Company/Resources/Purchases/VendorResource/Pages/ViewVendor.php
  16. 3
    4
      app/Filament/Company/Resources/Sales/ClientResource/Pages/ViewClient.php
  17. 3
    7
      app/Filament/Company/Resources/Sales/EstimateResource/Pages/ViewEstimate.php
  18. 3
    8
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ViewInvoice.php
  19. 4
    5
      app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php
  20. 81
    0
      app/Filament/Tables/Actions/EditTransactionAction.php
  21. 169
    0
      app/Models/Accounting/Transaction.php
  22. 6
    0
      app/Models/Common/Client.php
  23. 7
    0
      app/Models/Common/Vendor.php
  24. 14
    0
      app/Observers/TransactionObserver.php
  25. 69
    0
      app/Policies/TransactionPolicy.php
  26. 5
    6
      app/Providers/Filament/CompanyPanelProvider.php
  27. 14
    0
      app/Providers/MacroServiceProvider.php
  28. 7
    7
      app/Services/AccountService.php
  29. 1
    27
      app/Services/ReportService.php
  30. 1
    1
      app/Transformers/AccountTransactionReportTransformer.php
  31. 18
    18
      composer.lock
  32. 30
    0
      database/migrations/2025_05_17_194711_add_payeeable_to_transactions_table.php
  33. 303
    212
      package-lock.json
  34. 4
    0
      resources/css/filament/company/theme.css
  35. 12
    30
      resources/views/components/company/tables/reports/account-transactions.blade.php
  36. 10
    10
      resources/views/filament/company/pages/accounting/chart.blade.php
  37. 0
    3
      resources/views/filament/company/pages/accounting/transactions.blade.php
  38. 21
    49
      tests/Feature/Accounting/TransactionTest.php

+ 49
- 26
app/Concerns/HasJournalEntryActions.php Wyświetl plik

@@ -4,6 +4,7 @@ namespace App\Concerns;
4 4
 
5 5
 use App\Enums\Accounting\JournalEntryType;
6 6
 use App\Utilities\Currency\CurrencyAccessor;
7
+use Filament\Tables\Actions\Action;
7 8
 
8 9
 trait HasJournalEntryActions
9 10
 {
@@ -104,7 +105,8 @@ trait HasJournalEntryActions
104 105
      */
105 106
     public function resetJournalEntryAmounts(): void
106 107
     {
107
-        $this->reset(['debitAmount', 'creditAmount']);
108
+        $this->debitAmount = 0;
109
+        $this->creditAmount = 0;
108 110
     }
109 111
 
110 112
     public function adjustJournalEntryAmountsForTypeChange(JournalEntryType $newType, JournalEntryType $oldType, ?string $amount): void
@@ -113,19 +115,29 @@ trait HasJournalEntryActions
113 115
             return;
114 116
         }
115 117
 
116
-        $amountComplete = $this->ensureCompleteDecimal($amount);
117
-        $normalizedAmount = $this->convertAmountToCents($amountComplete);
118
-
119
-        if ($normalizedAmount === 0) {
120
-            return;
121
-        }
122
-
123
-        if ($oldType->isDebit() && $newType->isCredit()) {
124
-            $this->debitAmount -= $normalizedAmount;
125
-            $this->creditAmount += $normalizedAmount;
126
-        } elseif ($oldType->isCredit() && $newType->isDebit()) {
127
-            $this->debitAmount += $normalizedAmount;
128
-            $this->creditAmount -= $normalizedAmount;
118
+        $entries = $this instanceof Action
119
+            ? ($this->getLivewire()->mountedTableActionsData[0]['journalEntries'] ?? [])
120
+            : ($this->getLivewire()->mountedActionsData[0]['journalEntries'] ?? []);
121
+
122
+        // Reset the totals
123
+        $this->debitAmount = 0;
124
+        $this->creditAmount = 0;
125
+
126
+        // Recalculate totals from all entries
127
+        foreach ($entries as $entry) {
128
+            if (empty($entry['type']) || empty($entry['amount'])) {
129
+                continue;
130
+            }
131
+
132
+            $entryType = JournalEntryType::parse($entry['type']);
133
+            $entryAmount = $this->ensureCompleteDecimal($entry['amount']);
134
+            $formattedAmount = $this->convertAmountToCents($entryAmount);
135
+
136
+            if ($entryType->isDebit()) {
137
+                $this->debitAmount += $formattedAmount;
138
+            } else {
139
+                $this->creditAmount += $formattedAmount;
140
+            }
129 141
         }
130 142
     }
131 143
 
@@ -140,18 +152,29 @@ trait HasJournalEntryActions
140 152
             return;
141 153
         }
142 154
 
143
-        $newAmountComplete = $this->ensureCompleteDecimal($newAmount);
144
-        $oldAmountComplete = $this->ensureCompleteDecimal($oldAmount);
145
-
146
-        $formattedNewAmount = $this->convertAmountToCents($newAmountComplete);
147
-        $formattedOldAmount = $this->convertAmountToCents($oldAmountComplete);
148
-
149
-        $difference = $formattedNewAmount - $formattedOldAmount;
150
-
151
-        if ($journalEntryType->isDebit()) {
152
-            $this->debitAmount += $difference;
153
-        } else {
154
-            $this->creditAmount += $difference;
155
+        $entries = $this instanceof Action
156
+            ? ($this->getLivewire()->mountedTableActionsData[0]['journalEntries'] ?? [])
157
+            : ($this->getLivewire()->mountedActionsData[0]['journalEntries'] ?? []);
158
+
159
+        // Reset the totals
160
+        $this->debitAmount = 0;
161
+        $this->creditAmount = 0;
162
+
163
+        // Recalculate totals from all entries
164
+        foreach ($entries as $entry) {
165
+            if (empty($entry['type']) || empty($entry['amount'])) {
166
+                continue;
167
+            }
168
+
169
+            $entryType = JournalEntryType::parse($entry['type']);
170
+            $entryAmount = $this->ensureCompleteDecimal($entry['amount']);
171
+            $formattedAmount = $this->convertAmountToCents($entryAmount);
172
+
173
+            if ($entryType->isDebit()) {
174
+                $this->debitAmount += $formattedAmount;
175
+            } else {
176
+                $this->creditAmount += $formattedAmount;
177
+            }
155 178
         }
156 179
     }
157 180
 

+ 390
- 0
app/Concerns/HasTransactionAction.php Wyświetl plik

@@ -0,0 +1,390 @@
1
+<?php
2
+
3
+namespace App\Concerns;
4
+
5
+use App\Enums\Accounting\JournalEntryType;
6
+use App\Enums\Accounting\TransactionType;
7
+use App\Filament\Forms\Components\CustomTableRepeater;
8
+use App\Models\Accounting\JournalEntry;
9
+use App\Models\Accounting\Transaction;
10
+use App\Models\Banking\BankAccount;
11
+use App\Utilities\Currency\CurrencyAccessor;
12
+use App\Utilities\Currency\CurrencyConverter;
13
+use Awcodes\TableRepeater\Header;
14
+use Closure;
15
+use Filament\Forms;
16
+use Filament\Forms\Components\Actions\Action as FormAction;
17
+use Filament\Forms\Form;
18
+use Illuminate\Contracts\View\View;
19
+use Illuminate\Support\Str;
20
+
21
+trait HasTransactionAction
22
+{
23
+    use HasJournalEntryActions;
24
+
25
+    protected TransactionType | Closure | null $transactionType = null;
26
+
27
+    public function type(TransactionType | Closure | null $type = null): static
28
+    {
29
+        $this->transactionType = $type;
30
+
31
+        return $this;
32
+    }
33
+
34
+    public function getTransactionType(): ?TransactionType
35
+    {
36
+        return $this->evaluate($this->transactionType);
37
+    }
38
+
39
+    protected function getFormDefaultsForType(TransactionType $type): array
40
+    {
41
+        $commonDefaults = [
42
+            'posted_at' => today(),
43
+        ];
44
+
45
+        return match ($type) {
46
+            TransactionType::Deposit, TransactionType::Withdrawal, TransactionType::Transfer => array_merge($commonDefaults, $this->transactionDefaults($type)),
47
+            TransactionType::Journal => array_merge($commonDefaults, $this->journalEntryDefaults()),
48
+        };
49
+    }
50
+
51
+    protected function journalEntryDefaults(): array
52
+    {
53
+        return [
54
+            'journalEntries' => [
55
+                $this->defaultEntry(JournalEntryType::Debit),
56
+                $this->defaultEntry(JournalEntryType::Credit),
57
+            ],
58
+        ];
59
+    }
60
+
61
+    protected function defaultEntry(JournalEntryType $journalEntryType): array
62
+    {
63
+        return [
64
+            'type' => $journalEntryType,
65
+            'account_id' => Transaction::getUncategorizedAccountByType($journalEntryType->isDebit() ? TransactionType::Withdrawal : TransactionType::Deposit)?->id,
66
+            'amount' => '0.00',
67
+        ];
68
+    }
69
+
70
+    protected function transactionDefaults(TransactionType $type): array
71
+    {
72
+        return [
73
+            'type' => $type,
74
+            'bank_account_id' => BankAccount::where('enabled', true)->first()?->id,
75
+            'amount' => '0.00',
76
+            'account_id' => ! $type->isTransfer() ? Transaction::getUncategorizedAccountByType($type)->id : null,
77
+        ];
78
+    }
79
+
80
+    public function transactionForm(Form $form): Form
81
+    {
82
+        return $form
83
+            ->schema([
84
+                Forms\Components\DatePicker::make('posted_at')
85
+                    ->label('Date')
86
+                    ->required(),
87
+                Forms\Components\TextInput::make('description')
88
+                    ->label('Description'),
89
+                Forms\Components\Select::make('bank_account_id')
90
+                    ->label('Account')
91
+                    ->options(fn (?Transaction $transaction) => Transaction::getBankAccountOptions(currentBankAccountId: $transaction?->bank_account_id))
92
+                    ->live()
93
+                    ->searchable()
94
+                    ->afterStateUpdated(function (Forms\Set $set, $state, $old, Forms\Get $get) {
95
+                        $amount = CurrencyConverter::convertAndSet(
96
+                            BankAccount::find($state)->account->currency_code,
97
+                            BankAccount::find($old)->account->currency_code ?? CurrencyAccessor::getDefaultCurrency(),
98
+                            $get('amount')
99
+                        );
100
+
101
+                        if ($amount !== null) {
102
+                            $set('amount', $amount);
103
+                        }
104
+                    })
105
+                    ->required(),
106
+                Forms\Components\Select::make('type')
107
+                    ->label('Type')
108
+                    ->live()
109
+                    ->options([
110
+                        TransactionType::Deposit->value => TransactionType::Deposit->getLabel(),
111
+                        TransactionType::Withdrawal->value => TransactionType::Withdrawal->getLabel(),
112
+                    ])
113
+                    ->required()
114
+                    ->afterStateUpdated(static fn (Forms\Set $set, $state) => $set('account_id', Transaction::getUncategorizedAccountByType(TransactionType::parse($state))?->id)),
115
+                Forms\Components\TextInput::make('amount')
116
+                    ->label('Amount')
117
+                    ->money(static fn (Forms\Get $get) => BankAccount::find($get('bank_account_id'))?->account?->currency_code ?? CurrencyAccessor::getDefaultCurrency())
118
+                    ->required(),
119
+                Forms\Components\Select::make('account_id')
120
+                    ->label('Category')
121
+                    ->options(fn (Forms\Get $get, ?Transaction $transaction) => Transaction::getTransactionAccountOptions(type: TransactionType::parse($get('type')), currentAccountId: $transaction?->account_id))
122
+                    ->searchable()
123
+                    ->required(),
124
+                Forms\Components\Textarea::make('notes')
125
+                    ->label('Notes')
126
+                    ->autosize()
127
+                    ->rows(10)
128
+                    ->columnSpanFull(),
129
+            ])
130
+            ->columns();
131
+    }
132
+
133
+    public function transferForm(Form $form): Form
134
+    {
135
+        return $form
136
+            ->schema([
137
+                Forms\Components\DatePicker::make('posted_at')
138
+                    ->label('Date')
139
+                    ->required(),
140
+                Forms\Components\TextInput::make('description')
141
+                    ->label('Description'),
142
+                Forms\Components\Select::make('bank_account_id')
143
+                    ->label('From account')
144
+                    ->options(fn (Forms\Get $get, ?Transaction $transaction) => Transaction::getBankAccountOptions(excludedAccountId: $get('account_id'), currentBankAccountId: $transaction?->bank_account_id))
145
+                    ->live()
146
+                    ->searchable()
147
+                    ->afterStateUpdated(function (Forms\Set $set, $state, $old, Forms\Get $get) {
148
+                        $amount = CurrencyConverter::convertAndSet(
149
+                            BankAccount::find($state)->account->currency_code,
150
+                            BankAccount::find($old)->account->currency_code ?? CurrencyAccessor::getDefaultCurrency(),
151
+                            $get('amount')
152
+                        );
153
+
154
+                        if ($amount !== null) {
155
+                            $set('amount', $amount);
156
+                        }
157
+                    })
158
+                    ->required(),
159
+                Forms\Components\Select::make('type')
160
+                    ->label('Type')
161
+                    ->options([
162
+                        TransactionType::Transfer->value => TransactionType::Transfer->getLabel(),
163
+                    ])
164
+                    ->disabled()
165
+                    ->dehydrated()
166
+                    ->required(),
167
+                Forms\Components\TextInput::make('amount')
168
+                    ->label('Amount')
169
+                    ->money(static fn (Forms\Get $get) => BankAccount::find($get('bank_account_id'))?->account?->currency_code ?? CurrencyAccessor::getDefaultCurrency())
170
+                    ->required(),
171
+                Forms\Components\Select::make('account_id')
172
+                    ->label('To account')
173
+                    ->live()
174
+                    ->options(fn (Forms\Get $get, ?Transaction $transaction) => Transaction::getBankAccountAccountOptions(excludedBankAccountId: $get('bank_account_id'), currentAccountId: $transaction?->account_id))
175
+                    ->searchable()
176
+                    ->required(),
177
+                Forms\Components\Textarea::make('notes')
178
+                    ->label('Notes')
179
+                    ->autosize()
180
+                    ->rows(10)
181
+                    ->columnSpanFull(),
182
+            ])
183
+            ->columns();
184
+    }
185
+
186
+    public function journalTransactionForm(Form $form): Form
187
+    {
188
+        return $form
189
+            ->schema([
190
+                Forms\Components\Tabs::make('Tabs')
191
+                    ->contained(false)
192
+                    ->tabs([
193
+                        $this->getJournalTransactionFormEditTab(),
194
+                        $this->getJournalTransactionFormNotesTab(),
195
+                    ]),
196
+            ])
197
+            ->columns(1);
198
+    }
199
+
200
+    protected function getJournalTransactionFormEditTab(): Forms\Components\Tabs\Tab
201
+    {
202
+        return Forms\Components\Tabs\Tab::make('Edit')
203
+            ->label('Edit')
204
+            ->icon('heroicon-o-pencil-square')
205
+            ->schema([
206
+                $this->getTransactionDetailsGrid(),
207
+                $this->getJournalEntriesTableRepeater(),
208
+            ]);
209
+    }
210
+
211
+    protected function getJournalTransactionFormNotesTab(): Forms\Components\Tabs\Tab
212
+    {
213
+        return Forms\Components\Tabs\Tab::make('Notes')
214
+            ->label('Notes')
215
+            ->icon('heroicon-o-clipboard')
216
+            ->id('notes')
217
+            ->schema([
218
+                $this->getTransactionDetailsGrid(),
219
+                Forms\Components\Textarea::make('notes')
220
+                    ->label('Notes')
221
+                    ->rows(10)
222
+                    ->autosize(),
223
+            ]);
224
+    }
225
+
226
+    protected function getTransactionDetailsGrid(): Forms\Components\Grid
227
+    {
228
+        return Forms\Components\Grid::make(8)
229
+            ->schema([
230
+                Forms\Components\DatePicker::make('posted_at')
231
+                    ->label('Date')
232
+                    ->softRequired()
233
+                    ->displayFormat('Y-m-d'),
234
+                Forms\Components\TextInput::make('description')
235
+                    ->label('Description')
236
+                    ->columnSpan(2),
237
+            ]);
238
+    }
239
+
240
+    protected function getJournalEntriesTableRepeater(): CustomTableRepeater
241
+    {
242
+        return CustomTableRepeater::make('journalEntries')
243
+            ->relationship('journalEntries')
244
+            ->hiddenLabel()
245
+            ->columns(4)
246
+            ->headers($this->getJournalEntriesTableRepeaterHeaders())
247
+            ->schema($this->getJournalEntriesTableRepeaterSchema())
248
+            ->deletable(fn (CustomTableRepeater $repeater) => $repeater->getItemsCount() > 2)
249
+            ->deleteAction(function (Forms\Components\Actions\Action $action) {
250
+                return $action
251
+                    ->action(function (array $arguments, CustomTableRepeater $component): void {
252
+                        $items = $component->getState();
253
+
254
+                        $amount = $items[$arguments['item']]['amount'];
255
+                        $type = $items[$arguments['item']]['type'];
256
+
257
+                        $this->updateJournalEntryAmount(JournalEntryType::parse($type), '0.00', $amount);
258
+
259
+                        unset($items[$arguments['item']]);
260
+
261
+                        $component->state($items);
262
+
263
+                        $component->callAfterStateUpdated();
264
+                    });
265
+            })
266
+            ->rules([
267
+                function () {
268
+                    return function (string $attribute, $value, \Closure $fail) {
269
+                        if (empty($value) || ! is_array($value)) {
270
+                            $fail('Journal entries are required.');
271
+
272
+                            return;
273
+                        }
274
+
275
+                        $hasDebit = false;
276
+                        $hasCredit = false;
277
+
278
+                        foreach ($value as $entry) {
279
+                            if (! isset($entry['type'])) {
280
+                                continue;
281
+                            }
282
+
283
+                            if (JournalEntryType::parse($entry['type'])->isDebit()) {
284
+                                $hasDebit = true;
285
+                            } elseif (JournalEntryType::parse($entry['type'])->isCredit()) {
286
+                                $hasCredit = true;
287
+                            }
288
+
289
+                            if ($hasDebit && $hasCredit) {
290
+                                break;
291
+                            }
292
+                        }
293
+
294
+                        if (! $hasDebit) {
295
+                            $fail('At least one debit entry is required.');
296
+                        }
297
+
298
+                        if (! $hasCredit) {
299
+                            $fail('At least one credit entry is required.');
300
+                        }
301
+                    };
302
+                },
303
+            ])
304
+            ->minItems(2)
305
+            ->defaultItems(2)
306
+            ->addable(false)
307
+            ->footerItem(fn (): View => $this->getJournalTransactionModalFooter())
308
+            ->extraActions([
309
+                $this->buildAddJournalEntryAction(JournalEntryType::Debit),
310
+                $this->buildAddJournalEntryAction(JournalEntryType::Credit),
311
+            ]);
312
+    }
313
+
314
+    protected function getJournalEntriesTableRepeaterHeaders(): array
315
+    {
316
+        return [
317
+            Header::make('type')
318
+                ->width('150px')
319
+                ->label('Type'),
320
+            Header::make('description')
321
+                ->width('320px')
322
+                ->label('Description'),
323
+            Header::make('account_id')
324
+                ->width('320px')
325
+                ->label('Account'),
326
+            Header::make('amount')
327
+                ->width('192px')
328
+                ->label('Amount'),
329
+        ];
330
+    }
331
+
332
+    protected function getJournalEntriesTableRepeaterSchema(): array
333
+    {
334
+        return [
335
+            Forms\Components\Select::make('type')
336
+                ->label('Type')
337
+                ->options(JournalEntryType::class)
338
+                ->live()
339
+                ->afterStateUpdated(function (Forms\Get $get, Forms\Set $set, $state, $old) {
340
+                    $this->adjustJournalEntryAmountsForTypeChange(JournalEntryType::parse($state), JournalEntryType::parse($old), $get('amount'));
341
+                })
342
+                ->softRequired(),
343
+            Forms\Components\TextInput::make('description')
344
+                ->label('Description'),
345
+            Forms\Components\Select::make('account_id')
346
+                ->label('Account')
347
+                ->options(fn (?JournalEntry $journalEntry): array => Transaction::getJournalAccountOptions(currentAccountId: $journalEntry?->account_id))
348
+                ->softRequired()
349
+                ->searchable(),
350
+            Forms\Components\TextInput::make('amount')
351
+                ->label('Amount')
352
+                ->live()
353
+                ->mask(moneyMask(CurrencyAccessor::getDefaultCurrency()))
354
+                ->afterStateUpdated(function (Forms\Get $get, Forms\Set $set, ?string $state, ?string $old) {
355
+                    $this->updateJournalEntryAmount(JournalEntryType::parse($get('type')), $state, $old);
356
+                })
357
+                ->softRequired(),
358
+        ];
359
+    }
360
+
361
+    protected function buildAddJournalEntryAction(JournalEntryType $type): FormAction
362
+    {
363
+        $typeLabel = $type->getLabel();
364
+
365
+        return FormAction::make("add{$typeLabel}Entry")
366
+            ->button()
367
+            ->outlined()
368
+            ->color($type->isDebit() ? 'primary' : 'gray')
369
+            ->action(function (CustomTableRepeater $component) use ($type) {
370
+                $state = $component->getState();
371
+                $newUuid = (string) Str::uuid();
372
+                $state[$newUuid] = $this->defaultEntry($type);
373
+
374
+                $component->state($state);
375
+            });
376
+    }
377
+
378
+    public function getJournalTransactionModalFooter(): View
379
+    {
380
+        return view(
381
+            'filament.company.components.actions.journal-entry-footer',
382
+            [
383
+                'debitAmount' => $this->getFormattedDebitAmount(),
384
+                'creditAmount' => $this->getFormattedCreditAmount(),
385
+                'difference' => $this->getFormattedBalanceDifference(),
386
+                'isJournalBalanced' => $this->isJournalEntryBalanced(),
387
+            ],
388
+        );
389
+    }
390
+}

+ 1
- 1
app/DTO/AccountTransactionDTO.php Wyświetl plik

@@ -14,6 +14,6 @@ class AccountTransactionDTO
14 14
         public string $credit,
15 15
         public string $balance,
16 16
         public ?TransactionType $type,
17
-        public ?array $tableAction,
17
+        public ?string $url = null,
18 18
     ) {}
19 19
 }

+ 89
- 0
app/Filament/Actions/CreateTransactionAction.php Wyświetl plik

@@ -0,0 +1,89 @@
1
+<?php
2
+
3
+namespace App\Filament\Actions;
4
+
5
+use App\Concerns\HasTransactionAction;
6
+use App\Enums\Accounting\TransactionType;
7
+use App\Models\Accounting\Transaction;
8
+use Filament\Actions\CreateAction;
9
+use Filament\Actions\StaticAction;
10
+use Filament\Forms\Form;
11
+use Filament\Support\Enums\MaxWidth;
12
+
13
+class CreateTransactionAction extends CreateAction
14
+{
15
+    use HasTransactionAction;
16
+
17
+    protected function setUp(): void
18
+    {
19
+        parent::setUp();
20
+
21
+        $this->label(null);
22
+
23
+        $this->groupedIcon(null);
24
+
25
+        $this->slideOver();
26
+
27
+        $this->modalWidth(function (): MaxWidth {
28
+            return match ($this->getTransactionType()) {
29
+                TransactionType::Journal => MaxWidth::Screen,
30
+                default => MaxWidth::ThreeExtraLarge,
31
+            };
32
+        });
33
+
34
+        $this->extraModalWindowAttributes(function (): array {
35
+            if ($this->getTransactionType() === TransactionType::Journal) {
36
+                return ['class' => 'journal-transaction-modal'];
37
+            }
38
+
39
+            return [];
40
+        });
41
+
42
+        $this->modalHeading(function (): string {
43
+            return match ($this->getTransactionType()) {
44
+                TransactionType::Journal => 'Create journal entry',
45
+                default => 'Create transaction',
46
+            };
47
+        });
48
+
49
+        $this->fillForm(fn (): array => $this->getFormDefaultsForType($this->getTransactionType()));
50
+
51
+        $this->form(function (Form $form) {
52
+            return match ($this->getTransactionType()) {
53
+                TransactionType::Transfer => $this->transferForm($form),
54
+                TransactionType::Journal => $this->journalTransactionForm($form),
55
+                default => $this->transactionForm($form),
56
+            };
57
+        });
58
+
59
+        $this->afterFormFilled(function () {
60
+            if ($this->getTransactionType() === TransactionType::Journal) {
61
+                $this->resetJournalEntryAmounts();
62
+            }
63
+        });
64
+
65
+        $this->modalSubmitAction(function (StaticAction $action) {
66
+            if ($this->getTransactionType() === TransactionType::Journal) {
67
+                $action->disabled(! $this->isJournalEntryBalanced());
68
+            }
69
+
70
+            return $action;
71
+        });
72
+
73
+        $this->after(function (Transaction $transaction) {
74
+            if ($this->getTransactionType() === TransactionType::Journal) {
75
+                $transaction->updateAmountIfBalanced();
76
+            }
77
+        });
78
+
79
+        $this->mutateFormDataUsing(function (array $data) {
80
+            if ($this->getTransactionType() === TransactionType::Journal) {
81
+                $data['type'] = TransactionType::Journal;
82
+            }
83
+
84
+            return $data;
85
+        });
86
+
87
+        $this->outlined(fn () => ! $this->getGroup());
88
+    }
89
+}

+ 81
- 0
app/Filament/Actions/EditTransactionAction.php Wyświetl plik

@@ -0,0 +1,81 @@
1
+<?php
2
+
3
+namespace App\Filament\Actions;
4
+
5
+use App\Concerns\HasTransactionAction;
6
+use App\Enums\Accounting\TransactionType;
7
+use App\Models\Accounting\Transaction;
8
+use Filament\Actions\EditAction;
9
+use Filament\Actions\StaticAction;
10
+use Filament\Forms\Form;
11
+use Filament\Support\Enums\MaxWidth;
12
+
13
+class EditTransactionAction extends EditAction
14
+{
15
+    use HasTransactionAction;
16
+
17
+    protected function setUp(): void
18
+    {
19
+        parent::setUp();
20
+
21
+        $this->type(static function (Transaction $record) {
22
+            return $record->type;
23
+        });
24
+
25
+        $this->label(function () {
26
+            return match ($this->getTransactionType()) {
27
+                TransactionType::Journal => 'Edit journal entry',
28
+                default => 'Edit transaction',
29
+            };
30
+        });
31
+
32
+        $this->slideOver();
33
+
34
+        $this->modalWidth(function (): MaxWidth {
35
+            return match ($this->getTransactionType()) {
36
+                TransactionType::Journal => MaxWidth::Screen,
37
+                default => MaxWidth::ThreeExtraLarge,
38
+            };
39
+        });
40
+
41
+        $this->extraModalWindowAttributes(function (): array {
42
+            if ($this->getTransactionType() === TransactionType::Journal) {
43
+                return ['class' => 'journal-transaction-modal'];
44
+            }
45
+
46
+            return [];
47
+        });
48
+
49
+        $this->form(function (Form $form) {
50
+            return match ($this->getTransactionType()) {
51
+                TransactionType::Transfer => $this->transferForm($form),
52
+                TransactionType::Journal => $this->journalTransactionForm($form),
53
+                default => $this->transactionForm($form),
54
+            };
55
+        });
56
+
57
+        $this->afterFormFilled(function (Transaction $record) {
58
+            if ($this->getTransactionType() === TransactionType::Journal) {
59
+                $debitAmounts = $record->journalEntries->sumDebits()->getAmount();
60
+                $creditAmounts = $record->journalEntries->sumCredits()->getAmount();
61
+
62
+                $this->setDebitAmount($debitAmounts);
63
+                $this->setCreditAmount($creditAmounts);
64
+            }
65
+        });
66
+
67
+        $this->modalSubmitAction(function (StaticAction $action) {
68
+            if ($this->getTransactionType() === TransactionType::Journal) {
69
+                $action->disabled(! $this->isJournalEntryBalanced());
70
+            }
71
+
72
+            return $action;
73
+        });
74
+
75
+        $this->after(function (Transaction $transaction) {
76
+            if ($this->getTransactionType() === TransactionType::Journal) {
77
+                $transaction->updateAmountIfBalanced();
78
+            }
79
+        });
80
+    }
81
+}

+ 32
- 36
app/Filament/Company/Pages/Accounting/AccountChart.php Wyświetl plik

@@ -48,7 +48,7 @@ class AccountChart extends Page
48 48
     }
49 49
 
50 50
     #[Computed]
51
-    public function categories(): Collection
51
+    public function accountCategories(): Collection
52 52
     {
53 53
         return AccountSubtype::withCount('accounts')
54 54
             ->with(['accounts' => function ($query) {
@@ -58,42 +58,39 @@ class AccountChart extends Page
58 58
             ->groupBy('category');
59 59
     }
60 60
 
61
-    public function editChartAction(): Action
61
+    public function editAccountAction(): Action
62 62
     {
63
-        return EditAction::make()
64
-            ->iconButton()
65
-            ->name('editChart')
63
+        return EditAction::make('editAccount')
66 64
             ->label('Edit account')
67
-            ->modalHeading('Edit Account')
65
+            ->iconButton()
68 66
             ->icon('heroicon-m-pencil-square')
69
-            ->record(fn (array $arguments) => Account::find($arguments['chart']))
70
-            ->form(fn (Form $form) => $this->getChartForm($form)->operation('edit'));
67
+            ->record(fn (array $arguments) => Account::find($arguments['account']))
68
+            ->form(fn (Form $form) => $this->getAccountForm($form)->operation('edit'));
71 69
     }
72 70
 
73
-    public function createChartAction(): Action
71
+    public function createAccountAction(): Action
74 72
     {
75
-        return CreateAction::make()
73
+        return CreateAction::make('createAccount')
76 74
             ->link()
77
-            ->name('createChart')
78 75
             ->model(Account::class)
79 76
             ->label('Add a new account')
80 77
             ->icon('heroicon-o-plus-circle')
81
-            ->form(fn (Form $form) => $this->getChartForm($form)->operation('create'))
82
-            ->fillForm(fn (array $arguments): array => $this->getChartFormDefaults($arguments['subtype']));
78
+            ->form(fn (Form $form) => $this->getAccountForm($form)->operation('create'))
79
+            ->fillForm(fn (array $arguments): array => $this->getAccountFormDefaults($arguments['accountSubtype']));
83 80
     }
84 81
 
85
-    private function getChartFormDefaults(int $subtypeId): array
82
+    private function getAccountFormDefaults(int $accountSubtypeId): array
86 83
     {
87
-        $accountSubtype = AccountSubtype::find($subtypeId);
84
+        $accountSubtype = AccountSubtype::find($accountSubtypeId);
88 85
         $generatedCode = AccountCode::generate($accountSubtype);
89 86
 
90 87
         return [
91
-            'subtype_id' => $subtypeId,
88
+            'subtype_id' => $accountSubtypeId,
92 89
             'code' => $generatedCode,
93 90
         ];
94 91
     }
95 92
 
96
-    private function getChartForm(Form $form, bool $useActiveTab = true): Form
93
+    private function getAccountForm(Form $form, bool $useActiveTab = true): Form
97 94
     {
98 95
         return $form
99 96
             ->schema([
@@ -115,7 +112,7 @@ class AccountChart extends Page
115 112
             ->live()
116 113
             ->disabledOn('edit')
117 114
             ->searchable()
118
-            ->options($this->getChartSubtypeOptions($useActiveTab))
115
+            ->options($this->getAccountSubtypeOptions($useActiveTab))
119 116
             ->afterStateUpdated(static function (?string $state, Set $set): void {
120 117
                 if ($state) {
121 118
                     $accountSubtype = AccountSubtype::find($state);
@@ -150,12 +147,12 @@ class AccountChart extends Page
150 147
                         return false;
151 148
                     }
152 149
 
153
-                    $subtype = $get('subtype_id');
154
-                    if (empty($subtype)) {
150
+                    $accountSubtypeId = $get('subtype_id');
151
+                    if (empty($accountSubtypeId)) {
155 152
                         return false;
156 153
                     }
157 154
 
158
-                    $accountSubtype = AccountSubtype::find($subtype);
155
+                    $accountSubtype = AccountSubtype::find($accountSubtypeId);
159 156
 
160 157
                     if (! $accountSubtype) {
161 158
                         return false;
@@ -168,22 +165,22 @@ class AccountChart extends Page
168 165
                 })
169 166
                 ->afterStateUpdated(static function ($state, Get $get, Set $set) {
170 167
                     if ($state) {
171
-                        $subtypeId = $get('subtype_id');
168
+                        $accountSubtypeId = $get('subtype_id');
172 169
 
173
-                        if (empty($subtypeId)) {
170
+                        if (empty($accountSubtypeId)) {
174 171
                             return;
175 172
                         }
176 173
 
177
-                        $subtype = AccountSubtype::find($subtypeId);
174
+                        $accountSubtype = AccountSubtype::find($accountSubtypeId);
178 175
 
179
-                        if (! $subtype) {
176
+                        if (! $accountSubtype) {
180 177
                             return;
181 178
                         }
182 179
 
183 180
                         // Set default bank account type based on account category
184
-                        if ($subtype->category === AccountCategory::Asset) {
181
+                        if ($accountSubtype->category === AccountCategory::Asset) {
185 182
                             $set('bankAccount.type', BankAccountType::Depository->value);
186
-                        } elseif ($subtype->category === AccountCategory::Liability) {
183
+                        } elseif ($accountSubtype->category === AccountCategory::Liability) {
187 184
                             $set('bankAccount.type', BankAccountType::Credit->value);
188 185
                         }
189 186
                     } else {
@@ -198,13 +195,13 @@ class AccountChart extends Page
198 195
                     Select::make('type')
199 196
                         ->label('Bank account type')
200 197
                         ->options(function (Get $get) {
201
-                            $subtype = $get('../subtype_id');
198
+                            $accountSubtypeId = $get('../subtype_id');
202 199
 
203
-                            if (empty($subtype)) {
200
+                            if (empty($accountSubtypeId)) {
204 201
                                 return [];
205 202
                             }
206 203
 
207
-                            $accountSubtype = AccountSubtype::find($subtype);
204
+                            $accountSubtype = AccountSubtype::find($accountSubtypeId);
208 205
 
209 206
                             if (! $accountSubtype) {
210 207
                                 return [];
@@ -287,14 +284,14 @@ class AccountChart extends Page
287 284
             ->hiddenOn('create');
288 285
     }
289 286
 
290
-    private function getChartSubtypeOptions($useActiveTab = true): array
287
+    private function getAccountSubtypeOptions($useActiveTab = true): array
291 288
     {
292
-        $subtypes = $useActiveTab ?
289
+        $accountSubtypes = $useActiveTab ?
293 290
             AccountSubtype::where('category', $this->activeTab)->get() :
294 291
             AccountSubtype::all();
295 292
 
296
-        return $subtypes->groupBy(fn (AccountSubtype $subtype) => $subtype->type->getLabel())
297
-            ->map(fn (Collection $subtypes, string $type) => $subtypes->mapWithKeys(static fn (AccountSubtype $subtype) => [$subtype->id => $subtype->name]))
293
+        return $accountSubtypes->groupBy(fn (AccountSubtype $accountSubtype) => $accountSubtype->type->getLabel())
294
+            ->map(fn (Collection $accountSubtypes, string $type) => $accountSubtypes->mapWithKeys(static fn (AccountSubtype $accountSubtype) => [$accountSubtype->id => $accountSubtype->name]))
298 295
             ->toArray();
299 296
     }
300 297
 
@@ -303,9 +300,8 @@ class AccountChart extends Page
303 300
         return [
304 301
             CreateAction::make()
305 302
                 ->button()
306
-                ->label('Add new account')
307 303
                 ->model(Account::class)
308
-                ->form(fn (Form $form) => $this->getChartForm($form, false)->operation('create')),
304
+                ->form(fn (Form $form) => $this->getAccountForm($form, false)->operation('create')),
309 305
         ];
310 306
     }
311 307
 

+ 0
- 880
app/Filament/Company/Pages/Accounting/Transactions.php Wyświetl plik

@@ -1,880 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Pages\Accounting;
4
-
5
-use App\Concerns\HasJournalEntryActions;
6
-use App\Enums\Accounting\AccountCategory;
7
-use App\Enums\Accounting\JournalEntryType;
8
-use App\Enums\Accounting\TransactionType;
9
-use App\Facades\Accounting;
10
-use App\Filament\Company\Pages\Service\ConnectedAccount;
11
-use App\Filament\Forms\Components\CustomTableRepeater;
12
-use App\Filament\Forms\Components\DateRangeSelect;
13
-use App\Filament\Tables\Actions\ReplicateBulkAction;
14
-use App\Models\Accounting\Account;
15
-use App\Models\Accounting\JournalEntry;
16
-use App\Models\Accounting\Transaction;
17
-use App\Models\Banking\BankAccount;
18
-use App\Models\Company;
19
-use App\Services\PlaidService;
20
-use App\Utilities\Currency\CurrencyAccessor;
21
-use App\Utilities\Currency\CurrencyConverter;
22
-use Awcodes\TableRepeater\Header;
23
-use Exception;
24
-use Filament\Actions;
25
-use Filament\Facades\Filament;
26
-use Filament\Forms;
27
-use Filament\Forms\Components\Actions\Action as FormAction;
28
-use Filament\Forms\Components\DatePicker;
29
-use Filament\Forms\Components\Grid;
30
-use Filament\Forms\Components\Select;
31
-use Filament\Forms\Components\Tabs;
32
-use Filament\Forms\Components\Tabs\Tab;
33
-use Filament\Forms\Components\Textarea;
34
-use Filament\Forms\Components\TextInput;
35
-use Filament\Forms\Form;
36
-use Filament\Forms\Get;
37
-use Filament\Forms\Set;
38
-use Filament\Notifications\Notification;
39
-use Filament\Pages\Page;
40
-use Filament\Support\Colors\Color;
41
-use Filament\Support\Enums\FontWeight;
42
-use Filament\Support\Enums\IconPosition;
43
-use Filament\Support\Enums\IconSize;
44
-use Filament\Support\Enums\MaxWidth;
45
-use Filament\Tables;
46
-use Filament\Tables\Concerns\InteractsWithTable;
47
-use Filament\Tables\Contracts\HasTable;
48
-use Filament\Tables\Table;
49
-use Illuminate\Contracts\View\View;
50
-use Illuminate\Database\Eloquent\Builder;
51
-use Illuminate\Support\Carbon;
52
-use Illuminate\Support\Collection;
53
-use Illuminate\Support\Str;
54
-
55
-/**
56
- * @property Form $form
57
- */
58
-class Transactions extends Page implements HasTable
59
-{
60
-    use HasJournalEntryActions;
61
-    use InteractsWithTable;
62
-
63
-    protected static string $view = 'filament.company.pages.accounting.transactions';
64
-
65
-    protected static ?string $model = Transaction::class;
66
-
67
-    protected static ?string $navigationGroup = 'Accounting';
68
-
69
-    public string $fiscalYearStartDate = '';
70
-
71
-    public string $fiscalYearEndDate = '';
72
-
73
-    public function mount(): void
74
-    {
75
-        /** @var Company $company */
76
-        $company = Filament::getTenant();
77
-        $this->fiscalYearStartDate = $company->locale->fiscalYearStartDate();
78
-        $this->fiscalYearEndDate = $company->locale->fiscalYearEndDate();
79
-    }
80
-
81
-    public static function getModel(): string
82
-    {
83
-        return static::$model;
84
-    }
85
-
86
-    public static function getEloquentQuery(): Builder
87
-    {
88
-        return static::getModel()::query();
89
-    }
90
-
91
-    public function getMaxContentWidth(): MaxWidth | string | null
92
-    {
93
-        return 'max-w-8xl';
94
-    }
95
-
96
-    protected function getHeaderActions(): array
97
-    {
98
-        return [
99
-            $this->buildTransactionAction('addIncome', 'Add income', TransactionType::Deposit),
100
-            $this->buildTransactionAction('addExpense', 'Add expense', TransactionType::Withdrawal),
101
-            Actions\CreateAction::make('addTransfer')
102
-                ->label('Add transfer')
103
-                ->modalHeading('Add Transfer')
104
-                ->modalWidth(MaxWidth::ThreeExtraLarge)
105
-                ->model(static::getModel())
106
-                ->fillForm(fn (): array => $this->getFormDefaultsForType(TransactionType::Transfer))
107
-                ->form(fn (Form $form) => $this->transferForm($form))
108
-                ->button()
109
-                ->outlined(),
110
-            Actions\ActionGroup::make([
111
-                Actions\CreateAction::make('addJournalTransaction')
112
-                    ->label('Add journal transaction')
113
-                    ->fillForm(fn (): array => $this->getFormDefaultsForType(TransactionType::Journal))
114
-                    ->modalWidth(MaxWidth::Screen)
115
-                    ->extraModalWindowAttributes([
116
-                        'class' => 'journal-transaction-modal',
117
-                    ])
118
-                    ->model(static::getModel())
119
-                    ->form(fn (Form $form) => $this->journalTransactionForm($form))
120
-                    ->modalSubmitAction(fn (Actions\StaticAction $action) => $action->disabled(! $this->isJournalEntryBalanced()))
121
-                    ->groupedIcon(null)
122
-                    ->modalHeading('Journal Entry')
123
-                    ->mutateFormDataUsing(static fn (array $data) => array_merge($data, ['type' => TransactionType::Journal]))
124
-                    ->afterFormFilled(fn () => $this->resetJournalEntryAmounts())
125
-                    ->after(fn (Transaction $transaction) => $transaction->updateAmountIfBalanced()),
126
-                Actions\Action::make('connectBank')
127
-                    ->label('Connect your bank')
128
-                    ->visible(app(PlaidService::class)->isEnabled())
129
-                    ->url(ConnectedAccount::getUrl()),
130
-            ])
131
-                ->label('More')
132
-                ->button()
133
-                ->outlined()
134
-                ->dropdownWidth('max-w-fit')
135
-                ->dropdownPlacement('bottom-end')
136
-                ->icon('heroicon-c-chevron-down')
137
-                ->iconSize(IconSize::Small)
138
-                ->iconPosition(IconPosition::After),
139
-        ];
140
-    }
141
-
142
-    public function transferForm(Form $form): Form
143
-    {
144
-        return $form
145
-            ->schema([
146
-                Forms\Components\DatePicker::make('posted_at')
147
-                    ->label('Date')
148
-                    ->required(),
149
-                Forms\Components\TextInput::make('description')
150
-                    ->label('Description'),
151
-                Forms\Components\Select::make('bank_account_id')
152
-                    ->label('From account')
153
-                    ->options(fn (Get $get, ?Transaction $transaction) => $this->getBankAccountOptions(excludedAccountId: $get('account_id'), currentBankAccountId: $transaction?->bank_account_id))
154
-                    ->live()
155
-                    ->searchable()
156
-                    ->afterStateUpdated(function (Set $set, $state, $old, Get $get) {
157
-                        $amount = CurrencyConverter::convertAndSet(
158
-                            BankAccount::find($state)->account->currency_code,
159
-                            BankAccount::find($old)->account->currency_code ?? CurrencyAccessor::getDefaultCurrency(),
160
-                            $get('amount')
161
-                        );
162
-
163
-                        if ($amount !== null) {
164
-                            $set('amount', $amount);
165
-                        }
166
-                    })
167
-                    ->required(),
168
-                Forms\Components\Select::make('type')
169
-                    ->label('Type')
170
-                    ->options([
171
-                        TransactionType::Transfer->value => TransactionType::Transfer->getLabel(),
172
-                    ])
173
-                    ->disabled()
174
-                    ->dehydrated()
175
-                    ->required(),
176
-                Forms\Components\TextInput::make('amount')
177
-                    ->label('Amount')
178
-                    ->money(static fn (Forms\Get $get) => BankAccount::find($get('bank_account_id'))?->account?->currency_code ?? CurrencyAccessor::getDefaultCurrency())
179
-                    ->required(),
180
-                Forms\Components\Select::make('account_id')
181
-                    ->label('To account')
182
-                    ->live()
183
-                    ->options(fn (Get $get, ?Transaction $transaction) => $this->getBankAccountAccountOptions(excludedBankAccountId: $get('bank_account_id'), currentAccountId: $transaction?->account_id))
184
-                    ->searchable()
185
-                    ->required(),
186
-                Forms\Components\Textarea::make('notes')
187
-                    ->label('Notes')
188
-                    ->autosize()
189
-                    ->rows(10)
190
-                    ->columnSpanFull(),
191
-            ])
192
-            ->columns();
193
-    }
194
-
195
-    public function transactionForm(Form $form): Form
196
-    {
197
-        return $form
198
-            ->schema([
199
-                Forms\Components\DatePicker::make('posted_at')
200
-                    ->label('Date')
201
-                    ->required(),
202
-                Forms\Components\TextInput::make('description')
203
-                    ->label('Description'),
204
-                Forms\Components\Select::make('bank_account_id')
205
-                    ->label('Account')
206
-                    ->options(fn (?Transaction $transaction) => $this->getBankAccountOptions(currentBankAccountId: $transaction?->bank_account_id))
207
-                    ->live()
208
-                    ->searchable()
209
-                    ->afterStateUpdated(function (Set $set, $state, $old, Get $get) {
210
-                        $amount = CurrencyConverter::convertAndSet(
211
-                            BankAccount::find($state)->account->currency_code,
212
-                            BankAccount::find($old)->account->currency_code ?? CurrencyAccessor::getDefaultCurrency(),
213
-                            $get('amount')
214
-                        );
215
-
216
-                        if ($amount !== null) {
217
-                            $set('amount', $amount);
218
-                        }
219
-                    })
220
-                    ->required(),
221
-                Forms\Components\Select::make('type')
222
-                    ->label('Type')
223
-                    ->live()
224
-                    ->options([
225
-                        TransactionType::Deposit->value => TransactionType::Deposit->getLabel(),
226
-                        TransactionType::Withdrawal->value => TransactionType::Withdrawal->getLabel(),
227
-                    ])
228
-                    ->required()
229
-                    ->afterStateUpdated(static fn (Forms\Set $set, $state) => $set('account_id', static::getUncategorizedAccountByType(TransactionType::parse($state))?->id)),
230
-                Forms\Components\TextInput::make('amount')
231
-                    ->label('Amount')
232
-                    ->money(static fn (Forms\Get $get) => BankAccount::find($get('bank_account_id'))?->account?->currency_code ?? CurrencyAccessor::getDefaultCurrency())
233
-                    ->required(),
234
-                Forms\Components\Select::make('account_id')
235
-                    ->label('Category')
236
-                    ->options(fn (Forms\Get $get, ?Transaction $transaction) => $this->getChartAccountOptions(type: TransactionType::parse($get('type')), nominalAccountsOnly: true, currentAccountId: $transaction?->account_id))
237
-                    ->searchable()
238
-                    ->preload()
239
-                    ->required(),
240
-                Forms\Components\Textarea::make('notes')
241
-                    ->label('Notes')
242
-                    ->autosize()
243
-                    ->rows(10)
244
-                    ->columnSpanFull(),
245
-            ])
246
-            ->columns();
247
-    }
248
-
249
-    public function journalTransactionForm(Form $form): Form
250
-    {
251
-        return $form
252
-            ->schema([
253
-                Tabs::make('Tabs')
254
-                    ->contained(false)
255
-                    ->tabs([
256
-                        $this->getJournalTransactionFormEditTab(),
257
-                        $this->getJournalTransactionFormNotesTab(),
258
-                    ]),
259
-            ])
260
-            ->columns(1);
261
-    }
262
-
263
-    /**
264
-     * @throws Exception
265
-     */
266
-    public function table(Table $table): Table
267
-    {
268
-        return $table
269
-            ->query(static::getEloquentQuery())
270
-            ->modifyQueryUsing(function (Builder $query) {
271
-                $query->with([
272
-                    'account',
273
-                    'bankAccount.account',
274
-                    'journalEntries.account',
275
-                ])
276
-                    ->where(function (Builder $query) {
277
-                        $query->whereNull('transactionable_id')
278
-                            ->orWhere('is_payment', true);
279
-                    });
280
-            })
281
-            ->columns([
282
-                Tables\Columns\TextColumn::make('posted_at')
283
-                    ->label('Date')
284
-                    ->sortable()
285
-                    ->defaultDateFormat(),
286
-                Tables\Columns\TextColumn::make('type')
287
-                    ->label('Type')
288
-                    ->sortable()
289
-                    ->toggleable(isToggledHiddenByDefault: true),
290
-                Tables\Columns\TextColumn::make('description')
291
-                    ->label('Description')
292
-                    ->limit(50)
293
-                    ->toggleable(),
294
-                Tables\Columns\TextColumn::make('bankAccount.account.name')
295
-                    ->label('Account')
296
-                    ->toggleable(),
297
-                Tables\Columns\TextColumn::make('account.name')
298
-                    ->label('Category')
299
-                    ->prefix(static fn (Transaction $transaction) => $transaction->type->isTransfer() ? 'Transfer to ' : null)
300
-                    ->toggleable()
301
-                    ->state(static fn (Transaction $transaction) => $transaction->account->name ?? 'Journal Entry'),
302
-                Tables\Columns\TextColumn::make('amount')
303
-                    ->label('Amount')
304
-                    ->weight(static fn (Transaction $transaction) => $transaction->reviewed ? null : FontWeight::SemiBold)
305
-                    ->color(
306
-                        static fn (Transaction $transaction) => match ($transaction->type) {
307
-                            TransactionType::Deposit => Color::rgb('rgb(' . Color::Green[700] . ')'),
308
-                            TransactionType::Journal => 'primary',
309
-                            default => null,
310
-                        }
311
-                    )
312
-                    ->sortable()
313
-                    ->currency(static fn (Transaction $transaction) => $transaction->bankAccount?->account->currency_code),
314
-            ])
315
-            ->recordClasses(static fn (Transaction $transaction) => $transaction->reviewed ? 'bg-primary-300/10' : null)
316
-            ->defaultSort('posted_at', 'desc')
317
-            ->filters([
318
-                Tables\Filters\SelectFilter::make('bank_account_id')
319
-                    ->label('Account')
320
-                    ->searchable()
321
-                    ->options(fn () => $this->getBankAccountOptions(false)),
322
-                Tables\Filters\SelectFilter::make('account_id')
323
-                    ->label('Category')
324
-                    ->multiple()
325
-                    ->options(fn () => $this->getChartAccountOptions(nominalAccountsOnly: false)),
326
-                Tables\Filters\TernaryFilter::make('reviewed')
327
-                    ->label('Status')
328
-                    ->native(false)
329
-                    ->trueLabel('Reviewed')
330
-                    ->falseLabel('Not Reviewed'),
331
-                Tables\Filters\SelectFilter::make('type')
332
-                    ->label('Type')
333
-                    ->native(false)
334
-                    ->options(TransactionType::class),
335
-                $this->buildDateRangeFilter('posted_at', 'Posted', true),
336
-                $this->buildDateRangeFilter('updated_at', 'Last modified'),
337
-            ])
338
-            ->filtersFormSchema(fn (array $filters): array => [
339
-                Grid::make()
340
-                    ->schema([
341
-                        $filters['bank_account_id'],
342
-                        $filters['account_id'],
343
-                        $filters['reviewed'],
344
-                        $filters['type'],
345
-                    ])
346
-                    ->columnSpanFull()
347
-                    ->extraAttributes(['class' => 'border-b border-gray-200 dark:border-white/10 pb-8']),
348
-                $filters['posted_at'],
349
-                $filters['updated_at'],
350
-            ])
351
-            ->filtersFormWidth(MaxWidth::ThreeExtraLarge)
352
-            ->actions([
353
-                Tables\Actions\Action::make('markAsReviewed')
354
-                    ->label('Mark as reviewed')
355
-                    ->view('filament.company.components.tables.actions.mark-as-reviewed')
356
-                    ->icon(static fn (Transaction $transaction) => $transaction->reviewed ? 'heroicon-s-check-circle' : 'heroicon-o-check-circle')
357
-                    ->color(static fn (Transaction $transaction, Tables\Actions\Action $action) => match (static::determineTransactionState($transaction, $action)) {
358
-                        'reviewed' => 'primary',
359
-                        'unreviewed' => Color::rgb('rgb(' . Color::Gray[600] . ')'),
360
-                        'uncategorized' => 'gray',
361
-                    })
362
-                    ->tooltip(static fn (Transaction $transaction, Tables\Actions\Action $action) => match (static::determineTransactionState($transaction, $action)) {
363
-                        'reviewed' => 'Reviewed',
364
-                        'unreviewed' => 'Mark as reviewed',
365
-                        'uncategorized' => 'Categorize first to mark as reviewed',
366
-                    })
367
-                    ->disabled(fn (Transaction $transaction): bool => $transaction->isUncategorized())
368
-                    ->action(fn (Transaction $transaction) => $transaction->update(['reviewed' => ! $transaction->reviewed])),
369
-                Tables\Actions\ActionGroup::make([
370
-                    Tables\Actions\ActionGroup::make([
371
-                        Tables\Actions\EditAction::make('editTransaction')
372
-                            ->label('Edit transaction')
373
-                            ->modalHeading('Edit Transaction')
374
-                            ->modalWidth(MaxWidth::ThreeExtraLarge)
375
-                            ->form(fn (Form $form) => $this->transactionForm($form))
376
-                            ->visible(static fn (Transaction $transaction) => $transaction->type->isStandard() && ! $transaction->transactionable_id),
377
-                        Tables\Actions\EditAction::make('editTransfer')
378
-                            ->label('Edit transfer')
379
-                            ->modalHeading('Edit Transfer')
380
-                            ->modalWidth(MaxWidth::ThreeExtraLarge)
381
-                            ->form(fn (Form $form) => $this->transferForm($form))
382
-                            ->visible(static fn (Transaction $transaction) => $transaction->type->isTransfer()),
383
-                        Tables\Actions\EditAction::make('editJournalTransaction')
384
-                            ->label('Edit journal transaction')
385
-                            ->modalHeading('Journal Entry')
386
-                            ->modalWidth(MaxWidth::Screen)
387
-                            ->extraModalWindowAttributes([
388
-                                'class' => 'journal-transaction-modal',
389
-                            ])
390
-                            ->form(fn (Form $form) => $this->journalTransactionForm($form))
391
-                            ->afterFormFilled(function (Transaction $transaction) {
392
-                                $debitAmounts = $transaction->journalEntries->sumDebits()->getAmount();
393
-                                $creditAmounts = $transaction->journalEntries->sumCredits()->getAmount();
394
-
395
-                                $this->setDebitAmount($debitAmounts);
396
-                                $this->setCreditAmount($creditAmounts);
397
-                            })
398
-                            ->modalSubmitAction(fn (Actions\StaticAction $action) => $action->disabled(! $this->isJournalEntryBalanced()))
399
-                            ->after(fn (Transaction $transaction) => $transaction->updateAmountIfBalanced())
400
-                            ->visible(static fn (Transaction $transaction) => $transaction->type->isJournal() && ! $transaction->transactionable_id),
401
-                        Tables\Actions\ReplicateAction::make()
402
-                            ->excludeAttributes(['created_by', 'updated_by', 'created_at', 'updated_at'])
403
-                            ->modal(false)
404
-                            ->beforeReplicaSaved(static function (Transaction $replica) {
405
-                                $replica->description = '(Copy of) ' . $replica->description;
406
-                            })
407
-                            ->hidden(static fn (Transaction $transaction) => $transaction->transactionable_id)
408
-                            ->after(static function (Transaction $original, Transaction $replica) {
409
-                                $original->journalEntries->each(function (JournalEntry $entry) use ($replica) {
410
-                                    $entry->replicate([
411
-                                        'transaction_id',
412
-                                    ])->fill([
413
-                                        'transaction_id' => $replica->id,
414
-                                    ])->save();
415
-                                });
416
-                            }),
417
-                    ])->dropdown(false),
418
-                    Tables\Actions\DeleteAction::make(),
419
-                ]),
420
-            ])
421
-            ->bulkActions([
422
-                Tables\Actions\BulkActionGroup::make([
423
-                    Tables\Actions\DeleteBulkAction::make(),
424
-                    ReplicateBulkAction::make()
425
-                        ->label('Replicate')
426
-                        ->modalWidth(MaxWidth::Large)
427
-                        ->modalDescription('Replicating transactions will also replicate their journal entries. Are you sure you want to proceed?')
428
-                        ->successNotificationTitle('Transactions replicated successfully')
429
-                        ->failureNotificationTitle('Failed to replicate transactions')
430
-                        ->deselectRecordsAfterCompletion()
431
-                        ->excludeAttributes(['created_by', 'updated_by', 'created_at', 'updated_at'])
432
-                        ->beforeReplicaSaved(static function (Transaction $replica) {
433
-                            $replica->description = '(Copy of) ' . $replica->description;
434
-                        })
435
-                        ->before(function (\Illuminate\Database\Eloquent\Collection $records, ReplicateBulkAction $action) {
436
-                            $isInvalid = $records->contains(fn (Transaction $record) => $record->transactionable_id);
437
-
438
-                            if ($isInvalid) {
439
-                                Notification::make()
440
-                                    ->title('Cannot replicate transactions')
441
-                                    ->body('You cannot replicate transactions associated with bills or invoices')
442
-                                    ->persistent()
443
-                                    ->danger()
444
-                                    ->send();
445
-
446
-                                $action->cancel(true);
447
-                            }
448
-                        })
449
-                        ->withReplicatedRelationships(['journalEntries']),
450
-                ]),
451
-            ]);
452
-    }
453
-
454
-    protected function buildTransactionAction(string $name, string $label, TransactionType $type): Actions\CreateAction
455
-    {
456
-        return Actions\CreateAction::make($name)
457
-            ->label($label)
458
-            ->modalWidth(MaxWidth::ThreeExtraLarge)
459
-            ->model(static::getModel())
460
-            ->fillForm(fn (): array => $this->getFormDefaultsForType($type))
461
-            ->form(fn (Form $form) => $this->transactionForm($form))
462
-            ->button()
463
-            ->outlined();
464
-    }
465
-
466
-    protected function getFormDefaultsForType(TransactionType $type): array
467
-    {
468
-        $commonDefaults = [
469
-            'posted_at' => today(),
470
-        ];
471
-
472
-        return match ($type) {
473
-            TransactionType::Deposit, TransactionType::Withdrawal, TransactionType::Transfer => array_merge($commonDefaults, $this->transactionDefaults($type)),
474
-            TransactionType::Journal => array_merge($commonDefaults, $this->journalEntryDefaults()),
475
-        };
476
-    }
477
-
478
-    protected function journalEntryDefaults(): array
479
-    {
480
-        return [
481
-            'journalEntries' => [
482
-                $this->defaultEntry(JournalEntryType::Debit),
483
-                $this->defaultEntry(JournalEntryType::Credit),
484
-            ],
485
-        ];
486
-    }
487
-
488
-    protected function defaultEntry(JournalEntryType $journalEntryType): array
489
-    {
490
-        return [
491
-            'type' => $journalEntryType,
492
-            'account_id' => static::getUncategorizedAccountByType($journalEntryType->isDebit() ? TransactionType::Withdrawal : TransactionType::Deposit)?->id,
493
-            'amount' => '0.00',
494
-        ];
495
-    }
496
-
497
-    protected function transactionDefaults(TransactionType $type): array
498
-    {
499
-        return [
500
-            'type' => $type,
501
-            'bank_account_id' => BankAccount::where('enabled', true)->first()?->id,
502
-            'amount' => '0.00',
503
-            'account_id' => ! $type->isTransfer() ? static::getUncategorizedAccountByType($type)->id : null,
504
-        ];
505
-    }
506
-
507
-    public static function getUncategorizedAccountByType(TransactionType $type): ?Account
508
-    {
509
-        [$category, $accountName] = match ($type) {
510
-            TransactionType::Deposit => [AccountCategory::Revenue, 'Uncategorized Income'],
511
-            TransactionType::Withdrawal => [AccountCategory::Expense, 'Uncategorized Expense'],
512
-            default => [null, null],
513
-        };
514
-
515
-        return Account::where('category', $category)
516
-            ->where('name', $accountName)
517
-            ->first();
518
-    }
519
-
520
-    protected function getJournalTransactionFormEditTab(): Tab
521
-    {
522
-        return Tab::make('Edit')
523
-            ->label('Edit')
524
-            ->icon('heroicon-o-pencil-square')
525
-            ->schema([
526
-                $this->getTransactionDetailsGrid(),
527
-                $this->getJournalEntriesTableRepeater(),
528
-            ]);
529
-    }
530
-
531
-    protected function getJournalTransactionFormNotesTab(): Tab
532
-    {
533
-        return Tab::make('Notes')
534
-            ->label('Notes')
535
-            ->icon('heroicon-o-clipboard')
536
-            ->id('notes')
537
-            ->schema([
538
-                $this->getTransactionDetailsGrid(),
539
-                Textarea::make('notes')
540
-                    ->label('Notes')
541
-                    ->rows(10)
542
-                    ->autosize(),
543
-            ]);
544
-    }
545
-
546
-    protected function getTransactionDetailsGrid(): Grid
547
-    {
548
-        return Grid::make(8)
549
-            ->schema([
550
-                DatePicker::make('posted_at')
551
-                    ->label('Date')
552
-                    ->softRequired()
553
-                    ->displayFormat('Y-m-d'),
554
-                TextInput::make('description')
555
-                    ->label('Description')
556
-                    ->columnSpan(2),
557
-            ]);
558
-    }
559
-
560
-    protected function getJournalEntriesTableRepeater(): CustomTableRepeater
561
-    {
562
-        return CustomTableRepeater::make('journalEntries')
563
-            ->relationship('journalEntries')
564
-            ->hiddenLabel()
565
-            ->columns(4)
566
-            ->headers($this->getJournalEntriesTableRepeaterHeaders())
567
-            ->schema($this->getJournalEntriesTableRepeaterSchema())
568
-            ->deletable(fn (CustomTableRepeater $repeater) => $repeater->getItemsCount() > 2)
569
-            ->deleteAction(function (Forms\Components\Actions\Action $action) {
570
-                return $action
571
-                    ->action(function (array $arguments, CustomTableRepeater $component): void {
572
-                        $items = $component->getState();
573
-
574
-                        $amount = $items[$arguments['item']]['amount'];
575
-                        $type = $items[$arguments['item']]['type'];
576
-
577
-                        $this->updateJournalEntryAmount(JournalEntryType::parse($type), '0.00', $amount);
578
-
579
-                        unset($items[$arguments['item']]);
580
-
581
-                        $component->state($items);
582
-
583
-                        $component->callAfterStateUpdated();
584
-                    });
585
-            })
586
-            ->rules([
587
-                function () {
588
-                    return function (string $attribute, $value, \Closure $fail) {
589
-                        if (empty($value) || ! is_array($value)) {
590
-                            $fail('Journal entries are required.');
591
-
592
-                            return;
593
-                        }
594
-
595
-                        $hasDebit = false;
596
-                        $hasCredit = false;
597
-
598
-                        foreach ($value as $entry) {
599
-                            if (! isset($entry['type'])) {
600
-                                continue;
601
-                            }
602
-
603
-                            if (JournalEntryType::parse($entry['type'])->isDebit()) {
604
-                                $hasDebit = true;
605
-                            } elseif (JournalEntryType::parse($entry['type'])->isCredit()) {
606
-                                $hasCredit = true;
607
-                            }
608
-
609
-                            if ($hasDebit && $hasCredit) {
610
-                                break;
611
-                            }
612
-                        }
613
-
614
-                        if (! $hasDebit) {
615
-                            $fail('At least one debit entry is required.');
616
-                        }
617
-
618
-                        if (! $hasCredit) {
619
-                            $fail('At least one credit entry is required.');
620
-                        }
621
-                    };
622
-                },
623
-            ])
624
-            ->minItems(2)
625
-            ->defaultItems(2)
626
-            ->addable(false)
627
-            ->footerItem(fn (): View => $this->getJournalTransactionModalFooter())
628
-            ->extraActions([
629
-                $this->buildAddJournalEntryAction(JournalEntryType::Debit),
630
-                $this->buildAddJournalEntryAction(JournalEntryType::Credit),
631
-            ]);
632
-    }
633
-
634
-    protected function getJournalEntriesTableRepeaterHeaders(): array
635
-    {
636
-        return [
637
-            Header::make('type')
638
-                ->width('150px')
639
-                ->label('Type'),
640
-            Header::make('description')
641
-                ->width('320px')
642
-                ->label('Description'),
643
-            Header::make('account_id')
644
-                ->width('320px')
645
-                ->label('Account'),
646
-            Header::make('amount')
647
-                ->width('192px')
648
-                ->label('Amount'),
649
-        ];
650
-    }
651
-
652
-    protected function getJournalEntriesTableRepeaterSchema(): array
653
-    {
654
-        return [
655
-            Select::make('type')
656
-                ->label('Type')
657
-                ->options(JournalEntryType::class)
658
-                ->live()
659
-                ->afterStateUpdated(function (Get $get, Set $set, $state, $old) {
660
-                    $this->adjustJournalEntryAmountsForTypeChange(JournalEntryType::parse($state), JournalEntryType::parse($old), $get('amount'));
661
-                })
662
-                ->softRequired(),
663
-            TextInput::make('description')
664
-                ->label('Description'),
665
-            Select::make('account_id')
666
-                ->label('Account')
667
-                ->options(fn (?JournalEntry $journalEntry): array => $this->getChartAccountOptions(currentAccountId: $journalEntry?->account_id))
668
-                ->live()
669
-                ->softRequired()
670
-                ->searchable(),
671
-            TextInput::make('amount')
672
-                ->label('Amount')
673
-                ->live()
674
-                ->mask(moneyMask(CurrencyAccessor::getDefaultCurrency()))
675
-                ->afterStateUpdated(function (Get $get, Set $set, ?string $state, ?string $old) {
676
-                    $this->updateJournalEntryAmount(JournalEntryType::parse($get('type')), $state, $old);
677
-                })
678
-                ->softRequired(),
679
-        ];
680
-    }
681
-
682
-    protected function buildAddJournalEntryAction(JournalEntryType $type): FormAction
683
-    {
684
-        $typeLabel = $type->getLabel();
685
-
686
-        return FormAction::make("add{$typeLabel}Entry")
687
-            ->label("Add {$typeLabel} entry")
688
-            ->button()
689
-            ->outlined()
690
-            ->color($type->isDebit() ? 'primary' : 'gray')
691
-            ->iconSize(IconSize::Small)
692
-            ->iconPosition(IconPosition::Before)
693
-            ->action(function (CustomTableRepeater $component) use ($type) {
694
-                $state = $component->getState();
695
-                $newUuid = (string) Str::uuid();
696
-                $state[$newUuid] = $this->defaultEntry($type);
697
-
698
-                $component->state($state);
699
-            });
700
-    }
701
-
702
-    public function getJournalTransactionModalFooter(): View
703
-    {
704
-        return view(
705
-            'filament.company.components.actions.journal-entry-footer',
706
-            [
707
-                'debitAmount' => $this->getFormattedDebitAmount(),
708
-                'creditAmount' => $this->getFormattedCreditAmount(),
709
-                'difference' => $this->getFormattedBalanceDifference(),
710
-                'isJournalBalanced' => $this->isJournalEntryBalanced(),
711
-            ],
712
-        );
713
-    }
714
-
715
-    /**
716
-     * @throws Exception
717
-     */
718
-    protected function buildDateRangeFilter(string $fieldPrefix, string $label, bool $hasBottomBorder = false): Tables\Filters\Filter
719
-    {
720
-        return Tables\Filters\Filter::make($fieldPrefix)
721
-            ->columnSpanFull()
722
-            ->form([
723
-                Grid::make()
724
-                    ->live()
725
-                    ->schema([
726
-                        DateRangeSelect::make("{$fieldPrefix}_date_range")
727
-                            ->label($label)
728
-                            ->selectablePlaceholder(false)
729
-                            ->placeholder('Select a date range')
730
-                            ->startDateField("{$fieldPrefix}_start_date")
731
-                            ->endDateField("{$fieldPrefix}_end_date"),
732
-                        DatePicker::make("{$fieldPrefix}_start_date")
733
-                            ->label("{$label} from")
734
-                            ->columnStart(1)
735
-                            ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
736
-                                $set("{$fieldPrefix}_date_range", 'Custom');
737
-                            }),
738
-                        DatePicker::make("{$fieldPrefix}_end_date")
739
-                            ->label("{$label} to")
740
-                            ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
741
-                                $set("{$fieldPrefix}_date_range", 'Custom');
742
-                            }),
743
-                    ])
744
-                    ->extraAttributes($hasBottomBorder ? ['class' => 'border-b border-gray-200 dark:border-white/10 pb-8'] : []),
745
-            ])
746
-            ->query(function (Builder $query, array $data) use ($fieldPrefix): Builder {
747
-                $query
748
-                    ->when($data["{$fieldPrefix}_start_date"], fn (Builder $query, $startDate) => $query->whereDate($fieldPrefix, '>=', $startDate))
749
-                    ->when($data["{$fieldPrefix}_end_date"], fn (Builder $query, $endDate) => $query->whereDate($fieldPrefix, '<=', $endDate));
750
-
751
-                return $query;
752
-            })
753
-            ->indicateUsing(function (array $data) use ($fieldPrefix, $label): array {
754
-                $indicators = [];
755
-
756
-                $this->addIndicatorForDateRange($data, "{$fieldPrefix}_start_date", "{$fieldPrefix}_end_date", $label, $indicators);
757
-
758
-                return $indicators;
759
-            });
760
-
761
-    }
762
-
763
-    protected function addIndicatorForSingleSelection($data, $key, $label, &$indicators): void
764
-    {
765
-        if (filled($data[$key])) {
766
-            $indicators[] = Tables\Filters\Indicator::make($label)
767
-                ->removeField($key);
768
-        }
769
-    }
770
-
771
-    protected function addMultipleSelectionIndicator($data, $key, callable $labelRetriever, $field, &$indicators): void
772
-    {
773
-        if (filled($data[$key])) {
774
-            $labels = collect($data[$key])->map($labelRetriever);
775
-            $additionalCount = $labels->count() - 1;
776
-            $indicatorLabel = $additionalCount > 0 ? "{$labels->first()} + {$additionalCount}" : $labels->first();
777
-            $indicators[] = Tables\Filters\Indicator::make($indicatorLabel)
778
-                ->removeField($field);
779
-        }
780
-    }
781
-
782
-    protected function addIndicatorForDateRange($data, $startKey, $endKey, $labelPrefix, &$indicators): void
783
-    {
784
-        $formattedStartDate = filled($data[$startKey]) ? Carbon::parse($data[$startKey])->toFormattedDateString() : null;
785
-        $formattedEndDate = filled($data[$endKey]) ? Carbon::parse($data[$endKey])->toFormattedDateString() : null;
786
-        if ($formattedStartDate && $formattedEndDate) {
787
-            // If both start and end dates are set, show the combined date range as the indicator, no specific field needs to be removed since the entire filter will be removed
788
-            $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix}: {$formattedStartDate} - {$formattedEndDate}");
789
-        } else {
790
-            if ($formattedStartDate) {
791
-                $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix} After: {$formattedStartDate}")
792
-                    ->removeField($startKey);
793
-            }
794
-
795
-            if ($formattedEndDate) {
796
-                $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix} Before: {$formattedEndDate}")
797
-                    ->removeField($endKey);
798
-            }
799
-        }
800
-    }
801
-
802
-    protected static function determineTransactionState(Transaction $transaction, Tables\Actions\Action $action): string
803
-    {
804
-        if ($transaction->reviewed) {
805
-            return 'reviewed';
806
-        }
807
-
808
-        if ($transaction->reviewed === false && $action->isEnabled()) {
809
-            return 'unreviewed';
810
-        }
811
-
812
-        return 'uncategorized';
813
-    }
814
-
815
-    protected function getBankAccountOptions(?int $excludedAccountId = null, ?int $currentBankAccountId = null): array
816
-    {
817
-        return BankAccount::query()
818
-            ->whereHas('account', function (Builder $query) {
819
-                $query->where('archived', false);
820
-            })
821
-            ->with(['account' => function ($query) {
822
-                $query->where('archived', false);
823
-            }, 'account.subtype' => function ($query) {
824
-                $query->select(['id', 'name']);
825
-            }])
826
-            ->when($excludedAccountId, fn (Builder $query) => $query->where('account_id', '!=', $excludedAccountId))
827
-            ->when($currentBankAccountId, fn (Builder $query) => $query->orWhere('id', $currentBankAccountId))
828
-            ->get()
829
-            ->groupBy('account.subtype.name')
830
-            ->map(fn (Collection $bankAccounts, string $subtype) => $bankAccounts->pluck('account.name', 'id'))
831
-            ->toArray();
832
-    }
833
-
834
-    protected function getBankAccountAccountOptions(?int $excludedBankAccountId = null, ?int $currentAccountId = null): array
835
-    {
836
-        return Account::query()
837
-            ->whereHas('bankAccount', function (Builder $query) use ($excludedBankAccountId) {
838
-                // Exclude the specific bank account if provided
839
-                if ($excludedBankAccountId) {
840
-                    $query->whereNot('id', $excludedBankAccountId);
841
-                }
842
-            })
843
-            ->where(function (Builder $query) use ($currentAccountId) {
844
-                $query->where('archived', false)
845
-                    ->orWhere('id', $currentAccountId);
846
-            })
847
-            ->get()
848
-            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
849
-            ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
850
-            ->toArray();
851
-    }
852
-
853
-    protected function getChartAccountOptions(?TransactionType $type = null, ?bool $nominalAccountsOnly = null, ?int $currentAccountId = null): array
854
-    {
855
-        $nominalAccountsOnly ??= false;
856
-
857
-        $excludedCategory = match ($type) {
858
-            TransactionType::Deposit => AccountCategory::Expense,
859
-            TransactionType::Withdrawal => AccountCategory::Revenue,
860
-            default => null,
861
-        };
862
-
863
-        return Account::query()
864
-            ->when($nominalAccountsOnly, fn (Builder $query) => $query->doesntHave('bankAccount'))
865
-            ->when($excludedCategory, fn (Builder $query) => $query->whereNot('category', $excludedCategory))
866
-            ->where(function (Builder $query) use ($currentAccountId) {
867
-                $query->where('archived', false)
868
-                    ->orWhere('id', $currentAccountId);
869
-            })
870
-            ->get()
871
-            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
872
-            ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
873
-            ->toArray();
874
-    }
875
-
876
-    protected function getBalanceForAllAccounts(): string
877
-    {
878
-        return Accounting::getTotalBalanceForAllBankAccounts($this->fiscalYearStartDate, $this->fiscalYearEndDate)->format();
879
-    }
880
-}

+ 8
- 8
app/Filament/Company/Pages/Reports/AccountTransactions.php Wyświetl plik

@@ -4,7 +4,7 @@ namespace App\Filament\Company\Pages\Reports;
4 4
 
5 5
 use App\Contracts\ExportableReport;
6 6
 use App\DTO\ReportDTO;
7
-use App\Filament\Company\Pages\Accounting\Transactions;
7
+use App\Filament\Company\Resources\Accounting\TransactionResource;
8 8
 use App\Models\Accounting\Account;
9 9
 use App\Models\Common\Client;
10 10
 use App\Models\Common\Vendor;
@@ -48,8 +48,8 @@ class AccountTransactions extends BaseReportPage
48 48
             $this->setFilterState('selectedAccount', 'all');
49 49
         }
50 50
 
51
-        if (empty($this->getFilterState('selectedEntity'))) {
52
-            $this->setFilterState('selectedEntity', 'all');
51
+        if (empty($this->getFilterState('selectedPayee'))) {
52
+            $this->setFilterState('selectedPayee', 'all');
53 53
         }
54 54
     }
55 55
 
@@ -95,8 +95,8 @@ class AccountTransactions extends BaseReportPage
95 95
                 ])->extraFieldWrapperAttributes([
96 96
                     'class' => 'report-hidden-label',
97 97
                 ]),
98
-                Select::make('selectedEntity')
99
-                    ->label('Entity')
98
+                Select::make('selectedPayee')
99
+                    ->label('Payee')
100 100
                     ->options($this->getEntityOptions())
101 101
                     ->searchable()
102 102
                     ->selectablePlaceholder(false),
@@ -139,7 +139,7 @@ class AccountTransactions extends BaseReportPage
139 139
             ->toArray();
140 140
 
141 141
         $allEntitiesOption = [
142
-            'All Entities' => ['all' => 'All Entities'],
142
+            'All Payees' => ['all' => 'All Payees'],
143 143
         ];
144 144
 
145 145
         return $allEntitiesOption + [
@@ -155,7 +155,7 @@ class AccountTransactions extends BaseReportPage
155 155
             endDate: $this->getFormattedEndDate(),
156 156
             columns: $columns,
157 157
             accountId: $this->getFilterState('selectedAccount'),
158
-            entityId: $this->getFilterState('selectedEntity'),
158
+            entityId: $this->getFilterState('selectedPayee'),
159 159
         );
160 160
     }
161 161
 
@@ -194,7 +194,7 @@ class AccountTransactions extends BaseReportPage
194 194
         return [
195 195
             Action::make('createTransaction')
196 196
                 ->label('Create transaction')
197
-                ->url(Transactions::getUrl()),
197
+                ->url(TransactionResource::getUrl()),
198 198
         ];
199 199
     }
200 200
 

+ 1
- 3
app/Filament/Company/Pages/Reports/BaseReportPage.php Wyświetl plik

@@ -17,7 +17,6 @@ use Filament\Forms\Components\DatePicker;
17 17
 use Filament\Forms\Set;
18 18
 use Filament\Pages\Page;
19 19
 use Filament\Support\Enums\IconPosition;
20
-use Filament\Support\Enums\IconSize;
21 20
 use Illuminate\Support\Arr;
22 21
 use Illuminate\Support\Carbon;
23 22
 use Livewire\Attributes\Computed;
@@ -235,8 +234,7 @@ abstract class BaseReportPage extends Page
235 234
                 ->outlined()
236 235
                 ->dropdownWidth('max-w-[7rem]')
237 236
                 ->dropdownPlacement('bottom-end')
238
-                ->icon('heroicon-c-chevron-down')
239
-                ->iconSize(IconSize::Small)
237
+                ->icon('heroicon-m-chevron-down')
240 238
                 ->iconPosition(IconPosition::After),
241 239
         ];
242 240
     }

+ 324
- 0
app/Filament/Company/Resources/Accounting/TransactionResource.php Wyświetl plik

@@ -0,0 +1,324 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting;
4
+
5
+use App\Enums\Accounting\TransactionType;
6
+use App\Filament\Company\Resources\Accounting\TransactionResource\Pages;
7
+use App\Filament\Forms\Components\DateRangeSelect;
8
+use App\Filament\Tables\Actions\EditTransactionAction;
9
+use App\Filament\Tables\Actions\ReplicateBulkAction;
10
+use App\Models\Accounting\JournalEntry;
11
+use App\Models\Accounting\Transaction;
12
+use App\Models\Common\Client;
13
+use App\Models\Common\Vendor;
14
+use Exception;
15
+use Filament\Forms\Components\DatePicker;
16
+use Filament\Forms\Components\Grid;
17
+use Filament\Forms\Form;
18
+use Filament\Forms\Set;
19
+use Filament\Notifications\Notification;
20
+use Filament\Resources\Resource;
21
+use Filament\Support\Colors\Color;
22
+use Filament\Support\Enums\FontWeight;
23
+use Filament\Support\Enums\MaxWidth;
24
+use Filament\Tables;
25
+use Filament\Tables\Table;
26
+use Illuminate\Database\Eloquent\Builder;
27
+use Illuminate\Database\Eloquent\Collection;
28
+use Illuminate\Support\Carbon;
29
+
30
+class TransactionResource extends Resource
31
+{
32
+    protected static ?string $model = Transaction::class;
33
+
34
+    protected static ?string $recordTitleAttribute = 'description';
35
+
36
+    public static function form(Form $form): Form
37
+    {
38
+        return $form
39
+            ->schema([]);
40
+    }
41
+
42
+    public static function table(Table $table): Table
43
+    {
44
+        return $table
45
+            ->modifyQueryUsing(function (Builder $query) {
46
+                $query->with([
47
+                    'account',
48
+                    'bankAccount.account',
49
+                    'journalEntries.account',
50
+                    'payeeable',
51
+                ])
52
+                    ->where(function (Builder $query) {
53
+                        $query->whereNull('transactionable_id')
54
+                            ->orWhere('is_payment', true);
55
+                    });
56
+            })
57
+            ->columns([
58
+                Tables\Columns\TextColumn::make('posted_at')
59
+                    ->label('Date')
60
+                    ->sortable()
61
+                    ->defaultDateFormat(),
62
+                Tables\Columns\TextColumn::make('type')
63
+                    ->label('Type')
64
+                    ->sortable()
65
+                    ->toggleable(isToggledHiddenByDefault: true),
66
+                Tables\Columns\TextColumn::make('description')
67
+                    ->label('Description')
68
+                    ->limit(50)
69
+                    ->searchable()
70
+                    ->toggleable(),
71
+                Tables\Columns\TextColumn::make('payeeable.name')
72
+                    ->label('Payee')
73
+                    ->searchable()
74
+                    ->toggleable(isToggledHiddenByDefault: true),
75
+                Tables\Columns\TextColumn::make('bankAccount.account.name')
76
+                    ->label('Account')
77
+                    ->searchable()
78
+                    ->toggleable(),
79
+                Tables\Columns\TextColumn::make('account.name')
80
+                    ->label('Category')
81
+                    ->prefix(static fn (Transaction $transaction) => $transaction->type->isTransfer() ? 'Transfer to ' : null)
82
+                    ->searchable()
83
+                    ->toggleable()
84
+                    ->state(static fn (Transaction $transaction) => $transaction->account->name ?? 'Journal Entry'),
85
+                Tables\Columns\TextColumn::make('amount')
86
+                    ->label('Amount')
87
+                    ->weight(static fn (Transaction $transaction) => $transaction->reviewed ? null : FontWeight::SemiBold)
88
+                    ->color(
89
+                        static fn (Transaction $transaction) => match ($transaction->type) {
90
+                            TransactionType::Deposit => Color::rgb('rgb(' . Color::Green[700] . ')'),
91
+                            TransactionType::Journal => 'primary',
92
+                            default => null,
93
+                        }
94
+                    )
95
+                    ->sortable()
96
+                    ->currency(static fn (Transaction $transaction) => $transaction->bankAccount?->account->currency_code),
97
+            ])
98
+            ->defaultSort('posted_at', 'desc')
99
+            ->filters([
100
+                Tables\Filters\SelectFilter::make('bank_account_id')
101
+                    ->label('Account')
102
+                    ->searchable()
103
+                    ->options(static fn () => Transaction::getBankAccountOptions(excludeArchived: false)),
104
+                Tables\Filters\SelectFilter::make('account_id')
105
+                    ->label('Category')
106
+                    ->multiple()
107
+                    ->options(static fn () => Transaction::getChartAccountOptions()),
108
+                Tables\Filters\TernaryFilter::make('reviewed')
109
+                    ->label('Status')
110
+                    ->trueLabel('Reviewed')
111
+                    ->falseLabel('Not Reviewed'),
112
+                Tables\Filters\SelectFilter::make('type')
113
+                    ->label('Type')
114
+                    ->options(TransactionType::class),
115
+                Tables\Filters\TernaryFilter::make('is_payment')
116
+                    ->label('Payment')
117
+                    ->default(false),
118
+                Tables\Filters\SelectFilter::make('payee')
119
+                    ->label('Payee')
120
+                    ->options(static fn () => Transaction::getPayeeOptions())
121
+                    ->searchable()
122
+                    ->query(function (Builder $query, array $data): Builder {
123
+                        if (empty($data['value'])) {
124
+                            return $query;
125
+                        }
126
+
127
+                        $id = (int) $data['value'];
128
+
129
+                        if ($id < 0) {
130
+                            return $query->where('payeeable_type', Vendor::class)
131
+                                ->where('payeeable_id', abs($id));
132
+                        } else {
133
+                            return $query->where('payeeable_type', Client::class)
134
+                                ->where('payeeable_id', $id);
135
+                        }
136
+                    }),
137
+                static::buildDateRangeFilter('posted_at', 'Posted', true),
138
+                static::buildDateRangeFilter('updated_at', 'Last modified'),
139
+            ])
140
+            ->filtersFormSchema(fn (array $filters): array => [
141
+                Grid::make()
142
+                    ->schema([
143
+                        $filters['bank_account_id'],
144
+                        $filters['account_id'],
145
+                        $filters['reviewed'],
146
+                        $filters['type'],
147
+                        $filters['is_payment'],
148
+                        $filters['payee'],
149
+                    ])
150
+                    ->columnSpanFull()
151
+                    ->extraAttributes(['class' => 'border-b border-gray-200 dark:border-white/10 pb-8']),
152
+                $filters['posted_at'],
153
+                $filters['updated_at'],
154
+            ])
155
+            ->filtersFormWidth(MaxWidth::ThreeExtraLarge)
156
+            ->actions([
157
+                Tables\Actions\Action::make('markAsReviewed')
158
+                    ->label('Mark as reviewed')
159
+                    ->view('filament.company.components.tables.actions.mark-as-reviewed')
160
+                    ->icon(static fn (Transaction $transaction) => $transaction->reviewed ? 'heroicon-s-check-circle' : 'heroicon-o-check-circle')
161
+                    ->color(static fn (Transaction $transaction, Tables\Actions\Action $action) => match (static::determineTransactionState($transaction, $action)) {
162
+                        'reviewed' => 'primary',
163
+                        'unreviewed' => Color::rgb('rgb(' . Color::Gray[600] . ')'),
164
+                        'uncategorized' => 'gray',
165
+                    })
166
+                    ->tooltip(static fn (Transaction $transaction, Tables\Actions\Action $action) => match (static::determineTransactionState($transaction, $action)) {
167
+                        'reviewed' => 'Reviewed',
168
+                        'unreviewed' => 'Mark as reviewed',
169
+                        'uncategorized' => 'Categorize first to mark as reviewed',
170
+                    })
171
+                    ->disabled(fn (Transaction $transaction): bool => $transaction->isUncategorized())
172
+                    ->action(fn (Transaction $transaction) => $transaction->update(['reviewed' => ! $transaction->reviewed])),
173
+                Tables\Actions\ActionGroup::make([
174
+                    Tables\Actions\ActionGroup::make([
175
+                        EditTransactionAction::make(),
176
+                        Tables\Actions\ReplicateAction::make()
177
+                            ->excludeAttributes(['created_by', 'updated_by', 'created_at', 'updated_at'])
178
+                            ->modal(false)
179
+                            ->beforeReplicaSaved(static function (Transaction $replica) {
180
+                                $replica->description = '(Copy of) ' . $replica->description;
181
+                            })
182
+                            ->hidden(static fn (Transaction $transaction) => $transaction->transactionable_id)
183
+                            ->after(static function (Transaction $original, Transaction $replica) {
184
+                                $original->journalEntries->each(function (JournalEntry $entry) use ($replica) {
185
+                                    $entry->replicate([
186
+                                        'transaction_id',
187
+                                    ])->fill([
188
+                                        'transaction_id' => $replica->id,
189
+                                    ])->save();
190
+                                });
191
+                            }),
192
+                    ])->dropdown(false),
193
+                    Tables\Actions\DeleteAction::make(),
194
+                ]),
195
+            ])
196
+            ->bulkActions([
197
+                Tables\Actions\BulkActionGroup::make([
198
+                    Tables\Actions\DeleteBulkAction::make(),
199
+                    ReplicateBulkAction::make()
200
+                        ->label('Replicate')
201
+                        ->modalWidth(MaxWidth::Large)
202
+                        ->modalDescription('Replicating transactions will also replicate their journal entries. Are you sure you want to proceed?')
203
+                        ->successNotificationTitle('Transactions replicated successfully')
204
+                        ->failureNotificationTitle('Failed to replicate transactions')
205
+                        ->deselectRecordsAfterCompletion()
206
+                        ->excludeAttributes(['created_by', 'updated_by', 'created_at', 'updated_at'])
207
+                        ->beforeReplicaSaved(static function (Transaction $replica) {
208
+                            $replica->description = '(Copy of) ' . $replica->description;
209
+                        })
210
+                        ->before(function (Collection $records, ReplicateBulkAction $action) {
211
+                            $isInvalid = $records->contains(fn (Transaction $record) => $record->transactionable_id);
212
+
213
+                            if ($isInvalid) {
214
+                                Notification::make()
215
+                                    ->title('Cannot replicate transactions')
216
+                                    ->body('You cannot replicate transactions associated with bills or invoices')
217
+                                    ->persistent()
218
+                                    ->danger()
219
+                                    ->send();
220
+
221
+                                $action->cancel(true);
222
+                            }
223
+                        })
224
+                        ->withReplicatedRelationships(['journalEntries']),
225
+                ]),
226
+            ]);
227
+    }
228
+
229
+    public static function getRelations(): array
230
+    {
231
+        return [
232
+            //
233
+        ];
234
+    }
235
+
236
+    public static function getPages(): array
237
+    {
238
+        return [
239
+            'index' => Pages\ListTransactions::route('/'),
240
+            'view' => Pages\ViewTransaction::route('/{record}'),
241
+        ];
242
+    }
243
+
244
+    /**
245
+     * @throws Exception
246
+     */
247
+    public static function buildDateRangeFilter(string $fieldPrefix, string $label, bool $hasBottomBorder = false): Tables\Filters\Filter
248
+    {
249
+        return Tables\Filters\Filter::make($fieldPrefix)
250
+            ->columnSpanFull()
251
+            ->form([
252
+                Grid::make()
253
+                    ->live()
254
+                    ->schema([
255
+                        DateRangeSelect::make("{$fieldPrefix}_date_range")
256
+                            ->label($label)
257
+                            ->selectablePlaceholder(false)
258
+                            ->placeholder('Select a date range')
259
+                            ->startDateField("{$fieldPrefix}_start_date")
260
+                            ->endDateField("{$fieldPrefix}_end_date"),
261
+                        DatePicker::make("{$fieldPrefix}_start_date")
262
+                            ->label("{$label} from")
263
+                            ->columnStart(1)
264
+                            ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
265
+                                $set("{$fieldPrefix}_date_range", 'Custom');
266
+                            }),
267
+                        DatePicker::make("{$fieldPrefix}_end_date")
268
+                            ->label("{$label} to")
269
+                            ->afterStateUpdated(static function (Set $set) use ($fieldPrefix) {
270
+                                $set("{$fieldPrefix}_date_range", 'Custom');
271
+                            }),
272
+                    ])
273
+                    ->extraAttributes($hasBottomBorder ? ['class' => 'border-b border-gray-200 dark:border-white/10 pb-8'] : []),
274
+            ])
275
+            ->query(function (Builder $query, array $data) use ($fieldPrefix): Builder {
276
+                $query
277
+                    ->when($data["{$fieldPrefix}_start_date"], fn (Builder $query, $startDate) => $query->whereDate($fieldPrefix, '>=', $startDate))
278
+                    ->when($data["{$fieldPrefix}_end_date"], fn (Builder $query, $endDate) => $query->whereDate($fieldPrefix, '<=', $endDate));
279
+
280
+                return $query;
281
+            })
282
+            ->indicateUsing(function (array $data) use ($fieldPrefix, $label): array {
283
+                $indicators = [];
284
+
285
+                static::addIndicatorForDateRange($data, "{$fieldPrefix}_start_date", "{$fieldPrefix}_end_date", $label, $indicators);
286
+
287
+                return $indicators;
288
+            });
289
+
290
+    }
291
+
292
+    public static function addIndicatorForDateRange($data, $startKey, $endKey, $labelPrefix, &$indicators): void
293
+    {
294
+        $formattedStartDate = filled($data[$startKey]) ? Carbon::parse($data[$startKey])->toFormattedDateString() : null;
295
+        $formattedEndDate = filled($data[$endKey]) ? Carbon::parse($data[$endKey])->toFormattedDateString() : null;
296
+        if ($formattedStartDate && $formattedEndDate) {
297
+            // If both start and end dates are set, show the combined date range as the indicator, no specific field needs to be removed since the entire filter will be removed
298
+            $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix}: {$formattedStartDate} - {$formattedEndDate}");
299
+        } else {
300
+            if ($formattedStartDate) {
301
+                $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix} After: {$formattedStartDate}")
302
+                    ->removeField($startKey);
303
+            }
304
+
305
+            if ($formattedEndDate) {
306
+                $indicators[] = Tables\Filters\Indicator::make("{$labelPrefix} Before: {$formattedEndDate}")
307
+                    ->removeField($endKey);
308
+            }
309
+        }
310
+    }
311
+
312
+    protected static function determineTransactionState(Transaction $transaction, Tables\Actions\Action $action): string
313
+    {
314
+        if ($transaction->reviewed) {
315
+            return 'reviewed';
316
+        }
317
+
318
+        if ($transaction->reviewed === false && $action->isEnabled()) {
319
+            return 'unreviewed';
320
+        }
321
+
322
+        return 'uncategorized';
323
+    }
324
+}

+ 63
- 0
app/Filament/Company/Resources/Accounting/TransactionResource/Pages/ListTransactions.php Wyświetl plik

@@ -0,0 +1,63 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\TransactionResource\Pages;
4
+
5
+use App\Concerns\HasJournalEntryActions;
6
+use App\Enums\Accounting\TransactionType;
7
+use App\Filament\Actions\CreateTransactionAction;
8
+use App\Filament\Company\Pages\Service\ConnectedAccount;
9
+use App\Filament\Company\Resources\Accounting\TransactionResource;
10
+use App\Services\PlaidService;
11
+use Filament\Actions;
12
+use Filament\Resources\Pages\ListRecords;
13
+use Filament\Support\Enums\IconPosition;
14
+use Filament\Support\Enums\MaxWidth;
15
+
16
+class ListTransactions extends ListRecords
17
+{
18
+    use HasJournalEntryActions;
19
+
20
+    protected static string $resource = TransactionResource::class;
21
+
22
+    public function getMaxContentWidth(): MaxWidth | string | null
23
+    {
24
+        return 'max-w-8xl';
25
+    }
26
+
27
+    protected function getHeaderActions(): array
28
+    {
29
+        return [
30
+            Actions\ActionGroup::make([
31
+                CreateTransactionAction::make('createDeposit')
32
+                    ->label('Deposit')
33
+                    ->type(TransactionType::Deposit),
34
+                CreateTransactionAction::make('createWithdrawal')
35
+                    ->label('Withdrawal')
36
+                    ->type(TransactionType::Withdrawal),
37
+                CreateTransactionAction::make('createTransfer')
38
+                    ->label('Transfer')
39
+                    ->type(TransactionType::Transfer),
40
+                CreateTransactionAction::make('createJournalEntry')
41
+                    ->label('Journal entry')
42
+                    ->type(TransactionType::Journal),
43
+            ])
44
+                ->label('New transaction')
45
+                ->button()
46
+                ->dropdownPlacement('bottom-end')
47
+                ->icon('heroicon-m-chevron-down')
48
+                ->iconPosition(IconPosition::After),
49
+            Actions\ActionGroup::make([
50
+                Actions\Action::make('connectBank')
51
+                    ->label('Connect your bank')
52
+                    ->visible(app(PlaidService::class)->isEnabled())
53
+                    ->url(ConnectedAccount::getUrl()),
54
+            ])
55
+                ->label('More')
56
+                ->button()
57
+                ->outlined()
58
+                ->dropdownPlacement('bottom-end')
59
+                ->icon('heroicon-m-chevron-down')
60
+                ->iconPosition(IconPosition::After),
61
+        ];
62
+    }
63
+}

+ 162
- 0
app/Filament/Company/Resources/Accounting/TransactionResource/Pages/ViewTransaction.php Wyświetl plik

@@ -0,0 +1,162 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\TransactionResource\Pages;
4
+
5
+use App\Filament\Actions\EditTransactionAction;
6
+use App\Filament\Company\Resources\Accounting\TransactionResource;
7
+use App\Filament\Company\Resources\Purchases\BillResource\Pages\ViewBill;
8
+use App\Filament\Company\Resources\Purchases\VendorResource;
9
+use App\Filament\Company\Resources\Sales\ClientResource;
10
+use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ViewInvoice;
11
+use App\Filament\Infolists\Components\BannerEntry;
12
+use App\Models\Accounting\Bill;
13
+use App\Models\Accounting\Invoice;
14
+use App\Models\Accounting\JournalEntry;
15
+use App\Models\Accounting\Transaction;
16
+use App\Models\Common\Client;
17
+use App\Models\Common\Vendor;
18
+use App\Utilities\Currency\CurrencyAccessor;
19
+use Filament\Actions;
20
+use Filament\Infolists\Components\IconEntry;
21
+use Filament\Infolists\Components\Section;
22
+use Filament\Infolists\Components\TextEntry;
23
+use Filament\Infolists\Infolist;
24
+use Filament\Resources\Pages\ViewRecord;
25
+use Filament\Support\Enums\IconPosition;
26
+
27
+use function Filament\Support\get_model_label;
28
+
29
+class ViewTransaction extends ViewRecord
30
+{
31
+    protected static string $resource = TransactionResource::class;
32
+
33
+    protected $listeners = [
34
+        'refresh' => '$refresh',
35
+    ];
36
+
37
+    protected function getHeaderActions(): array
38
+    {
39
+        return [
40
+            EditTransactionAction::make()
41
+                ->outlined()
42
+                ->after(fn () => $this->dispatch('refresh')),
43
+            Actions\ViewAction::make('viewAssociatedDocument')
44
+                ->outlined()
45
+                ->icon('heroicon-o-document-text')
46
+                ->hidden(static fn (Transaction $record): bool => ! $record->transactionable_id)
47
+                ->label(static function (Transaction $record) {
48
+                    if (! $record->transactionable_type) {
49
+                        return 'View document';
50
+                    }
51
+
52
+                    return 'View ' . get_model_label($record->transactionable_type);
53
+                })
54
+                ->url(static function (Transaction $record) {
55
+                    return match ($record->transactionable_type) {
56
+                        Bill::class => ViewBill::getUrl(['record' => $record->transactionable_id]),
57
+                        Invoice::class => ViewInvoice::getUrl(['record' => $record->transactionable_id]),
58
+                        default => null,
59
+                    };
60
+                }),
61
+            Actions\ActionGroup::make([
62
+                Actions\ActionGroup::make([
63
+                    Actions\Action::make('markAsReviewed')
64
+                        ->label(static fn (Transaction $record) => $record->reviewed ? 'Mark as unreviewed' : 'Mark as reviewed')
65
+                        ->icon(static fn (Transaction $record) => $record->reviewed ? 'heroicon-s-check-circle' : 'heroicon-o-check-circle')
66
+                        ->hidden(fn (Transaction $record): bool => $record->isUncategorized())
67
+                        ->action(fn (Transaction $record) => $record->update(['reviewed' => ! $record->reviewed])),
68
+                    Actions\ReplicateAction::make()
69
+                        ->excludeAttributes(['created_by', 'updated_by', 'created_at', 'updated_at'])
70
+                        ->modal(false)
71
+                        ->beforeReplicaSaved(static function (Transaction $replica) {
72
+                            $replica->description = '(Copy of) ' . $replica->description;
73
+                        })
74
+                        ->hidden(static fn (Transaction $transaction) => $transaction->transactionable_id)
75
+                        ->after(static function (Transaction $original, Transaction $replica) {
76
+                            $original->journalEntries->each(function (JournalEntry $entry) use ($replica) {
77
+                                $entry->replicate([
78
+                                    'transaction_id',
79
+                                ])->fill([
80
+                                    'transaction_id' => $replica->id,
81
+                                ])->save();
82
+                            });
83
+                        }),
84
+                ])->dropdown(false),
85
+                Actions\DeleteAction::make(),
86
+            ])
87
+                ->label('Actions')
88
+                ->button()
89
+                ->outlined()
90
+                ->dropdownPlacement('bottom-end')
91
+                ->icon('heroicon-m-chevron-down')
92
+                ->iconPosition(IconPosition::After),
93
+        ];
94
+    }
95
+
96
+    public function infolist(Infolist $infolist): Infolist
97
+    {
98
+        return $infolist
99
+            ->schema([
100
+                BannerEntry::make('transactionUncategorized')
101
+                    ->warning()
102
+                    ->title('Transaction uncategorized')
103
+                    ->description('You must categorize this transaction before you can mark it as reviewed.')
104
+                    ->visible(fn (Transaction $record) => $record->isUncategorized())
105
+                    ->columnSpanFull(),
106
+                Section::make('Transaction Details')
107
+                    ->columns(3)
108
+                    ->schema([
109
+                        TextEntry::make('posted_at')
110
+                            ->label('Date')
111
+                            ->date(),
112
+                        TextEntry::make('type')
113
+                            ->badge(),
114
+                        IconEntry::make('is_payment')
115
+                            ->label('Payment')
116
+                            ->boolean(),
117
+                        TextEntry::make('description')
118
+                            ->label('Description'),
119
+                        TextEntry::make('bankAccount.account.name')
120
+                            ->label('Account')
121
+                            ->hidden(static fn (Transaction $record): bool => ! $record->bankAccount),
122
+                        TextEntry::make('payeeable.name')
123
+                            ->label('Payee')
124
+                            ->hidden(static fn (Transaction $record): bool => ! $record->payeeable_type)
125
+                            ->url(static function (Transaction $record): ?string {
126
+                                if (! $record->payeeable_type) {
127
+                                    return null;
128
+                                }
129
+
130
+                                return match ($record->payeeable_type) {
131
+                                    Vendor::class => VendorResource::getUrl('view', ['record' => $record->payeeable_id]),
132
+                                    Client::class => ClientResource::getUrl('view', ['record' => $record->payeeable_id]),
133
+                                    default => null,
134
+                                };
135
+                            })
136
+                            ->link(),
137
+                        TextEntry::make('account.name')
138
+                            ->label('Category')
139
+                            ->hidden(static fn (Transaction $record): bool => ! $record->account),
140
+                        TextEntry::make('amount')
141
+                            ->label('Amount')
142
+                            ->currency(static fn (Transaction $record) => $record->bankAccount?->account->currency_code ?? CurrencyAccessor::getDefaultCurrency()),
143
+                        TextEntry::make('reviewed')
144
+                            ->label('Status')
145
+                            ->badge()
146
+                            ->formatStateUsing(static fn (bool $state): string => $state ? 'Reviewed' : 'Not Reviewed')
147
+                            ->color(static fn (bool $state): string => $state ? 'success' : 'warning'),
148
+                        TextEntry::make('notes')
149
+                            ->label('Notes')
150
+                            ->columnSpan(2)
151
+                            ->visible(static fn (Transaction $record): bool => filled($record->notes)),
152
+                    ]),
153
+            ]);
154
+    }
155
+
156
+    protected function getAllRelationManagers(): array
157
+    {
158
+        return [
159
+            TransactionResource\RelationManagers\JournalEntriesRelationManager::class,
160
+        ];
161
+    }
162
+}

+ 62
- 0
app/Filament/Company/Resources/Accounting/TransactionResource/RelationManagers/JournalEntriesRelationManager.php Wyświetl plik

@@ -0,0 +1,62 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\TransactionResource\RelationManagers;
4
+
5
+use App\Utilities\Currency\CurrencyAccessor;
6
+use Filament\Forms\Form;
7
+use Filament\Resources\RelationManagers\RelationManager;
8
+use Filament\Support\Enums\FontWeight;
9
+use Filament\Tables;
10
+use Filament\Tables\Table;
11
+
12
+class JournalEntriesRelationManager extends RelationManager
13
+{
14
+    protected static string $relationship = 'journalEntries';
15
+
16
+    protected $listeners = [
17
+        'refresh' => '$refresh',
18
+    ];
19
+
20
+    public function form(Form $form): Form
21
+    {
22
+        return $form
23
+            ->schema([]);
24
+    }
25
+
26
+    public function table(Table $table): Table
27
+    {
28
+        return $table
29
+            ->columns([
30
+                Tables\Columns\TextColumn::make('type')
31
+                    ->label('Type'),
32
+                Tables\Columns\TextColumn::make('account.name')
33
+                    ->label('Account')
34
+                    ->searchable()
35
+                    ->sortable(),
36
+                Tables\Columns\TextColumn::make('account.category')
37
+                    ->label('Category')
38
+                    ->badge(),
39
+                Tables\Columns\TextColumn::make('description')
40
+                    ->label('Description')
41
+                    ->searchable()
42
+                    ->limit(50),
43
+                Tables\Columns\TextColumn::make('amount')
44
+                    ->label('Amount')
45
+                    ->weight(FontWeight::SemiBold)
46
+                    ->sortable()
47
+                    ->currency(CurrencyAccessor::getDefaultCurrency()),
48
+            ])
49
+            ->filters([
50
+                //
51
+            ])
52
+            ->headerActions([
53
+                //
54
+            ])
55
+            ->actions([
56
+                //
57
+            ])
58
+            ->bulkActions([
59
+                //
60
+            ]);
61
+    }
62
+}

+ 3
- 7
app/Filament/Company/Resources/Purchases/BillResource/Pages/ViewBill.php Wyświetl plik

@@ -10,9 +10,7 @@ use Filament\Infolists\Components\Section;
10 10
 use Filament\Infolists\Components\TextEntry;
11 11
 use Filament\Infolists\Infolist;
12 12
 use Filament\Resources\Pages\ViewRecord;
13
-use Filament\Support\Enums\FontWeight;
14 13
 use Filament\Support\Enums\IconPosition;
15
-use Filament\Support\Enums\IconSize;
16 14
 
17 15
 class ViewBill extends ViewRecord
18 16
 {
@@ -38,8 +36,7 @@ class ViewBill extends ViewRecord
38 36
                 ->button()
39 37
                 ->outlined()
40 38
                 ->dropdownPlacement('bottom-end')
41
-                ->icon('heroicon-c-chevron-down')
42
-                ->iconSize(IconSize::Small)
39
+                ->icon('heroicon-m-chevron-down')
43 40
                 ->iconPosition(IconPosition::After),
44 41
         ];
45 42
     }
@@ -57,9 +54,8 @@ class ViewBill extends ViewRecord
57 54
                             ->badge(),
58 55
                         TextEntry::make('vendor.name')
59 56
                             ->label('Vendor')
60
-                            ->color('primary')
61
-                            ->weight(FontWeight::SemiBold)
62
-                            ->url(static fn (Bill $record) => VendorResource::getUrl('view', ['record' => $record->vendor_id])),
57
+                            ->url(static fn (Bill $record) => VendorResource::getUrl('view', ['record' => $record->vendor_id]))
58
+                            ->link(),
63 59
                         TextEntry::make('total')
64 60
                             ->label('Total')
65 61
                             ->currency(fn (Bill $record) => $record->currency_code),

+ 3
- 4
app/Filament/Company/Resources/Purchases/VendorResource/Pages/ViewVendor.php Wyświetl plik

@@ -14,7 +14,6 @@ use Filament\Infolists\Components\TextEntry;
14 14
 use Filament\Infolists\Infolist;
15 15
 use Filament\Resources\Pages\ViewRecord;
16 16
 use Filament\Support\Enums\IconPosition;
17
-use Filament\Support\Enums\IconSize;
18 17
 
19 18
 class ViewVendor extends ViewRecord
20 19
 {
@@ -51,8 +50,7 @@ class ViewVendor extends ViewRecord
51 50
                 ->button()
52 51
                 ->outlined()
53 52
                 ->dropdownPlacement('bottom-end')
54
-                ->icon('heroicon-c-chevron-down')
55
-                ->iconSize(IconSize::Small)
53
+                ->icon('heroicon-m-chevron-down')
56 54
                 ->iconPosition(IconPosition::After),
57 55
         ];
58 56
     }
@@ -79,7 +77,8 @@ class ViewVendor extends ViewRecord
79 77
                             ->label('Primary phone'),
80 78
                         TextEntry::make('website')
81 79
                             ->label('Website')
82
-                            ->url(static fn ($state) => $state, true),
80
+                            ->url(static fn ($state) => $state, true)
81
+                            ->link(),
83 82
                     ]),
84 83
                 Section::make('Additional Details')
85 84
                     ->columns()

+ 3
- 4
app/Filament/Company/Resources/Sales/ClientResource/Pages/ViewClient.php Wyświetl plik

@@ -16,7 +16,6 @@ use Filament\Infolists\Components\TextEntry;
16 16
 use Filament\Infolists\Infolist;
17 17
 use Filament\Resources\Pages\ViewRecord;
18 18
 use Filament\Support\Enums\IconPosition;
19
-use Filament\Support\Enums\IconSize;
20 19
 use Illuminate\Contracts\Support\Htmlable;
21 20
 
22 21
 class ViewClient extends ViewRecord
@@ -64,8 +63,7 @@ class ViewClient extends ViewRecord
64 63
                 ->button()
65 64
                 ->outlined()
66 65
                 ->dropdownPlacement('bottom-end')
67
-                ->icon('heroicon-c-chevron-down')
68
-                ->iconSize(IconSize::Small)
66
+                ->icon('heroicon-m-chevron-down')
69 67
                 ->iconPosition(IconPosition::After),
70 68
         ];
71 69
     }
@@ -92,7 +90,8 @@ class ViewClient extends ViewRecord
92 90
                             ->label('Primary phone'),
93 91
                         TextEntry::make('website')
94 92
                             ->label('Website')
95
-                            ->url(static fn ($state) => $state, true),
93
+                            ->url(static fn ($state) => $state, true)
94
+                            ->link(),
96 95
                     ]),
97 96
                 Section::make('Additional Details')
98 97
                     ->columns()

+ 3
- 7
app/Filament/Company/Resources/Sales/EstimateResource/Pages/ViewEstimate.php Wyświetl plik

@@ -14,9 +14,7 @@ use Filament\Infolists\Components\Section;
14 14
 use Filament\Infolists\Components\TextEntry;
15 15
 use Filament\Infolists\Infolist;
16 16
 use Filament\Resources\Pages\ViewRecord;
17
-use Filament\Support\Enums\FontWeight;
18 17
 use Filament\Support\Enums\IconPosition;
19
-use Filament\Support\Enums\IconSize;
20 18
 use Illuminate\Support\HtmlString;
21 19
 
22 20
 class ViewEstimate extends ViewRecord
@@ -49,8 +47,7 @@ class ViewEstimate extends ViewRecord
49 47
                 ->button()
50 48
                 ->outlined()
51 49
                 ->dropdownPlacement('bottom-end')
52
-                ->icon('heroicon-c-chevron-down')
53
-                ->iconSize(IconSize::Small)
50
+                ->icon('heroicon-m-chevron-down')
54 51
                 ->iconPosition(IconPosition::After),
55 52
         ];
56 53
     }
@@ -95,9 +92,8 @@ class ViewEstimate extends ViewRecord
95 92
                                     ->badge(),
96 93
                                 TextEntry::make('client.name')
97 94
                                     ->label('Client')
98
-                                    ->color('primary')
99
-                                    ->weight(FontWeight::SemiBold)
100
-                                    ->url(static fn (Estimate $record) => ClientResource::getUrl('view', ['record' => $record->client_id])),
95
+                                    ->url(static fn (Estimate $record) => ClientResource::getUrl('view', ['record' => $record->client_id]))
96
+                                    ->link(),
101 97
                                 TextEntry::make('expiration_date')
102 98
                                     ->label('Expiration date')
103 99
                                     ->asRelativeDay(),

+ 3
- 8
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ViewInvoice.php Wyświetl plik

@@ -14,10 +14,7 @@ use Filament\Infolists\Components\Section;
14 14
 use Filament\Infolists\Components\TextEntry;
15 15
 use Filament\Infolists\Infolist;
16 16
 use Filament\Resources\Pages\ViewRecord;
17
-use Filament\Support\Enums\FontWeight;
18 17
 use Filament\Support\Enums\IconPosition;
19
-use Filament\Support\Enums\IconSize;
20
-use Filament\Support\Enums\MaxWidth;
21 18
 use Illuminate\Support\HtmlString;
22 19
 
23 20
 class ViewInvoice extends ViewRecord
@@ -47,8 +44,7 @@ class ViewInvoice extends ViewRecord
47 44
                 ->button()
48 45
                 ->outlined()
49 46
                 ->dropdownPlacement('bottom-end')
50
-                ->icon('heroicon-c-chevron-down')
51
-                ->iconSize(IconSize::Small)
47
+                ->icon('heroicon-m-chevron-down')
52 48
                 ->iconPosition(IconPosition::After),
53 49
         ];
54 50
     }
@@ -93,9 +89,8 @@ class ViewInvoice extends ViewRecord
93 89
                                     ->badge(),
94 90
                                 TextEntry::make('client.name')
95 91
                                     ->label('Client')
96
-                                    ->color('primary')
97
-                                    ->weight(FontWeight::SemiBold)
98
-                                    ->url(static fn (Invoice $record) => ClientResource::getUrl('view', ['record' => $record->client_id])),
92
+                                    ->url(static fn (Invoice $record) => ClientResource::getUrl('view', ['record' => $record->client_id]))
93
+                                    ->link(),
99 94
                                 TextEntry::make('amount_due')
100 95
                                     ->label('Amount due')
101 96
                                     ->currency(static fn (Invoice $record) => $record->currency_code),

+ 4
- 5
app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php Wyświetl plik

@@ -18,7 +18,6 @@ use Filament\Infolists\Infolist;
18 18
 use Filament\Resources\Pages\ViewRecord;
19 19
 use Filament\Support\Enums\FontWeight;
20 20
 use Filament\Support\Enums\IconPosition;
21
-use Filament\Support\Enums\IconSize;
22 21
 use Illuminate\Support\HtmlString;
23 22
 use Illuminate\Support\Str;
24 23
 
@@ -44,8 +43,7 @@ class ViewRecurringInvoice extends ViewRecord
44 43
                 ->button()
45 44
                 ->outlined()
46 45
                 ->dropdownPlacement('bottom-end')
47
-                ->icon('heroicon-c-chevron-down')
48
-                ->iconSize(IconSize::Small)
46
+                ->icon('heroicon-m-chevron-down')
49 47
                 ->iconPosition(IconPosition::After),
50 48
         ];
51 49
     }
@@ -91,7 +89,7 @@ class ViewRecurringInvoice extends ViewRecord
91 89
                     ]),
92 90
                 BannerEntry::make('readyToApprove')
93 91
                     ->info()
94
-                    ->title('Ready to Approve')
92
+                    ->title('Ready to approve')
95 93
                     ->description('This recurring invoice is ready for approval. Review the details, and approve it when you’re ready to start generating invoices.')
96 94
                     ->visible(fn (RecurringInvoice $record) => $record->canBeApproved() && ! $record->hasInactiveAdjustments())
97 95
                     ->columnSpanFull()
@@ -110,7 +108,8 @@ class ViewRecurringInvoice extends ViewRecord
110 108
                                     ->label('Client')
111 109
                                     ->color('primary')
112 110
                                     ->weight(FontWeight::SemiBold)
113
-                                    ->url(static fn (RecurringInvoice $record) => ClientResource::getUrl('view', ['record' => $record->client_id])),
111
+                                    ->url(static fn (RecurringInvoice $record) => ClientResource::getUrl('view', ['record' => $record->client_id]))
112
+                                    ->link(),
114 113
                                 TextEntry::make('last_date')
115 114
                                     ->label('Last invoice')
116 115
                                     ->date()

+ 81
- 0
app/Filament/Tables/Actions/EditTransactionAction.php Wyświetl plik

@@ -0,0 +1,81 @@
1
+<?php
2
+
3
+namespace App\Filament\Tables\Actions;
4
+
5
+use App\Concerns\HasTransactionAction;
6
+use App\Enums\Accounting\TransactionType;
7
+use App\Models\Accounting\Transaction;
8
+use Filament\Actions\StaticAction;
9
+use Filament\Forms\Form;
10
+use Filament\Support\Enums\MaxWidth;
11
+use Filament\Tables\Actions\EditAction;
12
+
13
+class EditTransactionAction extends EditAction
14
+{
15
+    use HasTransactionAction;
16
+
17
+    protected function setUp(): void
18
+    {
19
+        parent::setUp();
20
+
21
+        $this->type(static function (Transaction $record) {
22
+            return $record->type;
23
+        });
24
+
25
+        $this->label(function () {
26
+            return match ($this->getTransactionType()) {
27
+                TransactionType::Journal => 'Edit journal entry',
28
+                default => 'Edit transaction',
29
+            };
30
+        });
31
+
32
+        $this->slideOver();
33
+
34
+        $this->modalWidth(function (): MaxWidth {
35
+            return match ($this->getTransactionType()) {
36
+                TransactionType::Journal => MaxWidth::Screen,
37
+                default => MaxWidth::ThreeExtraLarge,
38
+            };
39
+        });
40
+
41
+        $this->extraModalWindowAttributes(function (): array {
42
+            if ($this->getTransactionType() === TransactionType::Journal) {
43
+                return ['class' => 'journal-transaction-modal'];
44
+            }
45
+
46
+            return [];
47
+        });
48
+
49
+        $this->form(function (Form $form) {
50
+            return match ($this->getTransactionType()) {
51
+                TransactionType::Transfer => $this->transferForm($form),
52
+                TransactionType::Journal => $this->journalTransactionForm($form),
53
+                default => $this->transactionForm($form),
54
+            };
55
+        });
56
+
57
+        $this->afterFormFilled(function (Transaction $record) {
58
+            if ($this->getTransactionType() === TransactionType::Journal) {
59
+                $debitAmounts = $record->journalEntries->sumDebits()->getAmount();
60
+                $creditAmounts = $record->journalEntries->sumCredits()->getAmount();
61
+
62
+                $this->setDebitAmount($debitAmounts);
63
+                $this->setCreditAmount($creditAmounts);
64
+            }
65
+        });
66
+
67
+        $this->modalSubmitAction(function (StaticAction $action) {
68
+            if ($this->getTransactionType() === TransactionType::Journal) {
69
+                $action->disabled(! $this->isJournalEntryBalanced());
70
+            }
71
+
72
+            return $action;
73
+        });
74
+
75
+        $this->after(function (Transaction $transaction) {
76
+            if ($this->getTransactionType() === TransactionType::Journal) {
77
+                $transaction->updateAmountIfBalanced();
78
+            }
79
+        });
80
+    }
81
+}

+ 169
- 0
app/Models/Accounting/Transaction.php Wyświetl plik

@@ -5,19 +5,28 @@ namespace App\Models\Accounting;
5 5
 use App\Casts\TransactionAmountCast;
6 6
 use App\Concerns\Blamable;
7 7
 use App\Concerns\CompanyOwned;
8
+use App\Enums\Accounting\AccountCategory;
9
+use App\Enums\Accounting\AccountType;
8 10
 use App\Enums\Accounting\PaymentMethod;
9 11
 use App\Enums\Accounting\TransactionType;
12
+use App\Filament\Company\Resources\Accounting\TransactionResource\Pages\ViewTransaction;
13
+use App\Filament\Company\Resources\Purchases\BillResource\Pages\ViewBill;
14
+use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ViewInvoice;
10 15
 use App\Models\Banking\BankAccount;
16
+use App\Models\Common\Client;
11 17
 use App\Models\Common\Contact;
18
+use App\Models\Common\Vendor;
12 19
 use App\Observers\TransactionObserver;
13 20
 use Database\Factories\Accounting\TransactionFactory;
14 21
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
22
+use Illuminate\Database\Eloquent\Builder;
15 23
 use Illuminate\Database\Eloquent\Factories\Factory;
16 24
 use Illuminate\Database\Eloquent\Factories\HasFactory;
17 25
 use Illuminate\Database\Eloquent\Model;
18 26
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19 27
 use Illuminate\Database\Eloquent\Relations\HasMany;
20 28
 use Illuminate\Database\Eloquent\Relations\MorphTo;
29
+use Illuminate\Support\Collection;
21 30
 
22 31
 #[ObservedBy(TransactionObserver::class)]
23 32
 class Transaction extends Model
@@ -83,11 +92,21 @@ class Transaction extends Model
83 92
         return $this->morphTo();
84 93
     }
85 94
 
95
+    public function payeeable(): MorphTo
96
+    {
97
+        return $this->morphTo();
98
+    }
99
+
86 100
     public function isUncategorized(): bool
87 101
     {
88 102
         return $this->journalEntries->contains(fn (JournalEntry $entry) => $entry->account->isUncategorized());
89 103
     }
90 104
 
105
+    public function isPayment(): bool
106
+    {
107
+        return $this->is_payment;
108
+    }
109
+
91 110
     public function updateAmountIfBalanced(): void
92 111
     {
93 112
         if ($this->journalEntries->areBalanced() && $this->journalEntries->sumDebits()->formatSimple() !== $this->getAttributeValue('amount')) {
@@ -96,6 +115,156 @@ class Transaction extends Model
96 115
         }
97 116
     }
98 117
 
118
+    public static function getBankAccountOptions(?int $excludedAccountId = null, ?int $currentBankAccountId = null, bool $excludeArchived = true): array
119
+    {
120
+        return BankAccount::query()
121
+            ->whereHas('account', function (Builder $query) use ($excludeArchived) {
122
+                if ($excludeArchived) {
123
+                    $query->where('archived', false);
124
+                }
125
+            })
126
+            ->with(['account' => function ($query) use ($excludeArchived) {
127
+                if ($excludeArchived) {
128
+                    $query->where('archived', false);
129
+                }
130
+            }, 'account.subtype' => function ($query) {
131
+                $query->select(['id', 'name']);
132
+            }])
133
+            ->when($excludedAccountId, fn (Builder $query) => $query->where('account_id', '!=', $excludedAccountId))
134
+            ->when($currentBankAccountId, fn (Builder $query) => $query->orWhere('id', $currentBankAccountId))
135
+            ->get()
136
+            ->groupBy('account.subtype.name')
137
+            ->map(fn (Collection $bankAccounts, string $subtype) => $bankAccounts->pluck('account.name', 'id'))
138
+            ->toArray();
139
+    }
140
+
141
+    public static function getBankAccountAccountOptions(?int $excludedBankAccountId = null, ?int $currentAccountId = null): array
142
+    {
143
+        return Account::query()
144
+            ->whereHas('bankAccount', function (Builder $query) use ($excludedBankAccountId) {
145
+                // Exclude the specific bank account if provided
146
+                if ($excludedBankAccountId) {
147
+                    $query->whereNot('id', $excludedBankAccountId);
148
+                }
149
+            })
150
+            ->where(function (Builder $query) use ($currentAccountId) {
151
+                $query->where('archived', false)
152
+                    ->orWhere('id', $currentAccountId);
153
+            })
154
+            ->get()
155
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
156
+            ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
157
+            ->toArray();
158
+    }
159
+
160
+    public static function getChartAccountOptions(): array
161
+    {
162
+        return Account::query()
163
+            ->select(['id', 'name', 'category'])
164
+            ->get()
165
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
166
+            ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
167
+            ->toArray();
168
+    }
169
+
170
+    public static function getTransactionAccountOptions(
171
+        TransactionType $type,
172
+        ?int $currentAccountId = null
173
+    ): array {
174
+        $associatedAccountTypes = match ($type) {
175
+            TransactionType::Deposit => [
176
+                AccountType::OperatingRevenue,     // Sales, service income
177
+                AccountType::NonOperatingRevenue,  // Interest, dividends received
178
+                AccountType::CurrentLiability,     // Loans received
179
+                AccountType::NonCurrentLiability,  // Long-term financing
180
+                AccountType::Equity,               // Owner contributions
181
+                AccountType::ContraExpense,        // Refunds of expenses
182
+                AccountType::UncategorizedRevenue,
183
+            ],
184
+            TransactionType::Withdrawal => [
185
+                AccountType::OperatingExpense,     // Regular business expenses
186
+                AccountType::NonOperatingExpense,  // Interest paid, etc.
187
+                AccountType::CurrentLiability,     // Loan payments
188
+                AccountType::NonCurrentLiability,  // Long-term debt payments
189
+                AccountType::Equity,               // Owner withdrawals
190
+                AccountType::ContraRevenue,        // Customer refunds, discounts
191
+                AccountType::UncategorizedExpense,
192
+            ],
193
+            default => null,
194
+        };
195
+
196
+        return Account::query()
197
+            ->doesntHave('adjustment')
198
+            ->doesntHave('bankAccount')
199
+            ->when($associatedAccountTypes, fn (Builder $query) => $query->whereIn('type', $associatedAccountTypes))
200
+            ->where(function (Builder $query) use ($currentAccountId) {
201
+                $query->where('archived', false)
202
+                    ->orWhere('id', $currentAccountId);
203
+            })
204
+            ->get()
205
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
206
+            ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
207
+            ->toArray();
208
+    }
209
+
210
+    public static function getJournalAccountOptions(
211
+        ?int $currentAccountId = null
212
+    ): array {
213
+        return Account::query()
214
+            ->where(function (Builder $query) use ($currentAccountId) {
215
+                $query->where('archived', false)
216
+                    ->orWhere('id', $currentAccountId);
217
+            })
218
+            ->get()
219
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
220
+            ->map(fn (Collection $accounts, string $category) => $accounts->pluck('name', 'id'))
221
+            ->toArray();
222
+    }
223
+
224
+    public static function getUncategorizedAccountByType(TransactionType $type): ?Account
225
+    {
226
+        [$category, $accountName] = match ($type) {
227
+            TransactionType::Deposit => [AccountCategory::Revenue, 'Uncategorized Income'],
228
+            TransactionType::Withdrawal => [AccountCategory::Expense, 'Uncategorized Expense'],
229
+            default => [null, null],
230
+        };
231
+
232
+        return Account::where('category', $category)
233
+            ->where('name', $accountName)
234
+            ->first();
235
+    }
236
+
237
+    public static function getPayeeOptions(): array
238
+    {
239
+        $clients = Client::query()
240
+            ->orderBy('name')
241
+            ->pluck('name', 'id')
242
+            ->toArray();
243
+
244
+        $vendors = Vendor::query()
245
+            ->orderBy('name')
246
+            ->pluck('name', 'id')
247
+            ->mapWithKeys(fn ($name, $id) => [-$id => $name])
248
+            ->toArray();
249
+
250
+        return [
251
+            'Clients' => $clients,
252
+            'Vendors' => $vendors,
253
+        ];
254
+    }
255
+
256
+    public function getReportTableUrl(): string
257
+    {
258
+        if ($this->transactionable_type && ! $this->is_payment) {
259
+            return match ($this->transactionable_type) {
260
+                Bill::class => ViewBill::getUrl(['record' => $this->transactionable_id]),
261
+                default => ViewInvoice::getUrl(['record' => $this->transactionable_id]),
262
+            };
263
+        }
264
+
265
+        return ViewTransaction::getUrl(['record' => $this->id]);
266
+    }
267
+
99 268
     protected static function newFactory(): Factory
100 269
     {
101 270
         return TransactionFactory::new();

+ 6
- 0
app/Models/Common/Client.php Wyświetl plik

@@ -8,6 +8,7 @@ use App\Enums\Common\AddressType;
8 8
 use App\Models\Accounting\Estimate;
9 9
 use App\Models\Accounting\Invoice;
10 10
 use App\Models\Accounting\RecurringInvoice;
11
+use App\Models\Accounting\Transaction;
11 12
 use App\Models\Setting\Currency;
12 13
 use Illuminate\Database\Eloquent\Factories\HasFactory;
13 14
 use Illuminate\Database\Eloquent\Model;
@@ -215,6 +216,11 @@ class Client extends Model
215 216
         return $this;
216 217
     }
217 218
 
219
+    public function transactions(): MorphMany
220
+    {
221
+        return $this->morphMany(Transaction::class, 'payeeable');
222
+    }
223
+
218 224
     public function contacts(): MorphMany
219 225
     {
220 226
         return $this->morphMany(Contact::class, 'contactable');

+ 7
- 0
app/Models/Common/Vendor.php Wyświetl plik

@@ -7,11 +7,13 @@ use App\Concerns\CompanyOwned;
7 7
 use App\Enums\Common\ContractorType;
8 8
 use App\Enums\Common\VendorType;
9 9
 use App\Models\Accounting\Bill;
10
+use App\Models\Accounting\Transaction;
10 11
 use App\Models\Setting\Currency;
11 12
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12 13
 use Illuminate\Database\Eloquent\Model;
13 14
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
14 15
 use Illuminate\Database\Eloquent\Relations\HasMany;
16
+use Illuminate\Database\Eloquent\Relations\MorphMany;
15 17
 use Illuminate\Database\Eloquent\Relations\MorphOne;
16 18
 
17 19
 class Vendor extends Model
@@ -112,6 +114,11 @@ class Vendor extends Model
112 114
         return $this->hasMany(Bill::class);
113 115
     }
114 116
 
117
+    public function transactions(): MorphMany
118
+    {
119
+        return $this->morphMany(Transaction::class, 'payeeable');
120
+    }
121
+
115 122
     public function currency(): BelongsTo
116 123
     {
117 124
         return $this->belongsTo(Currency::class, 'currency_code', 'code');

+ 14
- 0
app/Observers/TransactionObserver.php Wyświetl plik

@@ -7,6 +7,8 @@ use App\Enums\Accounting\InvoiceStatus;
7 7
 use App\Models\Accounting\Bill;
8 8
 use App\Models\Accounting\Invoice;
9 9
 use App\Models\Accounting\Transaction;
10
+use App\Models\Common\Client;
11
+use App\Models\Common\Vendor;
10 12
 use App\Services\TransactionService;
11 13
 use App\Utilities\Currency\CurrencyConverter;
12 14
 use Illuminate\Database\Eloquent\Builder;
@@ -26,6 +28,18 @@ class TransactionObserver
26 28
         if ($transaction->type->isTransfer() && $transaction->description === null) {
27 29
             $transaction->description = 'Account Transfer';
28 30
         }
31
+
32
+        if ($transaction->transactionable && ! $transaction->payeeable_id) {
33
+            $document = $transaction->transactionable;
34
+
35
+            if ($document instanceof Invoice) {
36
+                $transaction->payeeable_id = $document->client_id;
37
+                $transaction->payeeable_type = Client::class;
38
+            } elseif ($document instanceof Bill) {
39
+                $transaction->payeeable_id = $document->vendor_id;
40
+                $transaction->payeeable_type = Vendor::class;
41
+            }
42
+        }
29 43
     }
30 44
 
31 45
     /**

+ 69
- 0
app/Policies/TransactionPolicy.php Wyświetl plik

@@ -0,0 +1,69 @@
1
+<?php
2
+
3
+namespace App\Policies;
4
+
5
+use App\Models\Accounting\Transaction;
6
+use App\Models\User;
7
+
8
+class TransactionPolicy
9
+{
10
+    /**
11
+     * Determine whether the user can view any models.
12
+     */
13
+    public function viewAny(User $user): bool
14
+    {
15
+        return true;
16
+    }
17
+
18
+    /**
19
+     * Determine whether the user can view the model.
20
+     */
21
+    public function view(User $user, Transaction $transaction): bool
22
+    {
23
+        return true;
24
+    }
25
+
26
+    /**
27
+     * Determine whether the user can create models.
28
+     */
29
+    public function create(User $user): bool
30
+    {
31
+        return true;
32
+    }
33
+
34
+    /**
35
+     * Determine whether the user can update the model.
36
+     */
37
+    public function update(User $user, Transaction $transaction): bool
38
+    {
39
+        if ($transaction->transactionable_id) {
40
+            return false;
41
+        }
42
+
43
+        return true;
44
+    }
45
+
46
+    /**
47
+     * Determine whether the user can delete the model.
48
+     */
49
+    public function delete(User $user, Transaction $transaction): bool
50
+    {
51
+        return true;
52
+    }
53
+
54
+    /**
55
+     * Determine whether the user can restore the model.
56
+     */
57
+    public function restore(User $user, Transaction $transaction): bool
58
+    {
59
+        return true;
60
+    }
61
+
62
+    /**
63
+     * Determine whether the user can permanently delete the model.
64
+     */
65
+    public function forceDelete(User $user, Transaction $transaction): bool
66
+    {
67
+        return true;
68
+    }
69
+}

+ 5
- 6
app/Providers/Filament/CompanyPanelProvider.php Wyświetl plik

@@ -19,13 +19,13 @@ use App\Actions\FilamentCompanies\UpdateUserPassword;
19 19
 use App\Actions\FilamentCompanies\UpdateUserProfileInformation;
20 20
 use App\Filament\Company\Clusters\Settings;
21 21
 use App\Filament\Company\Pages\Accounting\AccountChart;
22
-use App\Filament\Company\Pages\Accounting\Transactions;
23 22
 use App\Filament\Company\Pages\CreateCompany;
24 23
 use App\Filament\Company\Pages\ManageCompany;
25 24
 use App\Filament\Company\Pages\Reports;
26 25
 use App\Filament\Company\Pages\Service\ConnectedAccount;
27 26
 use App\Filament\Company\Pages\Service\LiveCurrency;
28 27
 use App\Filament\Company\Resources\Accounting\BudgetResource;
28
+use App\Filament\Company\Resources\Accounting\TransactionResource;
29 29
 use App\Filament\Company\Resources\Banking\AccountResource;
30 30
 use App\Filament\Company\Resources\Common\OfferingResource;
31 31
 use App\Filament\Company\Resources\Purchases\BillResource;
@@ -92,7 +92,7 @@ class CompanyPanelProvider extends PanelProvider
92 92
                     ->passwordReset();
93 93
             })
94 94
             ->tenantMenu(false)
95
-            ->plugin(
95
+            ->plugins([
96 96
                 FilamentCompanies::make()
97 97
                     ->userPanel('user')
98 98
                     ->switchCurrentCompany()
@@ -114,8 +114,6 @@ class CompanyPanelProvider extends PanelProvider
114 114
                         providers: [Provider::Github],
115 115
                         features: [Feature::RememberSession, Feature::ProviderAvatars],
116 116
                     ),
117
-            )
118
-            ->plugin(
119 117
                 PanelShiftDropdown::make()
120 118
                     ->logoutItem()
121 119
                     ->companySettings()
@@ -123,7 +121,7 @@ class CompanyPanelProvider extends PanelProvider
123 121
                         return $builder
124 122
                             ->items(Account::getNavigationItems());
125 123
                     }),
126
-            )
124
+            ])
127 125
             ->colors([
128 126
                 'primary' => Color::Indigo,
129 127
             ])
@@ -158,7 +156,7 @@ class CompanyPanelProvider extends PanelProvider
158 156
                             ->items([
159 157
                                 // ...BudgetResource::getNavigationItems(),
160 158
                                 ...AccountChart::getNavigationItems(),
161
-                                ...Transactions::getNavigationItems(),
159
+                                ...TransactionResource::getNavigationItems(),
162 160
                             ]),
163 161
                         NavigationGroup::make('Banking')
164 162
                             ->localizeLabel()
@@ -173,6 +171,7 @@ class CompanyPanelProvider extends PanelProvider
173 171
                             ]),
174 172
                     ]);
175 173
             })
174
+            ->globalSearch(false)
176 175
             ->sidebarCollapsibleOnDesktop()
177 176
             ->viteTheme('resources/css/filament/company/theme.css')
178 177
             ->brandLogo(static fn () => view('components.icons.logo'))

+ 14
- 0
app/Providers/MacroServiceProvider.php Wyświetl plik

@@ -19,6 +19,7 @@ use Filament\Forms\Components\DatePicker;
19 19
 use Filament\Forms\Components\Field;
20 20
 use Filament\Forms\Components\TextInput;
21 21
 use Filament\Infolists\Components\TextEntry;
22
+use Filament\Support\Enums\IconPosition;
22 23
 use Filament\Tables\Columns\TextColumn;
23 24
 use Filament\Tables\Contracts\HasTable;
24 25
 use Illuminate\Contracts\Support\Htmlable;
@@ -375,6 +376,19 @@ class MacroServiceProvider extends ServiceProvider
375 376
             return $this;
376 377
         });
377 378
 
379
+        TextEntry::macro('link', function (bool $condition = true): static {
380
+            if ($condition) {
381
+                $this
382
+                    ->limit(50)
383
+                    ->openUrlInNewTab()
384
+                    ->icon('heroicon-o-arrow-top-right-on-square')
385
+                    ->iconColor('primary')
386
+                    ->iconPosition(IconPosition::After);
387
+            }
388
+
389
+            return $this;
390
+        });
391
+
378 392
         Money::macro('swapAmountFor', function ($newCurrency) {
379 393
             $oldCurrency = $this->currency->getCurrency();
380 394
             $balanceInSubunits = $this->getAmount();

+ 7
- 7
app/Services/AccountService.php Wyświetl plik

@@ -7,6 +7,8 @@ use App\Models\Accounting\Account;
7 7
 use App\Models\Accounting\Bill;
8 8
 use App\Models\Accounting\Invoice;
9 9
 use App\Models\Accounting\Transaction;
10
+use App\Models\Common\Client;
11
+use App\Models\Common\Vendor;
10 12
 use App\Repositories\Accounting\JournalEntryRepository;
11 13
 use App\Utilities\Currency\CurrencyAccessor;
12 14
 use App\ValueObjects\Money;
@@ -128,21 +130,19 @@ class AccountService
128 130
                 ->whereBetween('transactions.posted_at', [$startDate, $endDate])
129 131
                 ->join('transactions', 'transactions.id', '=', 'journal_entries.transaction_id')
130 132
                 ->orderBy('transactions.posted_at')
131
-                ->with('transaction:id,type,description,posted_at,is_payment,transactionable_id,transactionable_type');
133
+                ->with('transaction:id,type,description,posted_at,is_payment,payeeable_id,payeeable_type');
132 134
 
133 135
             if ($entityId) {
134 136
                 $entityId = (int) $entityId;
135 137
                 if ($entityId < 0) {
136 138
                     $query->whereHas('transaction', function ($query) use ($entityId) {
137
-                        $query->whereHasMorph('transactionable', [Bill::class], function ($query) use ($entityId) {
138
-                            $query->where('vendor_id', abs($entityId));
139
-                        });
139
+                        $query->where('payeeable_type', Vendor::class)
140
+                            ->where('payeeable_id', abs($entityId));
140 141
                     });
141 142
                 } else {
142 143
                     $query->whereHas('transaction', function ($query) use ($entityId) {
143
-                        $query->whereHasMorph('transactionable', [Invoice::class], function ($query) use ($entityId) {
144
-                            $query->where('client_id', $entityId);
145
-                        });
144
+                        $query->where('payeeable_type', Client::class)
145
+                            ->where('payeeable_id', $entityId);
146 146
                     });
147 147
                 }
148 148
             }

+ 1
- 27
app/Services/ReportService.php Wyświetl plik

@@ -20,11 +20,9 @@ use App\Enums\Accounting\AccountType;
20 20
 use App\Enums\Accounting\BillStatus;
21 21
 use App\Enums\Accounting\DocumentEntityType;
22 22
 use App\Enums\Accounting\InvoiceStatus;
23
-use App\Enums\Accounting\TransactionType;
24 23
 use App\Models\Accounting\Account;
25 24
 use App\Models\Accounting\Bill;
26 25
 use App\Models\Accounting\Invoice;
27
-use App\Models\Accounting\Transaction;
28 26
 use App\Support\Column;
29 27
 use App\Utilities\Currency\CurrencyAccessor;
30 28
 use App\Utilities\Currency\CurrencyConverter;
@@ -206,7 +204,6 @@ class ReportService
206 204
                 credit: '',
207 205
                 balance: money($currentBalance, $defaultCurrency)->format(),
208 206
                 type: null,
209
-                tableAction: null
210 207
             );
211 208
 
212 209
             foreach ($account->journalEntries as $journalEntry) {
@@ -236,7 +233,7 @@ class ReportService
236 233
                     credit: $journalEntry->type->isCredit() ? $formattedAmount : '',
237 234
                     balance: money($currentBalance, $defaultCurrency)->format(),
238 235
                     type: $transaction->type,
239
-                    tableAction: $this->determineTableAction($transaction),
236
+                    url: $transaction->getReportTableUrl(),
240 237
                 );
241 238
             }
242 239
 
@@ -250,7 +247,6 @@ class ReportService
250 247
                 credit: money($periodCreditTotal, $defaultCurrency)->format(),
251 248
                 balance: money($currentBalance, $defaultCurrency)->format(),
252 249
                 type: null,
253
-                tableAction: null
254 250
             );
255 251
 
256 252
             $accountTransactions[] = new AccountTransactionDTO(
@@ -261,7 +257,6 @@ class ReportService
261 257
                 credit: '',
262 258
                 balance: money($balanceChange, $defaultCurrency)->format(),
263 259
                 type: null,
264
-                tableAction: null
265 260
             );
266 261
 
267 262
             $reportCategories[] = [
@@ -274,27 +269,6 @@ class ReportService
274 269
         return new ReportDTO(categories: $reportCategories, fields: $columns);
275 270
     }
276 271
 
277
-    private function determineTableAction(Transaction $transaction): array
278
-    {
279
-        if ($transaction->transactionable_type === null || $transaction->is_payment) {
280
-            return [
281
-                'type' => 'transaction',
282
-                'action' => match ($transaction->type) {
283
-                    TransactionType::Journal => 'editJournalTransaction',
284
-                    TransactionType::Transfer => 'editTransfer',
285
-                    default => 'editTransaction',
286
-                },
287
-                'id' => $transaction->id,
288
-            ];
289
-        }
290
-
291
-        return [
292
-            'type' => 'transactionable',
293
-            'model' => $transaction->transactionable_type,
294
-            'id' => $transaction->transactionable_id,
295
-        ];
296
-    }
297
-
298 272
     public function buildTrialBalanceReport(string $trialBalanceType, string $asOfDate, array $columns = []): ReportDTO
299 273
     {
300 274
         $asOfDateCarbon = Carbon::parse($asOfDate);

+ 1
- 1
app/Transformers/AccountTransactionReportTransformer.php Wyświetl plik

@@ -44,7 +44,7 @@ class AccountTransactionReportTransformer extends BaseReportTransformer
44 44
                         'description' => [
45 45
                             'id' => $transaction->id,
46 46
                             'description' => $transaction->description,
47
-                            'tableAction' => $transaction->tableAction,
47
+                            'url' => $transaction->url,
48 48
                         ],
49 49
                         'debit' => $transaction->debit,
50 50
                         'credit' => $transaction->credit,

+ 18
- 18
composer.lock Wyświetl plik

@@ -368,16 +368,16 @@
368 368
         },
369 369
         {
370 370
             "name": "awcodes/filament-table-repeater",
371
-            "version": "v3.1.3",
371
+            "version": "v3.1.4",
372 372
             "source": {
373 373
                 "type": "git",
374 374
                 "url": "https://github.com/awcodes/filament-table-repeater.git",
375
-                "reference": "fd8df8fbb94a41d0a031a75ef739538290a14a8c"
375
+                "reference": "275de32e2123a2f7e586404352ee4c794f019a09"
376 376
             },
377 377
             "dist": {
378 378
                 "type": "zip",
379
-                "url": "https://api.github.com/repos/awcodes/filament-table-repeater/zipball/fd8df8fbb94a41d0a031a75ef739538290a14a8c",
380
-                "reference": "fd8df8fbb94a41d0a031a75ef739538290a14a8c",
379
+                "url": "https://api.github.com/repos/awcodes/filament-table-repeater/zipball/275de32e2123a2f7e586404352ee4c794f019a09",
380
+                "reference": "275de32e2123a2f7e586404352ee4c794f019a09",
381 381
                 "shasum": ""
382 382
             },
383 383
             "require": {
@@ -431,7 +431,7 @@
431 431
             ],
432 432
             "support": {
433 433
                 "issues": "https://github.com/awcodes/filament-table-repeater/issues",
434
-                "source": "https://github.com/awcodes/filament-table-repeater/tree/v3.1.3"
434
+                "source": "https://github.com/awcodes/filament-table-repeater/tree/v3.1.4"
435 435
             },
436 436
             "funding": [
437 437
                 {
@@ -439,7 +439,7 @@
439 439
                     "type": "github"
440 440
                 }
441 441
             ],
442
-            "time": "2025-05-03T14:59:55+00:00"
442
+            "time": "2025-05-15T15:46:52+00:00"
443 443
         },
444 444
         {
445 445
             "name": "aws/aws-crt-php",
@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.343.9",
500
+            "version": "3.343.13",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "6ca5eb1c60b879cf516e5fadefec87afc6219e74"
504
+                "reference": "eb50d111a09ef39675358e74801260ac129ee346"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6ca5eb1c60b879cf516e5fadefec87afc6219e74",
509
-                "reference": "6ca5eb1c60b879cf516e5fadefec87afc6219e74",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eb50d111a09ef39675358e74801260ac129ee346",
509
+                "reference": "eb50d111a09ef39675358e74801260ac129ee346",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -588,9 +588,9 @@
588 588
             "support": {
589 589
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
590 590
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
591
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.343.9"
591
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.343.13"
592 592
             },
593
-            "time": "2025-05-12T18:11:31+00:00"
593
+            "time": "2025-05-16T18:24:39+00:00"
594 594
         },
595 595
         {
596 596
             "name": "aws/aws-sdk-php-laravel",
@@ -1029,7 +1029,7 @@
1029 1029
         },
1030 1030
         {
1031 1031
             "name": "codewithdennis/filament-simple-alert",
1032
-            "version": "v3.0.17",
1032
+            "version": "v3.0.18",
1033 1033
             "source": {
1034 1034
                 "type": "git",
1035 1035
                 "url": "https://github.com/CodeWithDennis/filament-simple-alert.git",
@@ -9861,16 +9861,16 @@
9861 9861
         },
9862 9862
         {
9863 9863
             "name": "laravel/sail",
9864
-            "version": "v1.42.0",
9864
+            "version": "v1.43.0",
9865 9865
             "source": {
9866 9866
                 "type": "git",
9867 9867
                 "url": "https://github.com/laravel/sail.git",
9868
-                "reference": "2edaaf77f3c07a4099965bb3d7dfee16e801c0f6"
9868
+                "reference": "71a509b14b2621ce58574274a74290f933c687f7"
9869 9869
             },
9870 9870
             "dist": {
9871 9871
                 "type": "zip",
9872
-                "url": "https://api.github.com/repos/laravel/sail/zipball/2edaaf77f3c07a4099965bb3d7dfee16e801c0f6",
9873
-                "reference": "2edaaf77f3c07a4099965bb3d7dfee16e801c0f6",
9872
+                "url": "https://api.github.com/repos/laravel/sail/zipball/71a509b14b2621ce58574274a74290f933c687f7",
9873
+                "reference": "71a509b14b2621ce58574274a74290f933c687f7",
9874 9874
                 "shasum": ""
9875 9875
             },
9876 9876
             "require": {
@@ -9920,7 +9920,7 @@
9920 9920
                 "issues": "https://github.com/laravel/sail/issues",
9921 9921
                 "source": "https://github.com/laravel/sail"
9922 9922
             },
9923
-            "time": "2025-04-29T14:26:46+00:00"
9923
+            "time": "2025-05-13T13:34:34+00:00"
9924 9924
         },
9925 9925
         {
9926 9926
             "name": "mockery/mockery",

+ 30
- 0
database/migrations/2025_05_17_194711_add_payeeable_to_transactions_table.php Wyświetl plik

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::table('transactions', function (Blueprint $table) {
15
+            $table->after('transactionable_id', function (Blueprint $table) {
16
+                $table->nullableMorphs('payeeable');
17
+            });
18
+        });
19
+    }
20
+
21
+    /**
22
+     * Reverse the migrations.
23
+     */
24
+    public function down(): void
25
+    {
26
+        Schema::table('transactions', function (Blueprint $table) {
27
+            $table->dropMorphs('payeeable');
28
+        });
29
+    }
30
+};

+ 303
- 212
package-lock.json Wyświetl plik

@@ -30,9 +30,9 @@
30 30
             }
31 31
         },
32 32
         "node_modules/@esbuild/aix-ppc64": {
33
-            "version": "0.25.1",
34
-            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
35
-            "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
33
+            "version": "0.25.4",
34
+            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
35
+            "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
36 36
             "cpu": [
37 37
                 "ppc64"
38 38
             ],
@@ -47,9 +47,9 @@
47 47
             }
48 48
         },
49 49
         "node_modules/@esbuild/android-arm": {
50
-            "version": "0.25.1",
51
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
52
-            "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
50
+            "version": "0.25.4",
51
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
52
+            "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
53 53
             "cpu": [
54 54
                 "arm"
55 55
             ],
@@ -64,9 +64,9 @@
64 64
             }
65 65
         },
66 66
         "node_modules/@esbuild/android-arm64": {
67
-            "version": "0.25.1",
68
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
69
-            "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
67
+            "version": "0.25.4",
68
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
69
+            "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
70 70
             "cpu": [
71 71
                 "arm64"
72 72
             ],
@@ -81,9 +81,9 @@
81 81
             }
82 82
         },
83 83
         "node_modules/@esbuild/android-x64": {
84
-            "version": "0.25.1",
85
-            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
86
-            "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
84
+            "version": "0.25.4",
85
+            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
86
+            "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
87 87
             "cpu": [
88 88
                 "x64"
89 89
             ],
@@ -98,9 +98,9 @@
98 98
             }
99 99
         },
100 100
         "node_modules/@esbuild/darwin-arm64": {
101
-            "version": "0.25.1",
102
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
103
-            "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
101
+            "version": "0.25.4",
102
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
103
+            "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
104 104
             "cpu": [
105 105
                 "arm64"
106 106
             ],
@@ -115,9 +115,9 @@
115 115
             }
116 116
         },
117 117
         "node_modules/@esbuild/darwin-x64": {
118
-            "version": "0.25.1",
119
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
120
-            "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
118
+            "version": "0.25.4",
119
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
120
+            "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
121 121
             "cpu": [
122 122
                 "x64"
123 123
             ],
@@ -132,9 +132,9 @@
132 132
             }
133 133
         },
134 134
         "node_modules/@esbuild/freebsd-arm64": {
135
-            "version": "0.25.1",
136
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
137
-            "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
135
+            "version": "0.25.4",
136
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
137
+            "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
138 138
             "cpu": [
139 139
                 "arm64"
140 140
             ],
@@ -149,9 +149,9 @@
149 149
             }
150 150
         },
151 151
         "node_modules/@esbuild/freebsd-x64": {
152
-            "version": "0.25.1",
153
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
154
-            "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
152
+            "version": "0.25.4",
153
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
154
+            "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
155 155
             "cpu": [
156 156
                 "x64"
157 157
             ],
@@ -166,9 +166,9 @@
166 166
             }
167 167
         },
168 168
         "node_modules/@esbuild/linux-arm": {
169
-            "version": "0.25.1",
170
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
171
-            "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
169
+            "version": "0.25.4",
170
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
171
+            "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
172 172
             "cpu": [
173 173
                 "arm"
174 174
             ],
@@ -183,9 +183,9 @@
183 183
             }
184 184
         },
185 185
         "node_modules/@esbuild/linux-arm64": {
186
-            "version": "0.25.1",
187
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
188
-            "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
186
+            "version": "0.25.4",
187
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
188
+            "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
189 189
             "cpu": [
190 190
                 "arm64"
191 191
             ],
@@ -200,9 +200,9 @@
200 200
             }
201 201
         },
202 202
         "node_modules/@esbuild/linux-ia32": {
203
-            "version": "0.25.1",
204
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
205
-            "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
203
+            "version": "0.25.4",
204
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
205
+            "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
206 206
             "cpu": [
207 207
                 "ia32"
208 208
             ],
@@ -217,9 +217,9 @@
217 217
             }
218 218
         },
219 219
         "node_modules/@esbuild/linux-loong64": {
220
-            "version": "0.25.1",
221
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
222
-            "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
220
+            "version": "0.25.4",
221
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
222
+            "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
223 223
             "cpu": [
224 224
                 "loong64"
225 225
             ],
@@ -234,9 +234,9 @@
234 234
             }
235 235
         },
236 236
         "node_modules/@esbuild/linux-mips64el": {
237
-            "version": "0.25.1",
238
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
239
-            "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
237
+            "version": "0.25.4",
238
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
239
+            "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
240 240
             "cpu": [
241 241
                 "mips64el"
242 242
             ],
@@ -251,9 +251,9 @@
251 251
             }
252 252
         },
253 253
         "node_modules/@esbuild/linux-ppc64": {
254
-            "version": "0.25.1",
255
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
256
-            "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
254
+            "version": "0.25.4",
255
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
256
+            "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
257 257
             "cpu": [
258 258
                 "ppc64"
259 259
             ],
@@ -268,9 +268,9 @@
268 268
             }
269 269
         },
270 270
         "node_modules/@esbuild/linux-riscv64": {
271
-            "version": "0.25.1",
272
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
273
-            "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
271
+            "version": "0.25.4",
272
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
273
+            "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
274 274
             "cpu": [
275 275
                 "riscv64"
276 276
             ],
@@ -285,9 +285,9 @@
285 285
             }
286 286
         },
287 287
         "node_modules/@esbuild/linux-s390x": {
288
-            "version": "0.25.1",
289
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
290
-            "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
288
+            "version": "0.25.4",
289
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
290
+            "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
291 291
             "cpu": [
292 292
                 "s390x"
293 293
             ],
@@ -302,9 +302,9 @@
302 302
             }
303 303
         },
304 304
         "node_modules/@esbuild/linux-x64": {
305
-            "version": "0.25.1",
306
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
307
-            "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
305
+            "version": "0.25.4",
306
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
307
+            "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
308 308
             "cpu": [
309 309
                 "x64"
310 310
             ],
@@ -319,9 +319,9 @@
319 319
             }
320 320
         },
321 321
         "node_modules/@esbuild/netbsd-arm64": {
322
-            "version": "0.25.1",
323
-            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
324
-            "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
322
+            "version": "0.25.4",
323
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
324
+            "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
325 325
             "cpu": [
326 326
                 "arm64"
327 327
             ],
@@ -336,9 +336,9 @@
336 336
             }
337 337
         },
338 338
         "node_modules/@esbuild/netbsd-x64": {
339
-            "version": "0.25.1",
340
-            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
341
-            "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
339
+            "version": "0.25.4",
340
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
341
+            "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
342 342
             "cpu": [
343 343
                 "x64"
344 344
             ],
@@ -353,9 +353,9 @@
353 353
             }
354 354
         },
355 355
         "node_modules/@esbuild/openbsd-arm64": {
356
-            "version": "0.25.1",
357
-            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
358
-            "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
356
+            "version": "0.25.4",
357
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
358
+            "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
359 359
             "cpu": [
360 360
                 "arm64"
361 361
             ],
@@ -370,9 +370,9 @@
370 370
             }
371 371
         },
372 372
         "node_modules/@esbuild/openbsd-x64": {
373
-            "version": "0.25.1",
374
-            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
375
-            "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
373
+            "version": "0.25.4",
374
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
375
+            "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
376 376
             "cpu": [
377 377
                 "x64"
378 378
             ],
@@ -387,9 +387,9 @@
387 387
             }
388 388
         },
389 389
         "node_modules/@esbuild/sunos-x64": {
390
-            "version": "0.25.1",
391
-            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
392
-            "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
390
+            "version": "0.25.4",
391
+            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
392
+            "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
393 393
             "cpu": [
394 394
                 "x64"
395 395
             ],
@@ -404,9 +404,9 @@
404 404
             }
405 405
         },
406 406
         "node_modules/@esbuild/win32-arm64": {
407
-            "version": "0.25.1",
408
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
409
-            "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
407
+            "version": "0.25.4",
408
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
409
+            "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
410 410
             "cpu": [
411 411
                 "arm64"
412 412
             ],
@@ -421,9 +421,9 @@
421 421
             }
422 422
         },
423 423
         "node_modules/@esbuild/win32-ia32": {
424
-            "version": "0.25.1",
425
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
426
-            "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
424
+            "version": "0.25.4",
425
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
426
+            "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
427 427
             "cpu": [
428 428
                 "ia32"
429 429
             ],
@@ -438,9 +438,9 @@
438 438
             }
439 439
         },
440 440
         "node_modules/@esbuild/win32-x64": {
441
-            "version": "0.25.1",
442
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
443
-            "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
441
+            "version": "0.25.4",
442
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
443
+            "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
444 444
             "cpu": [
445 445
                 "x64"
446 446
             ],
@@ -575,9 +575,9 @@
575 575
             }
576 576
         },
577 577
         "node_modules/@rollup/rollup-android-arm-eabi": {
578
-            "version": "4.36.0",
579
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
580
-            "integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==",
578
+            "version": "4.40.2",
579
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
580
+            "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
581 581
             "cpu": [
582 582
                 "arm"
583 583
             ],
@@ -589,9 +589,9 @@
589 589
             ]
590 590
         },
591 591
         "node_modules/@rollup/rollup-android-arm64": {
592
-            "version": "4.36.0",
593
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.36.0.tgz",
594
-            "integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==",
592
+            "version": "4.40.2",
593
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
594
+            "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
595 595
             "cpu": [
596 596
                 "arm64"
597 597
             ],
@@ -603,9 +603,9 @@
603 603
             ]
604 604
         },
605 605
         "node_modules/@rollup/rollup-darwin-arm64": {
606
-            "version": "4.36.0",
607
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.36.0.tgz",
608
-            "integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==",
606
+            "version": "4.40.2",
607
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
608
+            "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
609 609
             "cpu": [
610 610
                 "arm64"
611 611
             ],
@@ -617,9 +617,9 @@
617 617
             ]
618 618
         },
619 619
         "node_modules/@rollup/rollup-darwin-x64": {
620
-            "version": "4.36.0",
621
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.36.0.tgz",
622
-            "integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==",
620
+            "version": "4.40.2",
621
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
622
+            "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
623 623
             "cpu": [
624 624
                 "x64"
625 625
             ],
@@ -631,9 +631,9 @@
631 631
             ]
632 632
         },
633 633
         "node_modules/@rollup/rollup-freebsd-arm64": {
634
-            "version": "4.36.0",
635
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.36.0.tgz",
636
-            "integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==",
634
+            "version": "4.40.2",
635
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
636
+            "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
637 637
             "cpu": [
638 638
                 "arm64"
639 639
             ],
@@ -645,9 +645,9 @@
645 645
             ]
646 646
         },
647 647
         "node_modules/@rollup/rollup-freebsd-x64": {
648
-            "version": "4.36.0",
649
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.36.0.tgz",
650
-            "integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==",
648
+            "version": "4.40.2",
649
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
650
+            "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
651 651
             "cpu": [
652 652
                 "x64"
653 653
             ],
@@ -659,9 +659,9 @@
659 659
             ]
660 660
         },
661 661
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
662
-            "version": "4.36.0",
663
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.36.0.tgz",
664
-            "integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==",
662
+            "version": "4.40.2",
663
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
664
+            "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
665 665
             "cpu": [
666 666
                 "arm"
667 667
             ],
@@ -673,9 +673,9 @@
673 673
             ]
674 674
         },
675 675
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
676
-            "version": "4.36.0",
677
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.36.0.tgz",
678
-            "integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==",
676
+            "version": "4.40.2",
677
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
678
+            "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
679 679
             "cpu": [
680 680
                 "arm"
681 681
             ],
@@ -687,9 +687,9 @@
687 687
             ]
688 688
         },
689 689
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
690
-            "version": "4.36.0",
691
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.36.0.tgz",
692
-            "integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==",
690
+            "version": "4.40.2",
691
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
692
+            "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
693 693
             "cpu": [
694 694
                 "arm64"
695 695
             ],
@@ -701,9 +701,9 @@
701 701
             ]
702 702
         },
703 703
         "node_modules/@rollup/rollup-linux-arm64-musl": {
704
-            "version": "4.36.0",
705
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.36.0.tgz",
706
-            "integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==",
704
+            "version": "4.40.2",
705
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
706
+            "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
707 707
             "cpu": [
708 708
                 "arm64"
709 709
             ],
@@ -715,9 +715,9 @@
715 715
             ]
716 716
         },
717 717
         "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
718
-            "version": "4.36.0",
719
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.36.0.tgz",
720
-            "integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==",
718
+            "version": "4.40.2",
719
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
720
+            "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
721 721
             "cpu": [
722 722
                 "loong64"
723 723
             ],
@@ -729,9 +729,9 @@
729 729
             ]
730 730
         },
731 731
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
732
-            "version": "4.36.0",
733
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.36.0.tgz",
734
-            "integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==",
732
+            "version": "4.40.2",
733
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
734
+            "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
735 735
             "cpu": [
736 736
                 "ppc64"
737 737
             ],
@@ -743,9 +743,23 @@
743 743
             ]
744 744
         },
745 745
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
746
-            "version": "4.36.0",
747
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.36.0.tgz",
748
-            "integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==",
746
+            "version": "4.40.2",
747
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
748
+            "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
749
+            "cpu": [
750
+                "riscv64"
751
+            ],
752
+            "dev": true,
753
+            "license": "MIT",
754
+            "optional": true,
755
+            "os": [
756
+                "linux"
757
+            ]
758
+        },
759
+        "node_modules/@rollup/rollup-linux-riscv64-musl": {
760
+            "version": "4.40.2",
761
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
762
+            "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
749 763
             "cpu": [
750 764
                 "riscv64"
751 765
             ],
@@ -757,9 +771,9 @@
757 771
             ]
758 772
         },
759 773
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
760
-            "version": "4.36.0",
761
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.36.0.tgz",
762
-            "integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==",
774
+            "version": "4.40.2",
775
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
776
+            "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
763 777
             "cpu": [
764 778
                 "s390x"
765 779
             ],
@@ -771,9 +785,9 @@
771 785
             ]
772 786
         },
773 787
         "node_modules/@rollup/rollup-linux-x64-gnu": {
774
-            "version": "4.36.0",
775
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.36.0.tgz",
776
-            "integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==",
788
+            "version": "4.40.2",
789
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
790
+            "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
777 791
             "cpu": [
778 792
                 "x64"
779 793
             ],
@@ -785,9 +799,9 @@
785 799
             ]
786 800
         },
787 801
         "node_modules/@rollup/rollup-linux-x64-musl": {
788
-            "version": "4.36.0",
789
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.36.0.tgz",
790
-            "integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==",
802
+            "version": "4.40.2",
803
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
804
+            "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
791 805
             "cpu": [
792 806
                 "x64"
793 807
             ],
@@ -799,9 +813,9 @@
799 813
             ]
800 814
         },
801 815
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
802
-            "version": "4.36.0",
803
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.36.0.tgz",
804
-            "integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==",
816
+            "version": "4.40.2",
817
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
818
+            "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
805 819
             "cpu": [
806 820
                 "arm64"
807 821
             ],
@@ -813,9 +827,9 @@
813 827
             ]
814 828
         },
815 829
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
816
-            "version": "4.36.0",
817
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.36.0.tgz",
818
-            "integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==",
830
+            "version": "4.40.2",
831
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
832
+            "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
819 833
             "cpu": [
820 834
                 "ia32"
821 835
             ],
@@ -827,9 +841,9 @@
827 841
             ]
828 842
         },
829 843
         "node_modules/@rollup/rollup-win32-x64-msvc": {
830
-            "version": "4.36.0",
831
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.36.0.tgz",
832
-            "integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==",
844
+            "version": "4.40.2",
845
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
846
+            "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
833 847
             "cpu": [
834 848
                 "x64"
835 849
             ],
@@ -870,9 +884,9 @@
870 884
             }
871 885
         },
872 886
         "node_modules/@types/estree": {
873
-            "version": "1.0.6",
874
-            "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
875
-            "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
887
+            "version": "1.0.7",
888
+            "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
889
+            "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
876 890
             "dev": true,
877 891
             "license": "MIT"
878 892
         },
@@ -976,9 +990,9 @@
976 990
             }
977 991
         },
978 992
         "node_modules/axios": {
979
-            "version": "1.8.3",
980
-            "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
981
-            "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
993
+            "version": "1.9.0",
994
+            "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
995
+            "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
982 996
             "dev": true,
983 997
             "license": "MIT",
984 998
             "dependencies": {
@@ -1031,9 +1045,9 @@
1031 1045
             }
1032 1046
         },
1033 1047
         "node_modules/browserslist": {
1034
-            "version": "4.24.4",
1035
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
1036
-            "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
1048
+            "version": "4.24.5",
1049
+            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
1050
+            "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
1037 1051
             "dev": true,
1038 1052
             "funding": [
1039 1053
                 {
@@ -1051,10 +1065,10 @@
1051 1065
             ],
1052 1066
             "license": "MIT",
1053 1067
             "dependencies": {
1054
-                "caniuse-lite": "^1.0.30001688",
1055
-                "electron-to-chromium": "^1.5.73",
1068
+                "caniuse-lite": "^1.0.30001716",
1069
+                "electron-to-chromium": "^1.5.149",
1056 1070
                 "node-releases": "^2.0.19",
1057
-                "update-browserslist-db": "^1.1.1"
1071
+                "update-browserslist-db": "^1.1.3"
1058 1072
             },
1059 1073
             "bin": {
1060 1074
                 "browserslist": "cli.js"
@@ -1088,9 +1102,9 @@
1088 1102
             }
1089 1103
         },
1090 1104
         "node_modules/caniuse-lite": {
1091
-            "version": "1.0.30001706",
1092
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz",
1093
-            "integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==",
1105
+            "version": "1.0.30001718",
1106
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
1107
+            "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
1094 1108
             "dev": true,
1095 1109
             "funding": [
1096 1110
                 {
@@ -1264,9 +1278,9 @@
1264 1278
             "license": "MIT"
1265 1279
         },
1266 1280
         "node_modules/electron-to-chromium": {
1267
-            "version": "1.5.120",
1268
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz",
1269
-            "integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==",
1281
+            "version": "1.5.155",
1282
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
1283
+            "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
1270 1284
             "dev": true,
1271 1285
             "license": "ISC"
1272 1286
         },
@@ -1327,9 +1341,9 @@
1327 1341
             }
1328 1342
         },
1329 1343
         "node_modules/esbuild": {
1330
-            "version": "0.25.1",
1331
-            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
1332
-            "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
1344
+            "version": "0.25.4",
1345
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
1346
+            "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
1333 1347
             "dev": true,
1334 1348
             "hasInstallScript": true,
1335 1349
             "license": "MIT",
@@ -1340,31 +1354,31 @@
1340 1354
                 "node": ">=18"
1341 1355
             },
1342 1356
             "optionalDependencies": {
1343
-                "@esbuild/aix-ppc64": "0.25.1",
1344
-                "@esbuild/android-arm": "0.25.1",
1345
-                "@esbuild/android-arm64": "0.25.1",
1346
-                "@esbuild/android-x64": "0.25.1",
1347
-                "@esbuild/darwin-arm64": "0.25.1",
1348
-                "@esbuild/darwin-x64": "0.25.1",
1349
-                "@esbuild/freebsd-arm64": "0.25.1",
1350
-                "@esbuild/freebsd-x64": "0.25.1",
1351
-                "@esbuild/linux-arm": "0.25.1",
1352
-                "@esbuild/linux-arm64": "0.25.1",
1353
-                "@esbuild/linux-ia32": "0.25.1",
1354
-                "@esbuild/linux-loong64": "0.25.1",
1355
-                "@esbuild/linux-mips64el": "0.25.1",
1356
-                "@esbuild/linux-ppc64": "0.25.1",
1357
-                "@esbuild/linux-riscv64": "0.25.1",
1358
-                "@esbuild/linux-s390x": "0.25.1",
1359
-                "@esbuild/linux-x64": "0.25.1",
1360
-                "@esbuild/netbsd-arm64": "0.25.1",
1361
-                "@esbuild/netbsd-x64": "0.25.1",
1362
-                "@esbuild/openbsd-arm64": "0.25.1",
1363
-                "@esbuild/openbsd-x64": "0.25.1",
1364
-                "@esbuild/sunos-x64": "0.25.1",
1365
-                "@esbuild/win32-arm64": "0.25.1",
1366
-                "@esbuild/win32-ia32": "0.25.1",
1367
-                "@esbuild/win32-x64": "0.25.1"
1357
+                "@esbuild/aix-ppc64": "0.25.4",
1358
+                "@esbuild/android-arm": "0.25.4",
1359
+                "@esbuild/android-arm64": "0.25.4",
1360
+                "@esbuild/android-x64": "0.25.4",
1361
+                "@esbuild/darwin-arm64": "0.25.4",
1362
+                "@esbuild/darwin-x64": "0.25.4",
1363
+                "@esbuild/freebsd-arm64": "0.25.4",
1364
+                "@esbuild/freebsd-x64": "0.25.4",
1365
+                "@esbuild/linux-arm": "0.25.4",
1366
+                "@esbuild/linux-arm64": "0.25.4",
1367
+                "@esbuild/linux-ia32": "0.25.4",
1368
+                "@esbuild/linux-loong64": "0.25.4",
1369
+                "@esbuild/linux-mips64el": "0.25.4",
1370
+                "@esbuild/linux-ppc64": "0.25.4",
1371
+                "@esbuild/linux-riscv64": "0.25.4",
1372
+                "@esbuild/linux-s390x": "0.25.4",
1373
+                "@esbuild/linux-x64": "0.25.4",
1374
+                "@esbuild/netbsd-arm64": "0.25.4",
1375
+                "@esbuild/netbsd-x64": "0.25.4",
1376
+                "@esbuild/openbsd-arm64": "0.25.4",
1377
+                "@esbuild/openbsd-x64": "0.25.4",
1378
+                "@esbuild/sunos-x64": "0.25.4",
1379
+                "@esbuild/win32-arm64": "0.25.4",
1380
+                "@esbuild/win32-ia32": "0.25.4",
1381
+                "@esbuild/win32-x64": "0.25.4"
1368 1382
             }
1369 1383
         },
1370 1384
         "node_modules/escalade": {
@@ -2067,9 +2081,9 @@
2067 2081
             }
2068 2082
         },
2069 2083
         "node_modules/pirates": {
2070
-            "version": "4.0.6",
2071
-            "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
2072
-            "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
2084
+            "version": "4.0.7",
2085
+            "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
2086
+            "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
2073 2087
             "dev": true,
2074 2088
             "license": "MIT",
2075 2089
             "engines": {
@@ -2412,13 +2426,13 @@
2412 2426
             }
2413 2427
         },
2414 2428
         "node_modules/rollup": {
2415
-            "version": "4.36.0",
2416
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz",
2417
-            "integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==",
2429
+            "version": "4.40.2",
2430
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
2431
+            "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
2418 2432
             "dev": true,
2419 2433
             "license": "MIT",
2420 2434
             "dependencies": {
2421
-                "@types/estree": "1.0.6"
2435
+                "@types/estree": "1.0.7"
2422 2436
             },
2423 2437
             "bin": {
2424 2438
                 "rollup": "dist/bin/rollup"
@@ -2428,25 +2442,26 @@
2428 2442
                 "npm": ">=8.0.0"
2429 2443
             },
2430 2444
             "optionalDependencies": {
2431
-                "@rollup/rollup-android-arm-eabi": "4.36.0",
2432
-                "@rollup/rollup-android-arm64": "4.36.0",
2433
-                "@rollup/rollup-darwin-arm64": "4.36.0",
2434
-                "@rollup/rollup-darwin-x64": "4.36.0",
2435
-                "@rollup/rollup-freebsd-arm64": "4.36.0",
2436
-                "@rollup/rollup-freebsd-x64": "4.36.0",
2437
-                "@rollup/rollup-linux-arm-gnueabihf": "4.36.0",
2438
-                "@rollup/rollup-linux-arm-musleabihf": "4.36.0",
2439
-                "@rollup/rollup-linux-arm64-gnu": "4.36.0",
2440
-                "@rollup/rollup-linux-arm64-musl": "4.36.0",
2441
-                "@rollup/rollup-linux-loongarch64-gnu": "4.36.0",
2442
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.36.0",
2443
-                "@rollup/rollup-linux-riscv64-gnu": "4.36.0",
2444
-                "@rollup/rollup-linux-s390x-gnu": "4.36.0",
2445
-                "@rollup/rollup-linux-x64-gnu": "4.36.0",
2446
-                "@rollup/rollup-linux-x64-musl": "4.36.0",
2447
-                "@rollup/rollup-win32-arm64-msvc": "4.36.0",
2448
-                "@rollup/rollup-win32-ia32-msvc": "4.36.0",
2449
-                "@rollup/rollup-win32-x64-msvc": "4.36.0",
2445
+                "@rollup/rollup-android-arm-eabi": "4.40.2",
2446
+                "@rollup/rollup-android-arm64": "4.40.2",
2447
+                "@rollup/rollup-darwin-arm64": "4.40.2",
2448
+                "@rollup/rollup-darwin-x64": "4.40.2",
2449
+                "@rollup/rollup-freebsd-arm64": "4.40.2",
2450
+                "@rollup/rollup-freebsd-x64": "4.40.2",
2451
+                "@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
2452
+                "@rollup/rollup-linux-arm-musleabihf": "4.40.2",
2453
+                "@rollup/rollup-linux-arm64-gnu": "4.40.2",
2454
+                "@rollup/rollup-linux-arm64-musl": "4.40.2",
2455
+                "@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
2456
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
2457
+                "@rollup/rollup-linux-riscv64-gnu": "4.40.2",
2458
+                "@rollup/rollup-linux-riscv64-musl": "4.40.2",
2459
+                "@rollup/rollup-linux-s390x-gnu": "4.40.2",
2460
+                "@rollup/rollup-linux-x64-gnu": "4.40.2",
2461
+                "@rollup/rollup-linux-x64-musl": "4.40.2",
2462
+                "@rollup/rollup-win32-arm64-msvc": "4.40.2",
2463
+                "@rollup/rollup-win32-ia32-msvc": "4.40.2",
2464
+                "@rollup/rollup-win32-x64-msvc": "4.40.2",
2450 2465
                 "fsevents": "~2.3.2"
2451 2466
             }
2452 2467
         },
@@ -2735,6 +2750,51 @@
2735 2750
                 "node": ">=0.8"
2736 2751
             }
2737 2752
         },
2753
+        "node_modules/tinyglobby": {
2754
+            "version": "0.2.13",
2755
+            "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
2756
+            "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
2757
+            "dev": true,
2758
+            "license": "MIT",
2759
+            "dependencies": {
2760
+                "fdir": "^6.4.4",
2761
+                "picomatch": "^4.0.2"
2762
+            },
2763
+            "engines": {
2764
+                "node": ">=12.0.0"
2765
+            },
2766
+            "funding": {
2767
+                "url": "https://github.com/sponsors/SuperchupuDev"
2768
+            }
2769
+        },
2770
+        "node_modules/tinyglobby/node_modules/fdir": {
2771
+            "version": "6.4.4",
2772
+            "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
2773
+            "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
2774
+            "dev": true,
2775
+            "license": "MIT",
2776
+            "peerDependencies": {
2777
+                "picomatch": "^3 || ^4"
2778
+            },
2779
+            "peerDependenciesMeta": {
2780
+                "picomatch": {
2781
+                    "optional": true
2782
+                }
2783
+            }
2784
+        },
2785
+        "node_modules/tinyglobby/node_modules/picomatch": {
2786
+            "version": "4.0.2",
2787
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
2788
+            "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
2789
+            "dev": true,
2790
+            "license": "MIT",
2791
+            "engines": {
2792
+                "node": ">=12"
2793
+            },
2794
+            "funding": {
2795
+                "url": "https://github.com/sponsors/jonschlinkert"
2796
+            }
2797
+        },
2738 2798
         "node_modules/to-regex-range": {
2739 2799
             "version": "5.0.1",
2740 2800
             "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2794,15 +2854,18 @@
2794 2854
             "license": "MIT"
2795 2855
         },
2796 2856
         "node_modules/vite": {
2797
-            "version": "6.2.2",
2798
-            "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
2799
-            "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==",
2857
+            "version": "6.3.5",
2858
+            "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
2859
+            "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
2800 2860
             "dev": true,
2801 2861
             "license": "MIT",
2802 2862
             "dependencies": {
2803 2863
                 "esbuild": "^0.25.0",
2864
+                "fdir": "^6.4.4",
2865
+                "picomatch": "^4.0.2",
2804 2866
                 "postcss": "^8.5.3",
2805
-                "rollup": "^4.30.1"
2867
+                "rollup": "^4.34.9",
2868
+                "tinyglobby": "^0.2.13"
2806 2869
             },
2807 2870
             "bin": {
2808 2871
                 "vite": "bin/vite.js"
@@ -2876,6 +2939,34 @@
2876 2939
                 "picomatch": "^2.3.1"
2877 2940
             }
2878 2941
         },
2942
+        "node_modules/vite/node_modules/fdir": {
2943
+            "version": "6.4.4",
2944
+            "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
2945
+            "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
2946
+            "dev": true,
2947
+            "license": "MIT",
2948
+            "peerDependencies": {
2949
+                "picomatch": "^3 || ^4"
2950
+            },
2951
+            "peerDependenciesMeta": {
2952
+                "picomatch": {
2953
+                    "optional": true
2954
+                }
2955
+            }
2956
+        },
2957
+        "node_modules/vite/node_modules/picomatch": {
2958
+            "version": "4.0.2",
2959
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
2960
+            "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
2961
+            "dev": true,
2962
+            "license": "MIT",
2963
+            "engines": {
2964
+                "node": ">=12"
2965
+            },
2966
+            "funding": {
2967
+                "url": "https://github.com/sponsors/jonschlinkert"
2968
+            }
2969
+        },
2879 2970
         "node_modules/which": {
2880 2971
             "version": "2.0.2",
2881 2972
             "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -2991,16 +3082,16 @@
2991 3082
             }
2992 3083
         },
2993 3084
         "node_modules/yaml": {
2994
-            "version": "2.7.0",
2995
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
2996
-            "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
3085
+            "version": "2.8.0",
3086
+            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
3087
+            "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
2997 3088
             "dev": true,
2998 3089
             "license": "ISC",
2999 3090
             "bin": {
3000 3091
                 "yaml": "bin.mjs"
3001 3092
             },
3002 3093
             "engines": {
3003
-                "node": ">= 14"
3094
+                "node": ">= 14.6"
3004 3095
             }
3005 3096
         }
3006 3097
     }

+ 4
- 0
resources/css/filament/company/theme.css Wyświetl plik

@@ -10,6 +10,10 @@
10 10
 
11 11
 @config 'tailwind.config.js';
12 12
 
13
+.fi-in-text-item .group-hover\/item\:underline, .fi-ta-text-item .group-hover\/item\:underline {
14
+    @apply text-primary-600 dark:text-primary-400 font-semibold;
15
+}
16
+
13 17
 .fi-sidebar-nav {
14 18
     scrollbar-width: thin;
15 19
 }

+ 12
- 30
resources/views/components/company/tables/reports/account-transactions.blade.php Wyświetl plik

@@ -1,6 +1,6 @@
1 1
 @php
2
-    use App\Filament\Company\Pages\Accounting\Transactions;
3 2
     use App\Models\Accounting\Bill;
3
+    use App\Filament\Company\Resources\Accounting\TransactionResource;
4 4
     use App\Filament\Company\Resources\Purchases\BillResource\Pages\ViewBill;
5 5
     use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ViewInvoice;
6 6
 
@@ -49,35 +49,17 @@
49 49
                             ])
50 50
                         >
51 51
                             @if(is_array($cell) && isset($cell['description']))
52
-                                @if(isset($cell['id']) && $cell['tableAction'])
53
-                                    @if($cell['tableAction']['type'] === 'transaction')
54
-                                        <x-filament::link
55
-                                            :href="Transactions::getUrl(parameters: [
56
-                                                'tableAction' => $cell['tableAction']['action'],
57
-                                                'tableActionRecord' => $cell['tableAction']['id'],
58
-                                            ])"
59
-                                            target="_blank"
60
-                                            color="primary"
61
-                                            icon="heroicon-o-arrow-top-right-on-square"
62
-                                            :icon-position="$iconPosition"
63
-                                            icon-size="w-4 h-4 min-w-4 min-h-4"
64
-                                        >
65
-                                            {{ $cell['description'] }}
66
-                                        </x-filament::link>
67
-                                    @else
68
-                                        <x-filament::link
69
-                                            :href="$cell['tableAction']['model'] === Bill::class
70
-                                                ? ViewBill::getUrl(['record' => $cell['tableAction']['id']])
71
-                                                : ViewInvoice::getUrl(['record' => $cell['tableAction']['id']])"
72
-                                            target="_blank"
73
-                                            color="primary"
74
-                                            icon="heroicon-o-arrow-top-right-on-square"
75
-                                            :icon-position="$iconPosition"
76
-                                            icon-size="w-4 h-4 min-w-4 min-h-4"
77
-                                        >
78
-                                            {{ $cell['description'] }}
79
-                                        </x-filament::link>
80
-                                    @endif
52
+                                @if(isset($cell['id']) && isset($cell['url']))
53
+                                    <x-filament::link
54
+                                        :href="$cell['url']"
55
+                                        target="_blank"
56
+                                        color="primary"
57
+                                        icon="heroicon-o-arrow-top-right-on-square"
58
+                                        :icon-position="$iconPosition"
59
+                                        icon-size="w-4 h-4 min-w-4 min-h-4"
60
+                                    >
61
+                                        {{ $cell['description'] }}
62
+                                    </x-filament::link>
81 63
                                 @else
82 64
                                     {{ $cell['description'] }}
83 65
                                 @endif

+ 10
- 10
resources/views/filament/company/pages/accounting/chart.blade.php Wyświetl plik

@@ -1,19 +1,19 @@
1 1
 <x-filament-panels::page>
2 2
     <div class="flex flex-col gap-y-6">
3 3
         <x-filament::tabs>
4
-            @foreach($this->categories as $categoryValue => $subtypes)
4
+            @foreach($this->accountCategories as $categoryValue => $accountSubtypes)
5 5
                 <x-filament::tabs.item
6 6
                     wire:key="tab-item-{{ $categoryValue }}"
7 7
                     :active="$activeTab === $categoryValue"
8 8
                     wire:click="$set('activeTab', '{{ $categoryValue }}')"
9
-                    :badge="$subtypes->sum('accounts_count')"
9
+                    :badge="$accountSubtypes->sum('accounts_count')"
10 10
                 >
11 11
                     {{ $this->getCategoryLabel($categoryValue) }}
12 12
                 </x-filament::tabs.item>
13 13
             @endforeach
14 14
         </x-filament::tabs>
15 15
 
16
-        @foreach($this->categories as $categoryValue => $subtypes)
16
+        @foreach($this->accountCategories as $categoryValue => $accountSubtypes)
17 17
             @if($activeTab === $categoryValue)
18 18
                 <div
19 19
                     class="es-table__container overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:divide-white/10 dark:bg-gray-900 dark:ring-white/10">
@@ -29,7 +29,7 @@
29 29
                                 <col span="1" style="width: 10%;">
30 30
                                 <col span="1" style="width: 7.5%;">
31 31
                             </colgroup>
32
-                            @foreach($subtypes as $subtype)
32
+                            @foreach($accountSubtypes as $accountSubtype)
33 33
                                 <tbody
34 34
                                     class="es-table__rowgroup divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
35 35
                                 <!-- Subtype Name Header Row -->
@@ -38,10 +38,10 @@
38 38
                                         <div class="es-table__row-content flex items-center space-x-2">
39 39
                                             <span
40 40
                                                 class="es-table__row-title text-gray-800 dark:text-gray-200 font-semibold tracking-wider">
41
-                                                {{ $subtype->name }}
41
+                                                {{ $accountSubtype->name }}
42 42
                                             </span>
43 43
                                             <x-tooltip
44
-                                                text="{!! $subtype->description !!}"
44
+                                                text="{!! $accountSubtype->description !!}"
45 45
                                                 icon="heroicon-o-question-mark-circle"
46 46
                                                 placement="right"
47 47
                                                 maxWidth="300"
@@ -51,7 +51,7 @@
51 51
                                 </tr>
52 52
 
53 53
                                 <!-- Chart Rows -->
54
-                                @forelse($subtype->accounts as $account)
54
+                                @forelse($accountSubtype->accounts as $account)
55 55
                                     <tr class="es-table__row">
56 56
                                         <td colspan="1" class="es-table__cell px-4 py-4">{{ $account->code }}</td>
57 57
                                         <td colspan="1" class="es-table__cell px-4 py-4">
@@ -78,7 +78,7 @@
78 78
                                         <td colspan="1" class="es-table__cell px-4 py-4">
79 79
                                             <div>
80 80
                                                 @if($account->default === false && !$account->adjustment)
81
-                                                    {{ ($this->editChartAction)(['chart' => $account->id]) }}
81
+                                                    {{ ($this->editAccountAction)(['account' => $account->id]) }}
82 82
                                                 @endif
83 83
                                             </div>
84 84
                                         </td>
@@ -88,7 +88,7 @@
88 88
                                     <tr class="es-table__row">
89 89
                                         <td colspan="5"
90 90
                                             class="es-table__cell px-4 py-4 italic text-xs text-gray-500 dark:text-gray-400">
91
-                                            {{ __("You haven't added any {$subtype->name} accounts yet.") }}
91
+                                            {{ __("You haven't added any {$accountSubtype->name} accounts yet.") }}
92 92
                                         </td>
93 93
                                     </tr>
94 94
                                 @endforelse
@@ -96,7 +96,7 @@
96 96
                                 <!-- Add New Account Row -->
97 97
                                 <tr class="es-table__row">
98 98
                                     <td colspan="5" class="es-table__cell px-4 py-4">
99
-                                        {{ ($this->createChartAction)(['subtype' => $subtype->id]) }}
99
+                                        {{ ($this->createAccountAction)(['accountSubtype' => $accountSubtype->id]) }}
100 100
                                     </td>
101 101
                                 </tr>
102 102
                                 </tbody>

+ 0
- 3
resources/views/filament/company/pages/accounting/transactions.blade.php Wyświetl plik

@@ -1,3 +0,0 @@
1
-<x-filament-panels::page>
2
-    {{ $this->table }}
3
-</x-filament-panels::page>

+ 21
- 49
tests/Feature/Accounting/TransactionTest.php Wyświetl plik

@@ -2,8 +2,9 @@
2 2
 
3 3
 use App\Enums\Accounting\JournalEntryType;
4 4
 use App\Enums\Accounting\TransactionType;
5
-use App\Filament\Company\Pages\Accounting\Transactions;
5
+use App\Filament\Company\Resources\Accounting\TransactionResource\Pages\ListTransactions;
6 6
 use App\Filament\Forms\Components\JournalEntryRepeater;
7
+use App\Filament\Tables\Actions\EditTransactionAction;
7 8
 use App\Filament\Tables\Actions\ReplicateBulkAction;
8 9
 use App\Models\Accounting\Account;
9 10
 use App\Models\Accounting\Transaction;
@@ -227,9 +228,9 @@ it('handles multi-currency withdrawals correctly', function () {
227 228
 it('can add an income or expense transaction', function (TransactionType $transactionType, string $actionName) {
228 229
     $testCompany = $this->testCompany;
229 230
     $defaultBankAccount = $testCompany->default->bankAccount;
230
-    $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
231
+    $defaultAccount = Transaction::getUncategorizedAccountByType($transactionType);
231 232
 
232
-    livewire(Transactions::class)
233
+    livewire(ListTransactions::class)
233 234
         ->mountAction($actionName)
234 235
         ->assertActionDataSet([
235 236
             'posted_at' => today(),
@@ -254,8 +255,8 @@ it('can add an income or expense transaction', function (TransactionType $transa
254 255
         ->account->is($defaultAccount)->toBeTrue()
255 256
         ->journalEntries->count()->toBe(2);
256 257
 })->with([
257
-    [TransactionType::Deposit, 'addIncome'],
258
-    [TransactionType::Withdrawal, 'addExpense'],
258
+    [TransactionType::Deposit, 'createDeposit'],
259
+    [TransactionType::Withdrawal, 'createWithdrawal'],
259 260
 ]);
260 261
 
261 262
 it('can add a transfer transaction', function () {
@@ -263,8 +264,8 @@ it('can add a transfer transaction', function () {
263 264
     $sourceBankAccount = $testCompany->default->bankAccount;
264 265
     $destinationBankAccount = Account::factory()->withBankAccount('Destination Bank Account')->create();
265 266
 
266
-    livewire(Transactions::class)
267
-        ->mountAction('addTransfer')
267
+    livewire(ListTransactions::class)
268
+        ->mountAction('createTransfer')
268 269
         ->assertActionDataSet([
269 270
             'posted_at' => today(),
270 271
             'type' => TransactionType::Transfer,
@@ -291,13 +292,13 @@ it('can add a transfer transaction', function () {
291 292
 });
292 293
 
293 294
 it('can add a journal transaction', function () {
294
-    $defaultDebitAccount = Transactions::getUncategorizedAccountByType(TransactionType::Withdrawal);
295
-    $defaultCreditAccount = Transactions::getUncategorizedAccountByType(TransactionType::Deposit);
295
+    $defaultDebitAccount = Transaction::getUncategorizedAccountByType(TransactionType::Withdrawal);
296
+    $defaultCreditAccount = Transaction::getUncategorizedAccountByType(TransactionType::Deposit);
296 297
 
297 298
     $undoRepeaterFake = JournalEntryRepeater::fake();
298 299
 
299
-    livewire(Transactions::class)
300
-        ->mountAction('addJournalTransaction')
300
+    livewire(ListTransactions::class)
301
+        ->mountAction('createJournalEntry')
301 302
         ->assertActionDataSet([
302 303
             'posted_at' => today(),
303 304
             'journalEntries' => [
@@ -334,7 +335,7 @@ it('can add a journal transaction', function () {
334 335
 });
335 336
 
336 337
 it('can update a deposit or withdrawal transaction', function (TransactionType $transactionType) {
337
-    $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
338
+    $defaultAccount = Transaction::getUncategorizedAccountByType($transactionType);
338 339
 
339 340
     $transaction = Transaction::factory()
340 341
         ->forDefaultBankAccount()
@@ -344,8 +345,8 @@ it('can update a deposit or withdrawal transaction', function (TransactionType $
344 345
 
345 346
     $newDescription = 'Updated Description';
346 347
 
347
-    livewire(Transactions::class)
348
-        ->mountTableAction('editTransaction', $transaction)
348
+    livewire(ListTransactions::class)
349
+        ->mountTableAction(EditTransactionAction::class, $transaction)
349 350
         ->assertTableActionDataSet([
350 351
             'type' => $transactionType->value,
351 352
             'description' => $transaction->description,
@@ -367,23 +368,6 @@ it('can update a deposit or withdrawal transaction', function (TransactionType $
367 368
     TransactionType::Withdrawal,
368 369
 ]);
369 370
 
370
-it('does not show Edit Transfer or Edit Journal Transaction for deposit or withdrawal transactions', function (TransactionType $transactionType) {
371
-    $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
372
-
373
-    $transaction = Transaction::factory()
374
-        ->forDefaultBankAccount()
375
-        ->forAccount($defaultAccount)
376
-        ->forType($transactionType, 1000)
377
-        ->create();
378
-
379
-    livewire(Transactions::class)
380
-        ->assertTableActionHidden('editTransfer', $transaction)
381
-        ->assertTableActionHidden('editJournalTransaction', $transaction);
382
-})->with([
383
-    TransactionType::Deposit,
384
-    TransactionType::Withdrawal,
385
-]);
386
-
387 371
 it('can update a transfer transaction', function () {
388 372
     $transaction = Transaction::factory()
389 373
         ->forDefaultBankAccount()
@@ -393,8 +377,8 @@ it('can update a transfer transaction', function () {
393 377
 
394 378
     $newDescription = 'Updated Transfer Description';
395 379
 
396
-    livewire(Transactions::class)
397
-        ->mountTableAction('editTransfer', $transaction)
380
+    livewire(ListTransactions::class)
381
+        ->mountTableAction(EditTransactionAction::class, $transaction)
398 382
         ->assertTableActionDataSet([
399 383
             'type' => TransactionType::Transfer->value,
400 384
             'description' => $transaction->description,
@@ -413,18 +397,6 @@ it('can update a transfer transaction', function () {
413 397
         ->and($transaction->amount)->toEqual('2,000.00');
414 398
 });
415 399
 
416
-it('does not show Edit Transaction or Edit Journal Transaction for transfer transactions', function () {
417
-    $transaction = Transaction::factory()
418
-        ->forDefaultBankAccount()
419
-        ->forDestinationBankAccount()
420
-        ->asTransfer(1500)
421
-        ->create();
422
-
423
-    livewire(Transactions::class)
424
-        ->assertTableActionHidden('editTransaction', $transaction)
425
-        ->assertTableActionHidden('editJournalTransaction', $transaction);
426
-});
427
-
428 400
 it('replicates a transaction with correct journal entries', function () {
429 401
     $originalTransaction = Transaction::factory()
430 402
         ->forDefaultBankAccount()
@@ -432,7 +404,7 @@ it('replicates a transaction with correct journal entries', function () {
432 404
         ->asDeposit(1000)
433 405
         ->create();
434 406
 
435
-    livewire(Transactions::class)
407
+    livewire(ListTransactions::class)
436 408
         ->callTableAction(ReplicateAction::class, $originalTransaction);
437 409
 
438 410
     $replicatedTransaction = Transaction::whereKeyNot($originalTransaction->getKey())->first();
@@ -460,7 +432,7 @@ it('bulk replicates transactions with correct journal entries', function () {
460 432
         ->count(3)
461 433
         ->create();
462 434
 
463
-    livewire(Transactions::class)
435
+    livewire(ListTransactions::class)
464 436
         ->callTableBulkAction(ReplicateBulkAction::class, $originalTransactions);
465 437
 
466 438
     $replicatedTransactions = Transaction::whereKeyNot($originalTransactions->modelKeys())->get();
@@ -495,7 +467,7 @@ it('can delete a transaction with journal entries', function () {
495 467
 
496 468
     expect($transaction->journalEntries()->count())->toBe(2);
497 469
 
498
-    livewire(Transactions::class)
470
+    livewire(ListTransactions::class)
499 471
         ->callTableAction(DeleteAction::class, $transaction);
500 472
 
501 473
     $this->assertModelMissing($transaction);
@@ -513,7 +485,7 @@ it('can bulk delete transactions with journal entries', function () {
513 485
 
514 486
     expect($transactions->count())->toBe(3);
515 487
 
516
-    livewire(Transactions::class)
488
+    livewire(ListTransactions::class)
517 489
         ->callTableBulkAction(DeleteBulkAction::class, $transactions);
518 490
 
519 491
     $this->assertDatabaseEmpty('transactions');

Ładowanie…
Anuluj
Zapisz