Bläddra i källkod

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

Development 3.x
3.x
Andrew Wallo 10 månader sedan
förälder
incheckning
59a13df4f2
Inget konto är kopplat till bidragsgivarens mejladress
38 ändrade filer med 2053 tillägg och 1352 borttagningar
  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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

@@ -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 Visa fil

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

+ 21
- 49
tests/Feature/Accounting/TransactionTest.php Visa fil

@@ -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');

Laddar…
Avbryt
Spara