Andrew Wallo 6 місяці тому
джерело
коміт
30981981da

+ 176
- 74
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/CreateBudget.php Переглянути файл

@@ -5,55 +5,97 @@ namespace App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
5 5
 use App\Enums\Accounting\BudgetIntervalType;
6 6
 use App\Facades\Accounting;
7 7
 use App\Filament\Company\Resources\Accounting\BudgetResource;
8
-use App\Filament\Forms\Components\LinearWizard;
8
+use App\Filament\Forms\Components\CustomSection;
9 9
 use App\Models\Accounting\Account;
10 10
 use App\Models\Accounting\Budget;
11 11
 use App\Models\Accounting\BudgetAllocation;
12 12
 use App\Models\Accounting\BudgetItem;
13 13
 use App\Utilities\Currency\CurrencyConverter;
14
-use Filament\Actions\ActionGroup;
15 14
 use Filament\Forms;
16 15
 use Filament\Forms\Components\Actions\Action;
17 16
 use Filament\Forms\Components\Wizard\Step;
18
-use Filament\Forms\Form;
19 17
 use Filament\Resources\Pages\CreateRecord;
20 18
 use Illuminate\Database\Eloquent\Builder;
21 19
 use Illuminate\Database\Eloquent\Model;
22 20
 use Illuminate\Support\Carbon;
21
+use Illuminate\Support\Collection;
23 22
 
24 23
 class CreateBudget extends CreateRecord
25 24
 {
25
+    use CreateRecord\Concerns\HasWizard;
26
+
26 27
     protected static string $resource = BudgetResource::class;
27 28
 
28
-    public function getStartStep(): int
29
+    // Add computed properties
30
+    public function getBudgetableAccounts(): Collection
29 31
     {
30
-        return 1;
32
+        return $this->getAccountsCache('budgetable', function () {
33
+            return Account::query()->budgetable()->get();
34
+        });
31 35
     }
32 36
 
33
-    public function form(Form $form): Form
37
+    public function getAccountsWithActuals(): Collection
34 38
     {
35
-        return parent::form($form)
36
-            ->schema([
37
-                LinearWizard::make($this->getSteps())
38
-                    ->startOnStep($this->getStartStep())
39
-                    ->cancelAction($this->getCancelFormAction())
40
-                    ->submitAction($this->getSubmitFormAction()->label('Next'))
41
-                    ->skippable($this->hasSkippableSteps()),
42
-            ])
43
-            ->columns(null);
39
+        $fiscalYear = $this->data['actuals_fiscal_year'] ?? null;
40
+
41
+        if (blank($fiscalYear)) {
42
+            return collect();
43
+        }
44
+
45
+        return $this->getAccountsCache("actuals_{$fiscalYear}", function () use ($fiscalYear) {
46
+            return Account::query()
47
+                ->budgetable()
48
+                ->whereHas('journalEntries.transaction', function (Builder $query) use ($fiscalYear) {
49
+                    $query->whereYear('posted_at', $fiscalYear);
50
+                })
51
+                ->get();
52
+        });
44 53
     }
45 54
 
46
-    /**
47
-     * @return array<\Filament\Actions\Action | ActionGroup>
48
-     */
49
-    public function getFormActions(): array
55
+    public function getAccountsWithoutActuals(): Collection
50 56
     {
51
-        return [];
57
+        $fiscalYear = $this->data['actuals_fiscal_year'] ?? null;
58
+
59
+        if (blank($fiscalYear)) {
60
+            return collect();
61
+        }
62
+
63
+        $budgetableAccounts = $this->getBudgetableAccounts();
64
+        $accountsWithActuals = $this->getAccountsWithActuals();
65
+
66
+        return $budgetableAccounts->whereNotIn('id', $accountsWithActuals->pluck('id'));
52 67
     }
53 68
 
54
-    protected function hasSkippableSteps(): bool
69
+    public function getAccountBalances(): Collection
55 70
     {
56
-        return false;
71
+        $fiscalYear = $this->data['actuals_fiscal_year'] ?? null;
72
+
73
+        if (blank($fiscalYear)) {
74
+            return collect();
75
+        }
76
+
77
+        return $this->getAccountsCache("balances_{$fiscalYear}", function () use ($fiscalYear) {
78
+            $fiscalYearStart = Carbon::create($fiscalYear, 1, 1)->startOfYear();
79
+            $fiscalYearEnd = $fiscalYearStart->copy()->endOfYear();
80
+
81
+            return Accounting::getAccountBalances(
82
+                $fiscalYearStart->toDateString(),
83
+                $fiscalYearEnd->toDateString(),
84
+                $this->getBudgetableAccounts()->pluck('id')->toArray()
85
+            )->get();
86
+        });
87
+    }
88
+
89
+    // Cache helper to avoid duplicate queries
90
+    private array $accountsCache = [];
91
+
92
+    private function getAccountsCache(string $key, callable $callback): Collection
93
+    {
94
+        if (! isset($this->accountsCache[$key])) {
95
+            $this->accountsCache[$key] = $callback();
96
+        }
97
+
98
+        return $this->accountsCache[$key];
57 99
     }
58 100
 
59 101
     public function getSteps(): array
@@ -138,65 +180,125 @@ class CreateBudget extends CreateRecord
138 180
                                 })
