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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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. ->disabled(static fn (Forms\Get $get) => blank($get('start_date')))
  43. ->minDate(fn (Forms\Get $get) => match (BudgetIntervalType::parse($get('interval_type'))) {
  44. BudgetIntervalType::Week => Carbon::parse($get('start_date'))->addWeek(),
  45. BudgetIntervalType::Month => Carbon::parse($get('start_date'))->addMonth(),
  46. BudgetIntervalType::Quarter => Carbon::parse($get('start_date'))->addQuarter(),
  47. BudgetIntervalType::Year => Carbon::parse($get('start_date'))->addYear(),
  48. default => Carbon::parse($get('start_date'))->addDay(),
  49. })
  50. ->maxDate(fn (Forms\Get $get) => Carbon::parse($get('start_date'))->endOfYear()),
  51. Forms\Components\Textarea::make('notes')
  52. ->columnSpanFull(),
  53. ]),
  54. Forms\Components\Section::make('Budget Items')
  55. ->headerActions([
  56. Forms\Components\Actions\Action::make('addAllAccounts')
  57. ->label('Add All Accounts')
  58. ->icon('heroicon-m-plus')
  59. ->outlined()
  60. ->color('primary')
  61. ->action(static fn (Forms\Set $set, Forms\Get $get) => self::addAllAccounts($set, $get))
  62. ->hidden(static fn (Forms\Get $get) => filled($get('budgetItems'))),
  63. ])
  64. ->schema([
  65. Forms\Components\Repeater::make('budgetItems')
  66. ->columns(4)
  67. ->hiddenLabel()
  68. ->schema([
  69. Forms\Components\Select::make('account_id')
  70. ->label('Account')
  71. ->options(Account::query()->pluck('name', 'id'))
  72. ->searchable()
  73. ->disableOptionsWhenSelectedInSiblingRepeaterItems()
  74. ->columnSpan(1)
  75. ->required(),
  76. Forms\Components\TextInput::make('total_amount')
  77. ->label('Total Amount')
  78. ->numeric()
  79. ->columnSpan(1)
  80. ->suffixAction(
  81. Forms\Components\Actions\Action::make('disperse')
  82. ->label('Disperse')
  83. ->icon('heroicon-m-bars-arrow-down')
  84. ->color('primary')
  85. ->action(static fn (Forms\Set $set, Forms\Get $get, $state) => self::disperseTotalAmount($set, $get, $state))
  86. ),
  87. CustomSection::make('Budget Allocations')
  88. ->contained(false)
  89. ->columns(4)
  90. ->schema(static fn (Forms\Get $get) => self::getAllocationFields($get('../../start_date'), $get('../../end_date'), $get('../../interval_type'))),
  91. ])
  92. ->defaultItems(0)
  93. ->addActionLabel('Add Budget Item'),
  94. ]),
  95. ]);
  96. }
  97. public static function table(Table $table): Table
  98. {
  99. return $table
  100. ->columns([
  101. Tables\Columns\TextColumn::make('name')
  102. ->sortable()
  103. ->searchable(),
  104. Tables\Columns\TextColumn::make('interval_type')
  105. ->label('Interval')
  106. ->sortable()
  107. ->badge(),
  108. Tables\Columns\TextColumn::make('start_date')
  109. ->label('Start Date')
  110. ->date()
  111. ->sortable(),
  112. Tables\Columns\TextColumn::make('end_date')
  113. ->label('End Date')
  114. ->date()
  115. ->sortable(),
  116. Tables\Columns\TextColumn::make('total_budgeted_amount')
  117. ->label('Total Budgeted')
  118. ->money()
  119. ->sortable()
  120. ->alignEnd()
  121. ->getStateUsing(fn (Budget $record) => $record->budgetItems->sum(fn ($item) => $item->allocations->sum('amount'))),
  122. ])
  123. ->filters([
  124. //
  125. ])
  126. ->actions([
  127. Tables\Actions\ActionGroup::make([
  128. Tables\Actions\ViewAction::make(),
  129. Tables\Actions\EditAction::make(),
  130. ]),
  131. ])
  132. ->bulkActions([
  133. Tables\Actions\BulkActionGroup::make([
  134. Tables\Actions\DeleteBulkAction::make(),
  135. ]),
  136. ]);
  137. }
  138. private static function addAllAccounts(Forms\Set $set, Forms\Get $get): void
  139. {
  140. $accounts = Account::query()
  141. ->pluck('id');
  142. $budgetItems = $accounts->map(static fn ($accountId) => [
  143. 'account_id' => $accountId,
  144. 'total_amount' => 0, // Default to 0 until the user inputs amounts
  145. 'amounts' => self::generateDefaultAllocations($get('start_date'), $get('end_date'), $get('interval_type')),
  146. ])->toArray();
  147. $set('budgetItems', $budgetItems);
  148. }
  149. private static function generateDefaultAllocations(?string $startDate, ?string $endDate, ?string $intervalType): array
  150. {
  151. if (! $startDate || ! $endDate || ! $intervalType) {
  152. return [];
  153. }
  154. $labels = self::generateFormattedLabels($startDate, $endDate, $intervalType);
  155. return collect($labels)->mapWithKeys(static fn ($label) => [$label => 0])->toArray();
  156. }
  157. private static function disperseTotalAmount(Forms\Set $set, Forms\Get $get, float $totalAmount): void
  158. {
  159. $startDate = $get('../../start_date');
  160. $endDate = $get('../../end_date');
  161. $intervalType = $get('../../interval_type');
  162. if (! $startDate || ! $endDate || ! $intervalType || $totalAmount <= 0) {
  163. return;
  164. }
  165. $labels = self::generateFormattedLabels($startDate, $endDate, $intervalType);
  166. $numPeriods = count($labels);
  167. if ($numPeriods === 0) {
  168. return;
  169. }
  170. $baseAmount = floor($totalAmount / $numPeriods);
  171. $remainder = $totalAmount - ($baseAmount * $numPeriods);
  172. foreach ($labels as $index => $label) {
  173. $amount = $baseAmount + ($index === 0 ? $remainder : 0);
  174. $set("amounts.{$label}", $amount);
  175. }
  176. }
  177. private static function generateFormattedLabels(string $startDate, string $endDate, string $intervalType): array
  178. {
  179. $start = Carbon::parse($startDate);
  180. $end = Carbon::parse($endDate);
  181. $intervalTypeEnum = BudgetIntervalType::parse($intervalType);
  182. $labels = [];
  183. while ($start->lte($end)) {
  184. $labels[] = match ($intervalTypeEnum) {
  185. BudgetIntervalType::Week => 'W' . $start->weekOfYear . ' ' . $start->year, // Example: W10 2024
  186. BudgetIntervalType::Month => $start->format('M'), // Example: Jan, Feb, Mar
  187. BudgetIntervalType::Quarter => 'Q' . $start->quarter, // Example: Q1, Q2, Q3
  188. BudgetIntervalType::Year => (string) $start->year, // Example: 2024, 2025
  189. default => '',
  190. };
  191. match ($intervalTypeEnum) {
  192. BudgetIntervalType::Week => $start->addWeek(),
  193. BudgetIntervalType::Month => $start->addMonth(),
  194. BudgetIntervalType::Quarter => $start->addQuarter(),
  195. BudgetIntervalType::Year => $start->addYear(),
  196. default => null,
  197. };
  198. }
  199. return $labels;
  200. }
  201. private static function getAllocationFields(?string $startDate, ?string $endDate, ?string $intervalType): array
  202. {
  203. if (! $startDate || ! $endDate || ! $intervalType) {
  204. return [];
  205. }
  206. $start = Carbon::parse($startDate);
  207. $end = Carbon::parse($endDate);
  208. $intervalTypeEnum = BudgetIntervalType::parse($intervalType);
  209. $fields = [];
  210. while ($start->lte($end)) {
  211. $label = match ($intervalTypeEnum) {
  212. BudgetIntervalType::Week => 'W' . $start->weekOfYear . ' ' . $start->year, // Example: W10 2024
  213. BudgetIntervalType::Month => $start->format('M'), // Example: Jan, Feb, Mar
  214. BudgetIntervalType::Quarter => 'Q' . $start->quarter, // Example: Q1, Q2, Q3
  215. BudgetIntervalType::Year => (string) $start->year, // Example: 2024, 2025
  216. default => '',
  217. };
  218. $fields[] = Forms\Components\TextInput::make("amounts.{$label}")
  219. ->label($label)
  220. ->numeric()
  221. ->required();
  222. match ($intervalTypeEnum) {
  223. BudgetIntervalType::Week => $start->addWeek(),
  224. BudgetIntervalType::Month => $start->addMonth(),
  225. BudgetIntervalType::Quarter => $start->addQuarter(),
  226. BudgetIntervalType::Year => $start->addYear(),
  227. default => null,
  228. };
  229. }
  230. return $fields;
  231. }
  232. public static function getRelations(): array
  233. {
  234. return [
  235. //
  236. ];
  237. }
  238. public static function getPages(): array
  239. {
  240. return [
  241. 'index' => Pages\ListBudgets::route('/'),
  242. 'create' => Pages\CreateBudget::route('/create'),
  243. 'view' => Pages\ViewBudget::route('/{record}'),
  244. 'edit' => Pages\EditBudget::route('/{record}/edit'),
  245. ];
  246. }
  247. }