You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BudgetItemsRelationManager.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. namespace App\Filament\Company\Resources\Accounting\BudgetResource\RelationManagers;
  3. use App\Filament\Tables\Columns\DeferredTextInputColumn;
  4. use App\Models\Accounting\Budget;
  5. use App\Models\Accounting\BudgetAllocation;
  6. use App\Models\Accounting\BudgetItem;
  7. use App\Utilities\Currency\CurrencyConverter;
  8. use Filament\Notifications\Notification;
  9. use Filament\Resources\RelationManagers\RelationManager;
  10. use Filament\Support\RawJs;
  11. use Filament\Tables\Actions\Action;
  12. use Filament\Tables\Actions\BulkAction;
  13. use Filament\Tables\Columns\IconColumn;
  14. use Filament\Tables\Columns\Summarizers\Summarizer;
  15. use Filament\Tables\Columns\TextColumn;
  16. use Filament\Tables\Grouping\Group;
  17. use Filament\Tables\Table;
  18. use Illuminate\Database\Eloquent\Builder;
  19. use Illuminate\Database\Eloquent\Collection;
  20. class BudgetItemsRelationManager extends RelationManager
  21. {
  22. protected static string $relationship = 'budgetItems';
  23. protected static bool $isLazy = false;
  24. public array $batchChanges = [];
  25. public function handleBatchColumnChanged($data): void
  26. {
  27. $key = "{$data['recordKey']}.{$data['name']}";
  28. $this->batchChanges[$key] = $data['value'];
  29. }
  30. public function saveBatchChanges(): void
  31. {
  32. foreach ($this->batchChanges as $key => $value) {
  33. [$recordKey, $column] = explode('.', $key, 2);
  34. preg_match('/amount_(.+)/', $column, $matches);
  35. $period = str_replace('_', ' ', $matches[1] ?? '');
  36. if (! $period) {
  37. continue;
  38. }
  39. $record = BudgetItem::find($recordKey);
  40. if (! $record) {
  41. continue;
  42. }
  43. $allocation = $record->allocations->firstWhere('period', $period);
  44. if ($allocation) {
  45. $allocation->update(['amount' => $value]);
  46. } else {
  47. $record->allocations()->create([
  48. 'period' => $period,
  49. 'amount' => $value,
  50. ]);
  51. }
  52. }
  53. $this->batchChanges = [];
  54. Notification::make()
  55. ->title('Budget allocations updated')
  56. ->success()
  57. ->send();
  58. }
  59. protected function calculateTotalSum(array $budgetItemIds): int
  60. {
  61. $periods = BudgetAllocation::whereIn('budget_item_id', $budgetItemIds)
  62. ->pluck('period')
  63. ->unique()
  64. ->values()
  65. ->toArray();
  66. $total = 0;
  67. foreach ($periods as $period) {
  68. $total += $this->calculatePeriodSum($budgetItemIds, $period);
  69. }
  70. return $total;
  71. }
  72. protected function calculatePeriodSum(array $budgetItemIds, string $period): int
  73. {
  74. $dbTotal = BudgetAllocation::whereIn('budget_item_id', $budgetItemIds)
  75. ->where('period', $period)
  76. ->sum('amount');
  77. $batchTotal = 0;
  78. foreach ($budgetItemIds as $itemId) {
  79. $key = "{$itemId}.amount_" . str_replace(['-', '.', ' '], '_', $period);
  80. if (isset($this->batchChanges[$key])) {
  81. $batchValue = CurrencyConverter::convertToCents($this->batchChanges[$key]);
  82. $existingAmount = BudgetAllocation::where('budget_item_id', $itemId)
  83. ->where('period', $period)
  84. ->first()
  85. ?->getRawOriginal('amount') ?? 0;
  86. $batchTotal += ($batchValue - $existingAmount);
  87. }
  88. }
  89. return $dbTotal + $batchTotal;
  90. }
  91. public function table(Table $table): Table
  92. {
  93. /** @var Budget $budget */
  94. $budget = $this->getOwnerRecord();
  95. $periods = $budget->getPeriods();
  96. return $table
  97. ->recordTitleAttribute('account_id')
  98. ->paginated(false)
  99. ->heading(null)
  100. ->modifyQueryUsing(function (Builder $query) use ($periods) {
  101. $query->select('budget_items.*')
  102. ->leftJoin('budget_allocations', 'budget_allocations.budget_item_id', '=', 'budget_items.id');
  103. foreach ($periods as $period) {
  104. $alias = 'amount_' . str_replace(['-', '.', ' '], '_', $period);
  105. $query->selectRaw(
  106. "SUM(CASE WHEN budget_allocations.period = ? THEN budget_allocations.amount ELSE 0 END) as {$alias}",
  107. [$period]
  108. );
  109. }
  110. return $query->groupBy('budget_items.id');
  111. })
  112. ->groups([
  113. Group::make('account.category')
  114. ->titlePrefixedWithLabel(false)
  115. ->collapsible(),
  116. ])
  117. ->recordClasses(['budget-items-relation-manager'])
  118. ->defaultGroup('account.category')
  119. ->headerActions([
  120. Action::make('saveBatchChanges')
  121. ->label('Save all changes')
  122. ->action('saveBatchChanges')
  123. ->color('primary'),
  124. ])
  125. ->columns([
  126. TextColumn::make('account.name')
  127. ->label('Account')
  128. ->limit(30)
  129. ->searchable(),
  130. DeferredTextInputColumn::make('total')
  131. ->label('Total')
  132. ->alignRight()
  133. ->mask(RawJs::make('$money($input)'))
  134. ->getStateUsing(function (BudgetItem $record, DeferredTextInputColumn $column) {
  135. if (isset($this->batchChanges["{$record->getKey()}.{$column->getName()}"])) {
  136. return $this->batchChanges["{$record->getKey()}.{$column->getName()}"];
  137. }
  138. $total = $record->allocations->sum(
  139. fn (BudgetAllocation $allocation) => $allocation->getRawOriginal('amount')
  140. );
  141. return CurrencyConverter::convertCentsToFormatSimple($total);
  142. })
  143. ->batchMode()
  144. ->summarize(
  145. Summarizer::make()
  146. ->using(function (\Illuminate\Database\Query\Builder $query) {
  147. $budgetItemIds = $query->pluck('id')->toArray();
  148. $total = $this->calculateTotalSum($budgetItemIds);
  149. return CurrencyConverter::convertCentsToFormatSimple($total);
  150. })
  151. ),
  152. IconColumn::make('disperseAction')
  153. ->icon('heroicon-m-chevron-double-right')
  154. ->color('primary')
  155. ->label('')
  156. ->default('')
  157. ->tooltip('Disperse total across periods')
  158. ->action(
  159. Action::make('disperse')
  160. ->label('Disperse')
  161. ->action(function (BudgetItem $record) use ($periods) {
  162. if (empty($periods)) {
  163. return;
  164. }
  165. $totalKey = "{$record->getKey()}.total";
  166. $totalAmount = $this->batchChanges[$totalKey] ?? null;
  167. if (isset($totalAmount)) {
  168. $totalCents = CurrencyConverter::convertToCents($totalAmount);
  169. } else {
  170. $totalCents = $record->allocations->sum(function (BudgetAllocation $budgetAllocation) {
  171. return $budgetAllocation->getRawOriginal('amount');
  172. });
  173. }
  174. $numPeriods = count($periods);
  175. if ($numPeriods === 0) {
  176. return;
  177. }
  178. if ($totalCents <= 0) {
  179. foreach ($periods as $period) {
  180. $periodKey = "{$record->getKey()}.amount_" . str_replace(['-', '.', ' '], '_', $period);
  181. $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
  182. }
  183. return;
  184. }
  185. $baseAmount = floor($totalCents / $numPeriods);
  186. $remainder = $totalCents - ($baseAmount * $numPeriods);
  187. foreach ($periods as $index => $period) {
  188. $amount = $baseAmount + ($index === 0 ? $remainder : 0);
  189. $formattedAmount = CurrencyConverter::convertCentsToFormatSimple($amount);
  190. $periodKey = "{$record->getKey()}." . 'amount_' . str_replace(['-', '.', ' '], '_', $period);
  191. $this->batchChanges[$periodKey] = $formattedAmount;
  192. }
  193. }),
  194. ),
  195. ...array_map(function (string $period) {
  196. $alias = 'amount_' . str_replace(['-', '.', ' '], '_', $period); // Also replace space
  197. return DeferredTextInputColumn::make($alias)
  198. ->label($period)
  199. ->alignRight()
  200. ->batchMode()
  201. ->mask(RawJs::make('$money($input)'))
  202. ->getStateUsing(function ($record) use ($alias) {
  203. $key = "{$record->getKey()}.{$alias}";
  204. return $this->batchChanges[$key] ?? CurrencyConverter::convertCentsToFormatSimple($record->{$alias} ?? 0);
  205. })
  206. ->summarize(
  207. Summarizer::make()
  208. ->using(function (\Illuminate\Database\Query\Builder $query) use ($period) {
  209. $budgetItemIds = $query->pluck('id')->toArray();
  210. $total = $this->calculatePeriodSum($budgetItemIds, $period);
  211. return CurrencyConverter::convertCentsToFormatSimple($total);
  212. })
  213. );
  214. }, $periods),
  215. ])
  216. ->bulkActions([
  217. BulkAction::make('clearAllocations')
  218. ->label('Clear Allocations')
  219. ->icon('heroicon-o-trash')
  220. ->color('danger')
  221. ->requiresConfirmation()
  222. ->deselectRecordsAfterCompletion()
  223. ->action(function (Collection $records) use ($periods) {
  224. foreach ($records as $record) {
  225. foreach ($periods as $period) {
  226. $periodKey = "{$record->getKey()}.amount_" . str_replace(['-', '.', ' '], '_', $period);
  227. $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
  228. }
  229. }
  230. }),
  231. ]);
  232. }
  233. }