139 181
                                 ->required()
140 182
                                 ->live()
183
+                                ->afterStateUpdated(function (Forms\Set $set) {
184
+                                    // Clear the cache when the fiscal year changes
185
+                                    $this->accountsCache = [];
186
+
187
+                                    // Get all accounts without actuals
188
+                                    $accountIdsWithoutActuals = $this->getAccountsWithoutActuals()->pluck('id')->toArray();
189
+
190
+                                    // Set exclude_accounts_without_actuals to true by default
191
+                                    $set('exclude_accounts_without_actuals', true);
192
+
193
+                                    // Update the selected_accounts field to exclude accounts without actuals
194
+                                    $set('selected_accounts', $accountIdsWithoutActuals);
195
+                                })
141 196
                                 ->visible(fn (Forms\Get $get) => $get('prefill_method') === 'actuals'),
142 197
                         ])->visible(fn (Forms\Get $get) => $get('prefill_data') === true),
143 198
 
199
+                    CustomSection::make('Account Selection')
200
+                        ->contained(false)
201
+                        ->schema([
202
+                            Forms\Components\Checkbox::make('exclude_accounts_without_actuals')
203
+                                ->label('Exclude all accounts without actuals')
204
+                                ->helperText(function () {
205
+                                    $count = $this->getAccountsWithoutActuals()->count();
206
+
207
+                                    return "Will exclude {$count} accounts without transaction data in the selected fiscal year";
208
+                                })
209
+                                ->default(true)
210
+                                ->live()
211
+                                ->afterStateUpdated(function (Forms\Set $set, $state) {
212
+                                    if ($state) {
213
+                                        // When checked, select all accounts without actuals
214
+                                        $accountsWithoutActuals = $this->getAccountsWithoutActuals()->pluck('id')->toArray();
215
+                                        $set('selected_accounts', $accountsWithoutActuals);
216
+                                    } else {
217
+                                        // When unchecked, clear the selection
218
+                                        $set('selected_accounts', []);
219
+                                    }
220
+                                }),
221
+
222
+                            Forms\Components\CheckboxList::make('selected_accounts')
223
+                                ->label('Select Accounts to Exclude')
224
+                                ->options(function () {
225
+                                    // Get all budgetable accounts
226
+                                    return $this->getBudgetableAccounts()->pluck('name', 'id')->toArray();
227
+                                })
228
+                                ->descriptions(function (Forms\Components\CheckboxList $component) {
229
+                                    $fiscalYear = $this->data['actuals_fiscal_year'] ?? null;
230
+
231
+                                    if (blank($fiscalYear)) {
232
+                                        return [];
233
+                                    }
234
+
235
+                                    $accountIds = array_keys($component->getOptions());
236
+                                    $descriptions = [];
237
+
238
+                                    if (empty($accountIds)) {
239
+                                        return [];
240
+                                    }
241
+
242
+                                    // Get account balances
243
+                                    $accountBalances = $this->getAccountBalances()->keyBy('id');
244
+
245
+                                    // Get accounts with actuals
246
+                                    $accountsWithActuals = $this->getAccountsWithActuals()->pluck('id')->toArray();
247
+
248
+                                    // Process all accounts
249
+                                    foreach ($accountIds as $accountId) {
250
+                                        $balance = $accountBalances[$accountId] ?? null;
251
+                                        $hasActuals = in_array($accountId, $accountsWithActuals);
252
+
253
+                                        if ($balance && $hasActuals) {
254
+                                            // Calculate net movement
255
+                                            $netMovement = Accounting::calculateNetMovementByCategory(
256
+                                                $balance->category,
257
+                                                $balance->total_debit ?? 0,
258
+                                                $balance->total_credit ?? 0
259
+                                            );
260
+
261
+                                            // Format the amount for display
262
+                                            $formattedAmount = CurrencyConverter::formatCentsToMoney($netMovement);
263
+                                            $descriptions[$accountId] = "{$formattedAmount} in {$fiscalYear}";
264
+                                        } else {
265
+                                            $descriptions[$accountId] = "No transactions in {$fiscalYear}";
266
+                                        }
267
+                                    }
268
+
269
+                                    return $descriptions;
270
+                                })
271
+                                ->columns(2) // Display in two columns
272
+                                ->searchable() // Allow searching for accounts
273
+                                ->bulkToggleable() // Enable "Select All" / "Deselect All"
274
+                                ->selectAllAction(fn (Action $action) => $action->label('Exclude all accounts'))
275
+                                ->deselectAllAction(fn (Action $action) => $action->label('Include all accounts'))
276
+                                ->afterStateUpdated(function (Forms\Set $set, $state) {
277
+                                    // Get all accounts without actuals
278
+                                    $accountsWithoutActuals = $this->getAccountsWithoutActuals()->pluck('id')->toArray();
279
+
280
+                                    // Check if all accounts without actuals are in the selected accounts
281
+                                    $allAccountsWithoutActualsSelected = empty(array_diff($accountsWithoutActuals, $state));
282
+
283
+                                    // Update the exclude_accounts_without_actuals checkbox state
284
+                                    $set('exclude_accounts_without_actuals', $allAccountsWithoutActualsSelected);
285
+                                }),
286
+                        ])
287
+                        ->visible(function () {
288
+                            // Only show when using actuals with valid fiscal year AND accounts without transactions exist
289
+                            $prefillMethod = $this->data['prefill_method'] ?? null;
290
+
291
+                            if ($prefillMethod !== 'actuals' || blank($this->data['actuals_fiscal_year'] ?? null)) {
292
+                                return false;
293
+                            }
294
+
295
+                            return $this->getAccountsWithoutActuals()->isNotEmpty();
296
+                        }),
297
+
144 298
                     Forms\Components\Textarea::make('notes')
