Andrew Wallo 6 months ago
parent
commit
6052033f98

+ 2
- 2
app/Filament/Company/Resources/Accounting/BudgetResource.php View File

292
                                                 // Calculate the total for this budget item across all periods
292
                                                 // Calculate the total for this budget item across all periods
293
                                                 foreach ($periods as $period) {
293
                                                 foreach ($periods as $period) {
294
                                                     $allocation = $record->allocations->firstWhere('period', $period);
294
                                                     $allocation = $record->allocations->firstWhere('period', $period);
295
-                                                    $total += $allocation ? $allocation->amount : 0;
295
+                                                    $total += $allocation ? $allocation->getRawOriginal('amount') : 0;
296
                                                 }
296
                                                 }
297
-                                                $component->state($total);
297
+                                                $component->state(CurrencyConverter::convertCentsToFormatSimple($total));
298
                                             })
298
                                             })
299
                                             ->dehydrated(false),
299
                                             ->dehydrated(false),
300
 
300
 

+ 6
- 0
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/ViewBudget.php View File

7
 use Filament\Forms\Form;
7
 use Filament\Forms\Form;
8
 use Filament\Infolists\Infolist;
8
 use Filament\Infolists\Infolist;
9
 use Filament\Resources\Pages\ViewRecord;
9
 use Filament\Resources\Pages\ViewRecord;
10
+use Filament\Support\Enums\MaxWidth;
10
 
11
 
11
 class ViewBudget extends ViewRecord
12
 class ViewBudget extends ViewRecord
