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.

BudgetResource.php 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <?php
  2. namespace App\Filament\Company\Resources\Accounting;
  3. use App\Enums\Accounting\BudgetIntervalType;
  4. use App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
  5. use App\Filament\Forms\Components\CustomSection;
  6. use App\Models\Accounting\Account;
  7. use App\Models\Accounting\Budget;
  8. use Filament\Forms;
  9. use Filament\Forms\Form;
  10. use Filament\Resources\Resource;
  11. use Filament\Tables;
  12. use Filament\Tables\Table;
  13. use Illuminate\Support\Carbon;
  14. class BudgetResource extends Resource
  15. {
  16. protected static ?string $model = Budget::class;
  17. protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
  18. public static function form(Form $form): Form
  19. {
  20. return $form
  21. ->schema([
  22. Forms\Components\Section::make('Budget Details')
  23. ->columns()
  24. ->schema([
  25. Forms\Components\TextInput::make('name')
  26. ->required()
  27. ->maxLength(255),
  28. Forms\Components\Select::make('interval_type')
  29. ->label('Budget Interval')
  30. ->options(BudgetIntervalType::class)
  31. ->default(BudgetIntervalType::Month->value)
  32. ->required()
  33. ->live(),
  34. Forms\Components\DatePicker::make('start_date')
  35. ->required()
  36. ->default(now()->startOfYear())
  37. ->live(),
  38. Forms\Components\DatePicker::make('end_date')
  39. ->required()
  40. ->default(now()->endOfYear())
  41. ->live(),
  42. Forms\Components\Textarea::make('notes')->columnSpanFull(),
  43. ]),
  44. Forms\Components\Section::make('Budget Items')
  45. ->schema([
  46. Forms\Components\Repeater::make('budgetItems')
  47. ->columns(4)
  48. ->hiddenLabel()
  49. ->schema([
  50. Forms\Components\Select::make('account_id')
  51. ->label('Account')
  52. ->options(Account::query()->pluck('name', 'id'))
  53. ->searchable()
  54. ->columnSpan(1)
  55. ->required(),
  56. Forms\Components\TextInput::make('total_amount')
  57. ->label('Total Amount')
  58. ->numeric()
  59. ->required()
  60. ->columnSpan(1)
  61. ->suffixAction(
  62. Forms\Components\Actions\Action::make('disperse')
  63. ->label('Disperse')
  64. ->icon('heroicon-m-bars-arrow-down')
  65. ->color('primary')
  66. ->action(static fn (Forms\Set $set, Forms\Get $get, $state) => self::disperseTotalAmount($set, $get, $state))
  67. ),
  68. CustomSection::make('Budget Allocations')
  69. ->contained(false)
  70. ->columns(4)
  71. ->schema(static fn (Forms\Get $get) => self::getAllocationFields($get('../../start_date'), $get('../../end_date'), $get('../../interval_type'))),
  72. ])
  73. ->defaultItems(1)
  74. ->addActionLabel('Add Budget Item'),
  75. ]),
  76. ]);
  77. }
  78. public static function table(Table $table): Table
  79. {
  80. return $table
  81. ->columns([
  82. Tables\Columns\TextColumn::make('name')
  83. ->sortable()
  84. ->searchable(),
  85. Tables\Columns\TextColumn::make('interval_type')
  86. ->label('Interval')
  87. ->sortable()
  88. ->badge(),
  89. Tables\Columns\TextColumn::make('start_date')
  90. ->label('Start Date')
  91. ->date()
  92. ->sortable(),
  93. Tables\Columns\TextColumn::make('end_date')
  94. ->label('End Date')
  95. ->date()
  96. ->sortable(),
  97. Tables\Columns\TextColumn::make('total_budgeted_amount')
  98. ->label('Total Budgeted')
  99. ->money()
  100. ->sortable()
  101. ->alignEnd()
  102. ->getStateUsing(fn (Budget $record) => $record->budgetItems->sum(fn ($item) => $item->allocations->sum('amount'))),
  103. ])
  104. ->filters([
  105. //
  106. ])
  107. ->actions([
  108. Tables\Actions\ActionGroup::make([
  109. Tables\Actions\ViewAction::make(),
  110. Tables\Actions\EditAction::make(),
  111. ]),
  112. ])
  113. ->bulkActions([
  114. Tables\Actions\BulkActionGroup::make([
  115. Tables\Actions\DeleteBulkAction::make(),
  116. ]),
  117. ]);
  118. }
  119. /**
  120. * Disperses the total amount across the budget items based on the selected interval.
  121. */
  122. private static function disperseTotalAmount(Forms\Set $set, Forms\Get $get, float $totalAmount): void
  123. {
  124. $startDate = $get('../../start_date');
  125. $endDate = $get('../../end_date');
  126. $intervalType = $get('../../interval_type');
  127. if (! $startDate || ! $endDate || ! $intervalType || $totalAmount <= 0) {
  128. return;
  129. }
  130. // Generate labels based on interval type (must match `getAllocationFields()`)
  131. $labels = self::generateFormattedLabels($startDate, $endDate, $intervalType);
  132. $numPeriods = count($labels);
  133. if ($numPeriods === 0) {
  134. return;
  135. }
  136. // Calculate base allocation and handle rounding
  137. $baseAmount = floor($totalAmount / $numPeriods);
  138. $remainder = $totalAmount - ($baseAmount * $numPeriods);
  139. // Assign amounts to the correct fields using labels
  140. foreach ($labels as $index => $label) {
  141. $amount = $baseAmount + ($index === 0 ? $remainder : 0);
  142. $set("amounts.{$label}", $amount); // Now correctly assigns to the right field
  143. }
  144. }
  145. /**
  146. * Generates formatted labels for the budget allocation fields based on the selected interval type.
  147. */
  148. private static function generateFormattedLabels(string $startDate, string $endDate, string $intervalType): array
  149. {
  150. $start = Carbon::parse($startDate);
  151. $end = Carbon::parse($endDate);
  152. $intervalTypeEnum = BudgetIntervalType::parse($intervalType);
  153. $labels = [];
  154. while ($start->lte($end)) {
  155. $labels[] = match ($intervalTypeEnum) {
  156. BudgetIntervalType::Week => 'W' . $start->weekOfYear . ' ' . $start->year, // Example: W10 2024
  157. BudgetIntervalType::Month => $start->format('M'), // Example: Jan, Feb, Mar
  158. BudgetIntervalType::Quarter => 'Q' . $start->quarter, // Example: Q1, Q2, Q3
  159. BudgetIntervalType::Year => (string) $start->year, // Example: 2024, 2025
  160. default => '',
  161. };
  162. match ($intervalTypeEnum) {
  163. BudgetIntervalType::Week => $start->addWeek(),
  164. BudgetIntervalType::Month => $start->addMonth(),
  165. BudgetIntervalType::Quarter => $start->addQuarter(),
  166. BudgetIntervalType::Year => $start->addYear(),
  167. default => null,
  168. };
  169. }
  170. return $labels;
  171. }
  172. private static function getAllocationFields(?string $startDate, ?string $endDate, ?string $intervalType): array
  173. {
  174. if (! $startDate || ! $endDate || ! $intervalType) {
  175. return [];
  176. }
  177. $start = Carbon::parse($startDate);
  178. $end = Carbon::parse($endDate);
  179. $intervalTypeEnum = BudgetIntervalType::parse($intervalType);
  180. $fields = [];
  181. while ($start->lte($end)) {
  182. $label = match ($intervalTypeEnum) {
  183. BudgetIntervalType::Week => 'W' . $start->weekOfYear . ' ' . $start->year, // Example: W10 2024
  184. BudgetIntervalType::Month => $start->format('M'), // Example: Jan, Feb, Mar
  185. BudgetIntervalType::Quarter => 'Q' . $start->quarter, // Example: Q1, Q2, Q3
  186. BudgetIntervalType::Year => (string) $start->year, // Example: 2024, 2025
  187. default => '',
  188. };
  189. $fields[] = Forms\Components\TextInput::make("amounts.{$label}")
  190. ->label($label)
  191. ->numeric()
  192. ->required();
  193. // Move to the next period
  194. match ($intervalTypeEnum) {
  195. BudgetIntervalType::Week => $start->addWeek(),
  196. BudgetIntervalType::Month => $start->addMonth(),
  197. BudgetIntervalType::Quarter => $start->addQuarter(),
  198. BudgetIntervalType::Year => $start->addYear(),
  199. default => null,
  200. };
  201. }
  202. return $fields;
  203. }
  204. public static function getRelations(): array
  205. {
  206. return [
  207. //
  208. ];
  209. }
  210. public static function getPages(): array
  211. {
  212. return [
  213. 'index' => Pages\ListBudgets::route('/'),
  214. 'create' => Pages\CreateBudget::route('/create'),
  215. 'view' => Pages\ViewBudget::route('/{record}'),
  216. 'edit' => Pages\EditBudget::route('/{record}/edit'),
  217. ];
  218. }
  219. }