145 299
                         ->label('Notes')
146 300
                         ->columnSpanFull(),
147 301
                 ]),
148
-
149
-            Step::make('Modify Budget Structure')
150
-                ->icon('heroicon-o-adjustments-horizontal')
151
-                ->schema([
152
-                    Forms\Components\CheckboxList::make('selected_accounts')
153
-                        ->label('Select Accounts to Exclude')
154
-                        ->options(function (Forms\Get $get) {
155
-                            $fiscalYear = $get('actuals_fiscal_year');
156
-
157
-                            // Get all budgetable accounts
158
-                            $allAccounts = Account::query()->budgetable()->pluck('name', 'id')->toArray();
159
-
160
-                            // Get accounts that have actuals for the selected fiscal year
161
-                            $accountsWithActuals = Account::query()
162
-                                ->budgetable()
163
-                                ->whereHas('journalEntries.transaction', function (Builder $query) use ($fiscalYear) {
164
-                                    $query->whereYear('posted_at', $fiscalYear);
165
-                                })
166
-                                ->pluck('name', 'id')
167
-                                ->toArray();
168
-
169
-                            return $allAccounts + $accountsWithActuals; // Merge both sets
170
-                        })
171
-                        ->columns(2) // Display in two columns
172
-                        ->searchable() // Allow searching for accounts
173
-                        ->bulkToggleable() // Enable "Select All" / "Deselect All"
174
-                        ->selectAllAction(
175
-                            fn (Action $action, Forms\Get $get) => $action
176
-                                ->label('Remove all items without past actuals (' .
177
-                                    Account::query()->budgetable()->whereDoesntHave('journalEntries.transaction', function (Builder $query) use ($get) {
178
-                                        $query->whereYear('posted_at', $get('actuals_fiscal_year'));
179
-                                    })->count() . ' lines)')
180
-                        )
181
-                        ->disableOptionWhen(fn (string $value, Forms\Get $get) => in_array(
182
-                            $value,
183
-                            Account::query()->budgetable()->whereHas('journalEntries.transaction', function (Builder $query) use ($get) {
184
-                                $query->whereYear('posted_at', Carbon::parse($get('actuals_fiscal_year'))->year);
185
-                            })->pluck('id')->toArray()
186
-                        ))
187
-                        ->visible(fn (Forms\Get $get) => $get('prefill_method') === 'actuals'),
188
-                ])
189
-                ->visible(function (Forms\Get $get) {
190
-                    $prefillMethod = $get('prefill_method');
191
-
192
-                    if ($prefillMethod !== 'actuals' || blank($get('actuals_fiscal_year'))) {
193
-                        return false;
194
-                    }
195
-
196
-                    return Account::query()->budgetable()->whereDoesntHave('journalEntries.transaction', function (Builder $query) use ($get) {
197
-                        $query->whereYear('posted_at', $get('actuals_fiscal_year'));
198
-                    })->exists();
199
-                }),
200 302
         ];