12
 {
13
 {
13
     protected static string $resource = BudgetResource::class;
14
     protected static string $resource = BudgetResource::class;
14
 
15
 
16
+    public function getMaxContentWidth(): MaxWidth | string | null
17
+    {
18
+        return '8xl';
19
+    }
20
+
15
     protected function getHeaderActions(): array
21
     protected function getHeaderActions(): array
16
     {
22
     {
17
         return [
23
         return [

+ 174
- 8
app/Filament/Company/Resources/Accounting/BudgetResource/RelationManagers/BudgetItemsRelationManager.php View File

5
 use App\Filament\Tables\Columns\DeferredTextInputColumn;
5
 use App\Filament\Tables\Columns\DeferredTextInputColumn;
6
 use App\Models\Accounting\BudgetAllocation;
6
 use App\Models\Accounting\BudgetAllocation;
7
 use App\Models\Accounting\BudgetItem;
7
 use App\Models\Accounting\BudgetItem;
8
+use App\Utilities\Currency\CurrencyConverter;
8
 use Filament\Notifications\Notification;
9
 use Filament\Notifications\Notification;
9
 use Filament\Resources\RelationManagers\RelationManager;
10
 use Filament\Resources\RelationManagers\RelationManager;
11
+use Filament\Support\RawJs;
10
 use Filament\Tables\Actions\Action;
12
 use Filament\Tables\Actions\Action;
13
+use Filament\Tables\Actions\BulkAction;
14
+use Filament\Tables\Columns\IconColumn;
15
+use Filament\Tables\Columns\Summarizers\Summarizer;
11
 use Filament\Tables\Columns\TextColumn;
16
 use Filament\Tables\Columns\TextColumn;
17
+use Filament\Tables\Grouping\Group;
12
 use Filament\Tables\Table;
18
 use Filament\Tables\Table;
19
+use Illuminate\Database\Eloquent\Builder;
20
+use Illuminate\Database\Eloquent\Collection;
13
 use Livewire\Attributes\On;
21
 use Livewire\Attributes\On;
14
 
22
 
15
 class BudgetItemsRelationManager extends RelationManager
23
 class BudgetItemsRelationManager extends RelationManager
21
     // Store changes that are pending
29
     // Store changes that are pending
22
     public array $batchChanges = [];
30
     public array $batchChanges = [];
23
 
31
 
24
-    // Listen for events from the custom column
25
-
26
     #[On('batch-column-changed')]
32
     #[On('batch-column-changed')]
27
     public function handleBatchColumnChanged($data): void
33
     public function handleBatchColumnChanged($data): void
28
     {
34
     {
29
-        ray($data);
30
         // Store the changed value
35
         // Store the changed value
31
         $key = "{$data['recordKey']}.{$data['name']}";
36
         $key = "{$data['recordKey']}.{$data['name']}";
32
         $this->batchChanges[$key] = $data['value'];
37
         $this->batchChanges[$key] = $data['value'];
35
     #[On('save-batch-changes')]
40
     #[On('save-batch-changes')]
36
     public function saveBatchChanges(): void
41
     public function saveBatchChanges(): void
37
     {
42
     {
38
-        ray('Saving batch changes');
39
         foreach ($this->batchChanges as $key => $value) {
43
         foreach ($this->batchChanges as $key => $value) {
40
             // Parse the composite key
44
             // Parse the composite key
41
             [$recordKey, $column] = explode('.', $key, 2);
45
             [$recordKey, $column] = explode('.', $key, 2);
77
             ->send();
81
             ->send();
78
     }
82
     }
79
 
83
 
84
+    protected function calculateTotalSum(array $budgetItemIds): int
85
+    {
86
+        // Get all applicable periods
87
+        $periods = BudgetAllocation::whereIn('budget_item_id', $budgetItemIds)
88
+            ->pluck('period')
89
+            ->unique()
90
+            ->values()
91
+            ->toArray();
92
+
93
+        $total = 0;
94
+
95
+        // Sum up each period
96
+        foreach ($periods as $period) {
97
+            $total += $this->calculatePeriodSum($budgetItemIds, $period);
98
+        }
99
+
100
+        return $total;
101
+    }
102
+
103
+    protected function calculatePeriodSum(array $budgetItemIds, string $period): int
104
+    {
105
+        // First get database values
106
+        $dbTotal = BudgetAllocation::whereIn('budget_item_id', $budgetItemIds)
107
+            ->where('period', $period)
108
+            ->sum('amount');
109
+
110
+        // Now add any batch changes
111
+        $batchTotal = 0;
112
+        foreach ($budgetItemIds as $itemId) {
113
+            $key = "{$itemId}.allocations_by_period.{$period}";
114
+            if (isset($this->batchChanges[$key])) {
115
+                // Get the current value from batch changes
116
+                $batchValue = CurrencyConverter::convertToCents($this->batchChanges[$key]);
117
+
118
+                // Find if there's a current allocation in DB
119
+                $existingAmount = BudgetAllocation::where('budget_item_id', $itemId)
120
+                    ->where('period', $period)
121
+                    ->first()
122
+                    ->getRawOriginal('amount');
123
+
124
+                // Add the difference to our running total
125
+                $batchTotal += ($batchValue - $existingAmount);
126
+            }
127
+        }
128
+
129
+        return $dbTotal + $batchTotal;
130
+    }
131
+
80
     public function table(Table $table): Table
132
     public function table(Table $table): Table
81
     {
133
     {
82
         $budget = $this->getOwnerRecord();
134
         $budget = $this->getOwnerRecord();
91
             ->values()
143
             ->values()
92
             ->toArray();
144
             ->toArray();
93
 
145
 
94
-        ray($this->batchChanges);
95
-
96
         return $table
146
         return $table
97
             ->recordTitleAttribute('account_id')
147
             ->recordTitleAttribute('account_id')
98
             ->paginated(false)
148
             ->paginated(false)
99
             ->modifyQueryUsing(
149
             ->modifyQueryUsing(
100
-                fn ($query) => $query->with(['account', 'allocations'])
150
+                fn (Builder $query) => $query->with(['account', 'allocations'])
101
             )
151
             )
102
             ->headerActions([
152
             ->headerActions([
103
                 Action::make('saveBatchChanges')
153
                 Action::make('saveBatchChanges')
106
                     ->color('primary')
156
                     ->color('primary')
107
                     ->icon('heroicon-o-check-circle'),
157
                     ->icon('heroicon-o-check-circle'),
108
             ])
158
             ])
159
+            ->groups([
160
+                Group::make('account.category')
161
+                    ->titlePrefixedWithLabel(false)
162
+                    ->collapsible(),
163
+            ])
164
+            ->defaultGroup('account.category')
165
+            ->bulkActions([
166
+                BulkAction::make('clearAllocations')
167
+                    ->label('Clear Allocations')
168
+                    ->icon('heroicon-o-trash')
169
+                    ->color('danger')
170
+                    ->requiresConfirmation()
171
+                    ->deselectRecordsAfterCompletion()
172
+                    ->action(function (Collection $records) use ($periods) {
173
+                        foreach ($records as $record) {
174
+                            foreach ($periods as $period) {
175
+                                $periodKey = "{$record->getKey()}.allocations_by_period.{$period}";
176
+                                $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
177
+                            }
178
+
179
+                            $totalKey = "{$record->getKey()}.total";
180
+                            $this->batchChanges[$totalKey] = CurrencyConverter::convertCentsToFormatSimple(0);
181
+                        }
182
+                    }),
183
+            ])
109
             ->columns(array_merge([
184
             ->columns(array_merge([
110
                 TextColumn::make('account.name')
185
                 TextColumn::make('account.name')
111
                     ->label('Accounts')
186
                     ->label('Accounts')
112
-                    ->sortable()
187
+                    ->limit(30)
113
                     ->searchable(),
188
                     ->searchable(),
189
+                DeferredTextInputColumn::make('total')
190
+                    ->label('Total')
191
+                    ->alignRight()
192
+                    ->mask(RawJs::make('$money($input)'))
193
+                    ->getStateUsing(function (BudgetItem $record, DeferredTextInputColumn $column) {
194
+                        if (isset($this->batchChanges["{$record->getKey()}.{$column->getName()}"])) {
195
+                            return $this->batchChanges["{$record->getKey()}.{$column->getName()}"];
196
+                        }
197
+
198
+                        $total = $record->allocations->sum(function (BudgetAllocation $budgetAllocation) {
199
+                            return $budgetAllocation->getRawOriginal('amount');
200
+                        });
201
+
202
+                        return CurrencyConverter::convertCentsToFormatSimple($total);
203
+                    })
204
+                    ->batchMode()
205
+                    ->summarize(
206
+                        Summarizer::make()
207
+                            ->using(function (\Illuminate\Database\Query\Builder $query) {
208
+                                $budgetItemIds = $query->pluck('id')->toArray();
209
+                                $total = $this->calculateTotalSum($budgetItemIds);
210
+
211
+                                return CurrencyConverter::convertCentsToFormatSimple($total);
212
+                            })
213
+                    ),
214
+                IconColumn::make('disperseAction')
215
+                    ->icon('heroicon-m-chevron-double-right')
216
+                    ->color('primary')
217
+                    ->label('')
218
+                    ->default('')
219
+                    ->action(
220
+                        Action::make('disperse')
221
+                            ->label('Disperse')
222
+                            ->icon('heroicon-m-chevron-double-right')
223
+                            ->color('primary')
224
+                            ->iconButton()
225
+                            ->action(function (BudgetItem $record) use ($periods) {
226
+                                if (empty($periods)) {
227
+                                    return;
228
+                                }
229
+
230
+                                $totalKey = "{$record->getKey()}.total";
231
+                                $totalAmount = $this->batchChanges[$totalKey] ?? null;
232
+
233
+                                if (isset($totalAmount)) {
234
+                                    $totalCents = CurrencyConverter::convertToCents($totalAmount);
235
+                                } else {
236
+                                    $totalCents = $record->allocations->sum(function (BudgetAllocation $budgetAllocation) {
237
+                                        return $budgetAllocation->getRawOriginal('amount');
238
+                                    });
239
+                                }
240
+
241
+                                $numPeriods = count($periods);
242
+
243
+                                if ($numPeriods === 0) {
244
+                                    return;
245
+                                }
246
+
247
+                                if ($totalCents <= 0) {
248
+                                    foreach ($periods as $period) {
249
+                                        $periodKey = "{$record->getKey()}.allocations_by_period.{$period}";
250
+                                        $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
251
+                                    }
252
+
253
+                                    return;
254
+                                }
255
+
256
+                                $baseAmount = floor($totalCents / $numPeriods);
257
+                                $remainder = $totalCents - ($baseAmount * $numPeriods);
258
+
259
+                                foreach ($periods as $index => $period) {
260
+                                    $amount = $baseAmount + ($index === 0 ? $remainder : 0);
261
+                                    $formattedAmount = CurrencyConverter::convertCentsToFormatSimple($amount);
262
+
263
+                                    $periodKey = "{$record->getKey()}.allocations_by_period.{$period}";
264
+                                    $this->batchChanges[$periodKey] = $formattedAmount;
265
+                                }
266
+                            }),
267
+                    ),
114
             ], collect($periods)->map(
268
             ], collect($periods)->map(
115
                 fn ($period) => DeferredTextInputColumn::make("allocations_by_period.{$period}")
269
                 fn ($period) => DeferredTextInputColumn::make("allocations_by_period.{$period}")
116
                     ->label($period)
270
                     ->label($period)
271
+                    ->alignRight()
272
+                    ->mask(RawJs::make('$money($input)'))
273
+                    ->summarize(
274
+                        Summarizer::make()
275
+                            ->label($period)
276
+                            ->using(function (\Illuminate\Database\Query\Builder $query) use ($period) {
277
+                                $budgetItemIds = $query->pluck('id')->toArray();
278
+                                $total = $this->calculatePeriodSum($budgetItemIds, $period);
279
+
280
+                                return CurrencyConverter::convertCentsToFormatSimple($total);
281
+                            })
282
+                    )
117
                     ->getStateUsing(function ($record, DeferredTextInputColumn $column) use ($period) {
283
                     ->getStateUsing(function ($record, DeferredTextInputColumn $column) use ($period) {
118
                         $key = "{$record->getKey()}.{$column->getName()}";
284
                         $key = "{$record->getKey()}.{$column->getName()}";
119
 
285
 

+ 2
- 2
app/Models/Accounting/BudgetAllocation.php View File

2
 
2
 
3
 namespace App\Models\Accounting;
3
 namespace App\Models\Accounting;
4
 
4
 
5
-use App\Casts\DocumentMoneyCast;
5
+use App\Casts\MoneyCast;
6
 use App\Concerns\CompanyOwned;
6
 use App\Concerns\CompanyOwned;
7
 use App\Enums\Accounting\BudgetIntervalType;
7
 use App\Enums\Accounting\BudgetIntervalType;
8
 use Illuminate\Database\Eloquent\Factories\HasFactory;
8
 use Illuminate\Database\Eloquent\Factories\HasFactory;
28
         'start_date' => 'date',
28
         'start_date' => 'date',
29
         'end_date' => 'date',
29
         'end_date' => 'date',
30
         'interval_type' => BudgetIntervalType::class,
30
         'interval_type' => BudgetIntervalType::class,
31
-        'amount' => DocumentMoneyCast::class,
31
+        'amount' => MoneyCast::class,
32
     ];
32
     ];
33
 
33
 
34
     public function budgetItem(): BelongsTo
34
     public function budgetItem(): BelongsTo

+ 12
- 12
composer.lock View File

497
         },
497
         },
498
         {
498
         {
499
             "name": "aws/aws-sdk-php",
499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.342.11",
500
+            "version": "3.342.12",
501
             "source": {
501
             "source": {
502
                 "type": "git",
502
                 "type": "git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "e0afed3d0e2c89362f6c9b6bf8f278b04a4858b6"
504
+                "reference": "29c2c34ddc7ff02ec865355fc235c78acd095fcf"
505
             },
505
             },
506
             "dist": {
506
             "dist": {
507
                 "type": "zip",
507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e0afed3d0e2c89362f6c9b6bf8f278b04a4858b6",
509
-                "reference": "e0afed3d0e2c89362f6c9b6bf8f278b04a4858b6",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/29c2c34ddc7ff02ec865355fc235c78acd095fcf",
509
+                "reference": "29c2c34ddc7ff02ec865355fc235c78acd095fcf",
510
                 "shasum": ""
510
                 "shasum": ""
511
             },
511
             },
512
             "require": {
512
             "require": {
588
             "support": {
588
             "support": {
589
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
589
                 "forum": "https://github.com/aws/aws-sdk-php/discussions",
590
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
590
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
591
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.342.11"
591
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.342.12"
592
             },
592
             },
593
-            "time": "2025-03-21T18:11:10+00:00"
593
+            "time": "2025-03-24T18:06:44+00:00"
594
         },
594
         },
595
         {
595
         {
596
             "name": "aws/aws-sdk-php-laravel",
596
             "name": "aws/aws-sdk-php-laravel",
4519
         },
4519
         },
4520
         {
4520
         {
4521
             "name": "monolog/monolog",
4521
             "name": "monolog/monolog",
4522
-            "version": "3.8.1",
4522
+            "version": "3.9.0",
4523
             "source": {
4523
             "source": {
4524
                 "type": "git",
4524
                 "type": "git",
4525
                 "url": "https://github.com/Seldaek/monolog.git",
4525
                 "url": "https://github.com/Seldaek/monolog.git",
4526
-                "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4"
4526
+                "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
4527
             },
4527
             },
4528
             "dist": {
4528
             "dist": {
4529
                 "type": "zip",
4529
                 "type": "zip",
4530
-                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4",
4531
-                "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4",
4530
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
4531
+                "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
4532
                 "shasum": ""
4532
                 "shasum": ""
4533
             },
4533
             },
4534
             "require": {
4534
             "require": {
4606
             ],
4606
             ],
4607
             "support": {
4607
             "support": {
4608
                 "issues": "https://github.com/Seldaek/monolog/issues",
4608
                 "issues": "https://github.com/Seldaek/monolog/issues",
4609
-                "source": "https://github.com/Seldaek/monolog/tree/3.8.1"
4609
+                "source": "https://github.com/Seldaek/monolog/tree/3.9.0"
4610
             },
4610
             },
4611
             "funding": [
4611
             "funding": [
4612
                 {
4612
                 {
4618
                     "type": "tidelift"
4618
                     "type": "tidelift"
4619
                 }
4619
                 }
4620
             ],
4620
             ],
4621
-            "time": "2024-12-05T17:15:07+00:00"
4621
+            "time": "2025-03-24T10:02:05+00:00"
4622
         },
4622
         },
4623
         {
4623
         {
4624
             "name": "mtdowling/jmespath.php",
4624
             "name": "mtdowling/jmespath.php",

+ 1
- 5
resources/css/filament/company/theme.css View File

28
 
28
 
29
 /* End Alignment sortable column enhancement */
29
 /* End Alignment sortable column enhancement */
30
 .fi-ta-header-cell:has(button.justify-end > svg.fi-ta-header-cell-sort-icon) {
30
 .fi-ta-header-cell:has(button.justify-end > svg.fi-ta-header-cell-sort-icon) {
31
-    padding-right: 0.25rem;
32
-}
33
-
34
-.fi-ta-cell:has(.fi-ta-col-wrp > .justify-end) {
35
-    padding-right: 1rem;
31
+    padding-right: 0;
36
 }
32
 }
37
 
33
 
38
 :not(.dark) .fi-body {
34
 :not(.dark) .fi-body {

+ 1
- 1
resources/views/filament/tables/columns/deferred-text-input-column.blade.php View File

66
         $attributes
66
         $attributes
67
             ->merge($getExtraAttributes(), escape: false)
67
             ->merge($getExtraAttributes(), escape: false)
68
             ->class([
68
             ->class([
69
-                'fi-ta-text-input w-full min-w-48',
69
+                'fi-ta-text-input w-full min-w-36',
70
                 'px-3 py-4' => ! $isInline(),
70
                 'px-3 py-4' => ! $isInline(),
71
             ])
71
             ])
72
     }}
72
     }}

Loading…
Cancel
Save