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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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. ->action(
  158. Action::make('disperse')
  159. ->label('Disperse')
  160. ->action(function (BudgetItem $record) use ($periods) {
  161. if (empty($periods)) {
  162. return;
  163. }
  164. $totalKey = "{$record->getKey()}.total";
  165. $totalAmount = $this->batchChanges[$totalKey] ?? null;
  166. if (isset($totalAmount)) {
  167. $totalCents = CurrencyConverter::convertToCents($totalAmount);
  168. } else {
  169. $totalCents = $record->allocations->sum(function (BudgetAllocation $budgetAllocation) {
  170. return $budgetAllocation->getRawOriginal('amount');
  171. });
  172. }
  173. $numPeriods = count($periods);
  174. if ($numPeriods === 0) {
  175. return;
  176. }
  177. if ($totalCents <= 0) {
  178. foreach ($periods as $period) {
  179. $periodKey = "{$record->getKey()}.amount_" . str_replace(['-', '.', ' '], '_', $period);
  180. $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
  181. }
  182. return;
  183. }
  184. $baseAmount = floor($totalCents / $numPeriods);
  185. $remainder = $totalCents - ($baseAmount * $numPeriods);
  186. foreach ($periods as $index => $period) {
  187. $amount = $baseAmount + ($index === 0 ? $remainder : 0);
  188. $formattedAmount = CurrencyConverter::convertCentsToFormatSimple($amount);
  189. $periodKey = "{$record->getKey()}." . 'amount_' . str_replace(['-', '.', ' '], '_', $period);
  190. $this->batchChanges[$periodKey] = $formattedAmount;
  191. }
  192. }),
  193. ),
  194. ...array_map(function (string $period) {
  195. $alias = 'amount_' . str_replace(['-', '.', ' '], '_', $period); // Also replace space
  196. return DeferredTextInputColumn::make($alias)
  197. ->label($period)
  198. ->alignRight()
  199. ->batchMode()
  200. ->mask(RawJs::make('$money($input)'))
  201. ->getStateUsing(function ($record) use ($alias) {
  202. $key = "{$record->getKey()}.{$alias}";
  203. return $this->batchChanges[$key] ?? CurrencyConverter::convertCentsToFormatSimple($record->{$alias} ?? 0);
  204. })
  205. ->summarize(
  206. Summarizer::make()
  207. ->using(function (\Illuminate\Database\Query\Builder $query) use ($period) {
  208. $budgetItemIds = $query->pluck('id')->toArray();
  209. $total = $this->calculatePeriodSum($budgetItemIds, $period);
  210. return CurrencyConverter::convertCentsToFormatSimple($total);
  211. })
  212. );
  213. }, $periods),
  214. ])
  215. ->bulkActions([
  216. BulkAction::make('clearAllocations')
  217. ->label('Clear Allocations')
  218. ->icon('heroicon-o-trash')
  219. ->color('danger')
  220. ->requiresConfirmation()
  221. ->deselectRecordsAfterCompletion()
  222. ->action(function (Collection $records) use ($periods) {
  223. foreach ($records as $record) {
  224. foreach ($periods as $period) {
  225. $periodKey = "{$record->getKey()}.amount_" . str_replace(['-', '.', ' '], '_', $period);
  226. $this->batchChanges[$periodKey] = CurrencyConverter::convertCentsToFormatSimple(0);
  227. }
  228. }
  229. }),
  230. ]);
  231. }
  232. }