201 303
     }
202 304
 

+ 1
- 1
app/Services/AccountService.php Переглянути файл

@@ -78,7 +78,7 @@ class AccountService
78 78
         return new Money($endingBalance, $account->currency_code);
79 79
     }
80 80
 
81
-    private function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int
81
+    public function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int
82 82
     {
83 83
         if ($category->isNormalDebitBalance()) {
84 84
             return $debitBalance - $creditBalance;

+ 12
- 12
composer.lock Переглянути файл

@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.342.7",
500
+            "version": "3.342.8",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "4d42e384dac1e71107226ed72ed5e8e4d4ee3358"
504
+                "reference": "d8279a481cf87482a1fb74784f76e4160efcce90"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4d42e384dac1e71107226ed72ed5e8e4d4ee3358",
509
-                "reference": "4d42e384dac1e71107226ed72ed5e8e4d4ee3358",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d8279a481cf87482a1fb74784f76e4160efcce90",
509
+                "reference": "d8279a481cf87482a1fb74784f76e4160efcce90",
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.342.7"
591
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.342.8"
592 592
             },
593
-            "time": "2025-03-17T18:18:04+00:00"
593
+            "time": "2025-03-18T18:15:40+00:00"
594 594
         },
595 595
         {
596 596
             "name": "aws/aws-sdk-php-laravel",
@@ -12171,16 +12171,16 @@
12171 12171
         },
12172 12172
         {
12173 12173
             "name": "sebastian/type",
12174
-            "version": "5.1.0",
12174
+            "version": "5.1.2",
12175 12175
             "source": {
12176 12176
                 "type": "git",
12177 12177
                 "url": "https://github.com/sebastianbergmann/type.git",
12178
-                "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac"
12178
+                "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e"
12179 12179
             },
12180 12180
             "dist": {
12181 12181
                 "type": "zip",
12182
-                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac",
12183
-                "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac",
12182
+                "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e",
12183
+                "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e",
12184 12184
                 "shasum": ""
12185 12185
             },
12186 12186
             "require": {
@@ -12216,7 +12216,7 @@
12216 12216
             "support": {
12217 12217
                 "issues": "https://github.com/sebastianbergmann/type/issues",
12218 12218
                 "security": "https://github.com/sebastianbergmann/type/security/policy",
12219
-                "source": "https://github.com/sebastianbergmann/type/tree/5.1.0"
12219
+                "source": "https://github.com/sebastianbergmann/type/tree/5.1.2"
12220 12220
             },
12221 12221
             "funding": [
12222 12222
                 {
@@ -12224,7 +12224,7 @@
12224 12224
                     "type": "github"
12225 12225
                 }
12226 12226
             ],
12227
-            "time": "2024-09-17T13:12:04+00:00"
12227
+            "time": "2025-03-18T13:35:50+00:00"
12228 12228
         },
12229 12229
         {
12230 12230
             "name": "sebastian/version",

+ 6
- 6
package-lock.json Переглянути файл

@@ -1088,9 +1088,9 @@
1088 1088
             }
1089 1089
         },
1090 1090
         "node_modules/caniuse-lite": {
1091
-            "version": "1.0.30001705",
1092
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz",
1093
-            "integrity": "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==",
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==",
1094 1094
             "dev": true,
1095 1095
             "funding": [
1096 1096
                 {
@@ -1930,9 +1930,9 @@
1930 1930
             }
1931 1931
         },
1932 1932
         "node_modules/nanoid": {
1933
-            "version": "3.3.10",
1934
-            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz",
1935
-            "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==",
1933
+            "version": "3.3.11",
1934
+            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1935
+            "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1936 1936
             "dev": true,
1937 1937
             "funding": [
1938 1938
                 {

+ 2
- 1
resources/data/lang/en.json Переглянути файл

@@ -200,5 +200,6 @@
200 200
     "Column Labels": "Column Labels",
201 201
     "Budget Details": "Budget Details",
202 202
     "Budget Items": "Budget Items",
203
-    "Budget Allocations": "Budget Allocations"
203
+    "Budget Allocations": "Budget Allocations",
204
+    "Account Selection": "Account Selection"
204 205
 }

Завантаження…
Відмінити
Зберегти