選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

CreateBudget.php 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. <?php
  2. namespace App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
  3. use App\Enums\Accounting\BudgetIntervalType;
  4. use App\Enums\Accounting\BudgetSourceType;
  5. use App\Facades\Accounting;
  6. use App\Filament\Company\Resources\Accounting\BudgetResource;
  7. use App\Filament\Forms\Components\CustomSection;
  8. use App\Models\Accounting\Account;
  9. use App\Models\Accounting\Budget;
  10. use App\Models\Accounting\BudgetItem;
  11. use App\Utilities\Currency\CurrencyConverter;
  12. use Filament\Forms;
  13. use Filament\Forms\Components\Actions\Action;
  14. use Filament\Forms\Components\Wizard\Step;
  15. use Filament\Resources\Pages\CreateRecord;
  16. use Illuminate\Database\Eloquent\Builder;
  17. use Illuminate\Database\Eloquent\Model;
  18. use Illuminate\Support\Carbon;
  19. use Illuminate\Support\Collection;
  20. class CreateBudget extends CreateRecord
  21. {
  22. use CreateRecord\Concerns\HasWizard;
  23. protected static string $resource = BudgetResource::class;
  24. // Add computed properties
  25. public function getBudgetableAccounts(): Collection
  26. {
  27. return $this->getAccountsCache('budgetable', function () {
  28. return Account::query()->budgetable()->get();
  29. });
  30. }
  31. public function getAccountsWithActuals(): Collection
  32. {
  33. $fiscalYear = $this->data['source_fiscal_year'] ?? null;
  34. if (blank($fiscalYear)) {
  35. return collect();
  36. }
  37. return $this->getAccountsCache("actuals_{$fiscalYear}", function () use ($fiscalYear) {
  38. return Account::query()
  39. ->budgetable()
  40. ->whereHas('journalEntries.transaction', function (Builder $query) use ($fiscalYear) {
  41. $query->whereYear('posted_at', $fiscalYear);
  42. })
  43. ->get();
  44. });
  45. }
  46. public function getAccountsWithoutActuals(): Collection
  47. {
  48. $fiscalYear = $this->data['source_fiscal_year'] ?? null;
  49. if (blank($fiscalYear)) {
  50. return collect();
  51. }
  52. $budgetableAccounts = $this->getBudgetableAccounts();
  53. $accountsWithActuals = $this->getAccountsWithActuals();
  54. return $budgetableAccounts->whereNotIn('id', $accountsWithActuals->pluck('id'));
  55. }
  56. public function getAccountBalances(): Collection
  57. {
  58. $fiscalYear = $this->data['source_fiscal_year'] ?? null;
  59. if (blank($fiscalYear)) {
  60. return collect();
  61. }
  62. return $this->getAccountsCache("balances_{$fiscalYear}", function () use ($fiscalYear) {
  63. $fiscalYearStart = Carbon::create($fiscalYear, 1, 1)->startOfYear();
  64. $fiscalYearEnd = $fiscalYearStart->copy()->endOfYear();
  65. return Accounting::getAccountBalances(
  66. $fiscalYearStart->toDateString(),
  67. $fiscalYearEnd->toDateString(),
  68. $this->getBudgetableAccounts()->pluck('id')->toArray()
  69. )->get();
  70. });
  71. }
  72. // Cache helper to avoid duplicate queries
  73. private array $accountsCache = [];
  74. private function getAccountsCache(string $key, callable $callback): Collection
  75. {
  76. if (! isset($this->accountsCache[$key])) {
  77. $this->accountsCache[$key] = $callback();
  78. }
  79. return $this->accountsCache[$key];
  80. }
  81. public function getSteps(): array
  82. {
  83. return [
  84. Step::make('General Information')
  85. ->icon('heroicon-o-document-text')
  86. ->columns(2)
  87. ->schema([
  88. Forms\Components\TextInput::make('name')
  89. ->required()
  90. ->maxLength(255),
  91. Forms\Components\Select::make('interval_type')
  92. ->label('Budget Interval')
  93. ->options(BudgetIntervalType::class)
  94. ->default(BudgetIntervalType::Month->value)
  95. ->required()
  96. ->live(),
  97. Forms\Components\DatePicker::make('start_date')
  98. ->required()
  99. ->default(now()->startOfYear())
  100. ->live(),
  101. Forms\Components\DatePicker::make('end_date')
  102. ->required()
  103. ->default(now()->endOfYear())
  104. ->live()
  105. ->disabled(static fn (Forms\Get $get) => blank($get('start_date')))
  106. ->minDate(fn (Forms\Get $get) => match (BudgetIntervalType::parse($get('interval_type'))) {
  107. BudgetIntervalType::Month => Carbon::parse($get('start_date'))->addMonth(),
  108. BudgetIntervalType::Quarter => Carbon::parse($get('start_date'))->addQuarter(),
  109. BudgetIntervalType::Year => Carbon::parse($get('start_date'))->addYear(),
  110. default => Carbon::parse($get('start_date'))->addDay(),
  111. })
  112. ->maxDate(fn (Forms\Get $get) => Carbon::parse($get('start_date'))->endOfYear()),
  113. ]),
  114. Step::make('Budget Setup & Settings')
  115. ->icon('heroicon-o-cog-6-tooth')
  116. ->schema([
  117. // Prefill configuration
  118. Forms\Components\Toggle::make('prefill_data')
  119. ->label('Prefill Data')
  120. ->helperText('Enable this option to prefill the budget with historical data')
  121. ->default(false)
  122. ->live(),
  123. Forms\Components\Grid::make(1)
  124. ->schema([
  125. Forms\Components\Select::make('source_type')
  126. ->label('Prefill Method')
  127. ->options(BudgetSourceType::class)
  128. ->live()
  129. ->required(),
  130. // If user selects to copy a previous budget
  131. Forms\Components\Select::make('source_budget_id')
  132. ->label('Source Budget')
  133. ->options(fn () => Budget::query()
  134. ->orderByDesc('end_date')
  135. ->pluck('name', 'id'))
  136. ->searchable()
  137. ->required()
  138. ->visible(fn (Forms\Get $get) => BudgetSourceType::parse($get('source_type'))?->isBudget()),
  139. // If user selects to use historical actuals
  140. Forms\Components\Select::make('source_fiscal_year')
  141. ->label('Fiscal Year')
  142. ->options(function () {
  143. $options = [];
  144. $company = auth()->user()->currentCompany;
  145. $earliestDate = Carbon::parse(Accounting::getEarliestTransactionDate());
  146. $fiscalYearStartCurrent = Carbon::parse($company->locale->fiscalYearStartDate());
  147. for ($year = $fiscalYearStartCurrent->year; $year >= $earliestDate->year; $year--) {
  148. $options[$year] = $year;
  149. }
  150. return $options;
  151. })
  152. ->required()
  153. ->live()
  154. ->afterStateUpdated(function (Forms\Set $set) {
  155. // Clear the cache when the fiscal year changes
  156. $this->accountsCache = [];
  157. // Get all accounts without actuals
  158. $accountIdsWithoutActuals = $this->getAccountsWithoutActuals()->pluck('id')->toArray();
  159. // Set exclude_accounts_without_actuals to true by default
  160. $set('exclude_accounts_without_actuals', true);
  161. // Update the selected_accounts field to exclude accounts without actuals
  162. $set('selected_accounts', $accountIdsWithoutActuals);
  163. })
  164. ->visible(fn (Forms\Get $get) => BudgetSourceType::parse($get('source_type'))?->isActuals()),
  165. ])->visible(fn (Forms\Get $get) => $get('prefill_data') === true),
  166. CustomSection::make('Account Selection')
  167. ->contained(false)
  168. ->schema([
  169. Forms\Components\Checkbox::make('exclude_accounts_without_actuals')
  170. ->label('Exclude all accounts without actuals')
  171. ->helperText(function () {
  172. $count = $this->getAccountsWithoutActuals()->count();
  173. return "Will exclude {$count} accounts without transaction data in the selected fiscal year";
  174. })
  175. ->default(true)
  176. ->live()
  177. ->afterStateUpdated(function (Forms\Set $set, $state) {
  178. if ($state) {
  179. // When checked, select all accounts without actuals
  180. $accountsWithoutActuals = $this->getAccountsWithoutActuals()->pluck('id')->toArray();
  181. $set('selected_accounts', $accountsWithoutActuals);
  182. } else {
  183. // When unchecked, clear the selection
  184. $set('selected_accounts', []);
  185. }
  186. }),
  187. Forms\Components\CheckboxList::make('selected_accounts')
  188. ->label('Select Accounts to Exclude')
  189. ->options(function () {
  190. // Get all budgetable accounts
  191. return $this->getBudgetableAccounts()->pluck('name', 'id')->toArray();
  192. })
  193. ->descriptions(function (Forms\Components\CheckboxList $component) {
  194. $fiscalYear = $this->data['source_fiscal_year'] ?? null;
  195. if (blank($fiscalYear)) {
  196. return [];
  197. }
  198. $accountIds = array_keys($component->getOptions());
  199. $descriptions = [];
  200. if (empty($accountIds)) {
  201. return [];
  202. }
  203. // Get account balances
  204. $accountBalances = $this->getAccountBalances()->keyBy('id');
  205. // Get accounts with actuals
  206. $accountsWithActuals = $this->getAccountsWithActuals()->pluck('id')->toArray();
  207. // Process all accounts
  208. foreach ($accountIds as $accountId) {
  209. $balance = $accountBalances[$accountId] ?? null;
  210. $hasActuals = in_array($accountId, $accountsWithActuals);
  211. if ($balance && $hasActuals) {
  212. // Calculate net movement
  213. $netMovement = Accounting::calculateNetMovementByCategory(
  214. $balance->category,
  215. $balance->total_debit ?? 0,
  216. $balance->total_credit ?? 0
  217. );
  218. // Format the amount for display
  219. $formattedAmount = CurrencyConverter::formatCentsToMoney($netMovement);
  220. $descriptions[$accountId] = "{$formattedAmount} in {$fiscalYear}";
  221. } else {
  222. $descriptions[$accountId] = "No transactions in {$fiscalYear}";
  223. }
  224. }
  225. return $descriptions;
  226. })
  227. ->columns(2) // Display in two columns
  228. ->searchable() // Allow searching for accounts
  229. ->bulkToggleable() // Enable "Select All" / "Deselect All"
  230. ->selectAllAction(fn (Action $action) => $action->label('Exclude all accounts'))
  231. ->deselectAllAction(fn (Action $action) => $action->label('Include all accounts'))
  232. ->afterStateUpdated(function (Forms\Set $set, $state) {
  233. // Get all accounts without actuals
  234. $accountsWithoutActuals = $this->getAccountsWithoutActuals()->pluck('id')->toArray();
  235. // Check if all accounts without actuals are in the selected accounts
  236. $allAccountsWithoutActualsSelected = empty(array_diff($accountsWithoutActuals, $state));
  237. // Update the exclude_accounts_without_actuals checkbox state
  238. $set('exclude_accounts_without_actuals', $allAccountsWithoutActualsSelected);
  239. }),
  240. ])
  241. ->visible(function (Forms\Get $get) {
  242. // Only show when using actuals with valid fiscal year AND accounts without transactions exist
  243. $prefillSourceType = BudgetSourceType::parse($get('source_type'));
  244. if ($prefillSourceType !== BudgetSourceType::Actuals || blank($get('source_fiscal_year'))) {
  245. return false;
  246. }
  247. return $this->getAccountsWithoutActuals()->isNotEmpty();
  248. }),
  249. Forms\Components\Textarea::make('notes')
  250. ->label('Notes')
  251. ->columnSpanFull(),
  252. ]),
  253. ];
  254. }
  255. protected function handleRecordCreation(array $data): Model
  256. {
  257. /** @var Budget $budget */
  258. $budget = Budget::create([
  259. 'source_budget_id' => $data['source_budget_id'] ?? null,
  260. 'source_fiscal_year' => $data['source_fiscal_year'] ?? null,
  261. 'source_type' => $data['source_type'] ?? null,
  262. 'name' => $data['name'],
  263. 'interval_type' => $data['interval_type'],
  264. 'start_date' => $data['start_date'],
  265. 'end_date' => $data['end_date'],
  266. 'notes' => $data['notes'] ?? null,
  267. ]);
  268. $selectedAccounts = $data['selected_accounts'] ?? [];
  269. $accountsToInclude = Account::query()
  270. ->budgetable()
  271. ->whereNotIn('id', $selectedAccounts)
  272. ->get();
  273. foreach ($accountsToInclude as $account) {
  274. /** @var BudgetItem $budgetItem */
  275. $budgetItem = $budget->budgetItems()->create([
  276. 'account_id' => $account->id,
  277. ]);
  278. $allocationStart = Carbon::parse($data['start_date']);
  279. $budgetEndDate = Carbon::parse($data['end_date']);
  280. // Determine amounts based on the prefill method
  281. $amounts = match ($data['source_type'] ?? null) {
  282. 'actuals' => $this->getAmountsFromActuals($account, $data['source_fiscal_year'], BudgetIntervalType::parse($data['interval_type'])),
  283. 'previous_budget' => $this->getAmountsFromPreviousBudget($account, $data['source_budget_id'], BudgetIntervalType::parse($data['interval_type'])),
  284. default => $this->generateZeroAmounts($data['start_date'], $data['end_date'], BudgetIntervalType::parse($data['interval_type'])),
  285. };
  286. if (empty($amounts)) {
  287. $amounts = $this->generateZeroAmounts(
  288. $data['start_date'],
  289. $data['end_date'],
  290. BudgetIntervalType::parse($data['interval_type'])
  291. );
  292. }
  293. foreach ($amounts as $periodLabel => $amount) {
  294. if ($allocationStart->gt($budgetEndDate)) {
  295. break;
  296. }
  297. $allocationEnd = self::calculateEndDate($allocationStart, BudgetIntervalType::parse($data['interval_type']));
  298. if ($allocationEnd->gt($budgetEndDate)) {
  299. $allocationEnd = $budgetEndDate->copy();
  300. }
  301. $budgetItem->allocations()->create([
  302. 'period' => $periodLabel,
  303. 'interval_type' => $data['interval_type'],
  304. 'start_date' => $allocationStart->toDateString(),
  305. 'end_date' => $allocationEnd->toDateString(),
  306. 'amount' => CurrencyConverter::convertCentsToFloat($amount),
  307. ]);
  308. $allocationStart = $allocationEnd->addDay();
  309. }
  310. }
  311. return $budget;
  312. }
  313. private function getAmountsFromActuals(Account $account, int $fiscalYear, BudgetIntervalType $intervalType): array
  314. {
  315. $amounts = [];
  316. // Get the start and end date of the budget being created
  317. $budgetStartDate = Carbon::parse($this->data['start_date']);
  318. $budgetEndDate = Carbon::parse($this->data['end_date']);
  319. // Map to equivalent dates in the reference fiscal year
  320. $referenceStartDate = Carbon::create($fiscalYear, $budgetStartDate->month, $budgetStartDate->day);
  321. $referenceEndDate = Carbon::create($fiscalYear, $budgetEndDate->month, $budgetEndDate->day);
  322. // Handle year boundary case (if budget crosses year boundary)
  323. if ($budgetStartDate->month > $budgetEndDate->month ||
  324. ($budgetStartDate->month === $budgetEndDate->month && $budgetStartDate->day > $budgetEndDate->day)) {
  325. $referenceEndDate->year++;
  326. }
  327. if ($intervalType->isMonth()) {
  328. // Process month by month within the reference period
  329. $currentDate = $referenceStartDate->copy()->startOfMonth();
  330. $lastMonth = $referenceEndDate->copy()->startOfMonth();
  331. while ($currentDate->lte($lastMonth)) {
  332. $periodStart = $currentDate->copy()->startOfMonth();
  333. $periodEnd = $currentDate->copy()->endOfMonth();
  334. $periodLabel = $this->determinePeriod($periodStart, $intervalType);
  335. $netMovement = Accounting::getNetMovement(
  336. $account,
  337. $periodStart->toDateString(),
  338. $periodEnd->toDateString()
  339. );
  340. $amounts[$periodLabel] = $netMovement->getAmount();
  341. $currentDate->addMonth();
  342. }
  343. } elseif ($intervalType->isQuarter()) {
  344. // Process quarter by quarter within the reference period
  345. $currentDate = $referenceStartDate->copy()->startOfQuarter();
  346. $lastQuarter = $referenceEndDate->copy()->startOfQuarter();
  347. while ($currentDate->lte($lastQuarter)) {
  348. $periodStart = $currentDate->copy()->startOfQuarter();
  349. $periodEnd = $currentDate->copy()->endOfQuarter();
  350. $periodLabel = $this->determinePeriod($periodStart, $intervalType);
  351. $netMovement = Accounting::getNetMovement(
  352. $account,
  353. $periodStart->toDateString(),
  354. $periodEnd->toDateString()
  355. );
  356. $amounts[$periodLabel] = $netMovement->getAmount();
  357. $currentDate->addQuarter();
  358. }
  359. } else {
  360. // For yearly intervals
  361. $periodStart = $referenceStartDate->copy()->startOfYear();
  362. $periodEnd = $referenceEndDate->copy()->endOfYear();
  363. $periodLabel = $this->determinePeriod($periodStart, $intervalType);
  364. $netMovement = Accounting::getNetMovement(
  365. $account,
  366. $periodStart->toDateString(),
  367. $periodEnd->toDateString()
  368. );
  369. $amounts[$periodLabel] = $netMovement->getAmount();
  370. }
  371. return $amounts;
  372. }
  373. private function distributeAmountAcrossPeriods(int $totalAmountInCents, Carbon $startDate, Carbon $endDate, BudgetIntervalType $intervalType): array
  374. {
  375. $amounts = [];
  376. $periods = [];
  377. // Generate period labels based on interval type
  378. $currentPeriod = $startDate->copy();
  379. while ($currentPeriod->lte($endDate)) {
  380. $periods[] = $this->determinePeriod($currentPeriod, $intervalType);
  381. $currentPeriod->addUnit($intervalType->value);
  382. }
  383. // Evenly distribute total amount across periods
  384. $periodCount = count($periods);
  385. if ($periodCount === 0) {
  386. return $amounts;
  387. }
  388. $baseAmount = intdiv($totalAmountInCents, $periodCount); // Floor division to get the base amount in cents
  389. $remainder = $totalAmountInCents % $periodCount; // Remaining cents to distribute
  390. foreach ($periods as $index => $period) {
  391. $amounts[$period] = $baseAmount + ($index < $remainder ? 1 : 0); // Distribute remainder cents evenly
  392. }
  393. return $amounts;
  394. }
  395. private function getAmountsFromPreviousBudget(Account $account, int $sourceBudgetId, BudgetIntervalType $intervalType): array
  396. {
  397. $amounts = [];
  398. // Get the budget being created start and end dates
  399. $newBudgetStartDate = Carbon::parse($this->data['start_date']);
  400. $newBudgetEndDate = Carbon::parse($this->data['end_date']);
  401. // Get source budget's date information
  402. $sourceBudget = Budget::findOrFail($sourceBudgetId);
  403. $sourceBudgetType = $sourceBudget->interval_type;
  404. // Retrieve all previous allocations for this account
  405. $previousAllocations = BudgetAllocation::query()
  406. ->whereHas(
  407. 'budgetItem',
  408. fn ($query) => $query->where('account_id', $account->id)
  409. ->where('budget_id', $sourceBudgetId)
  410. )
  411. ->orderBy('start_date')
  412. ->get();
  413. if ($previousAllocations->isEmpty()) {
  414. return $this->generateZeroAmounts(
  415. $this->data['start_date'],
  416. $this->data['end_date'],
  417. $intervalType
  418. );
  419. }
  420. // Map previous budget periods to current budget periods
  421. if ($intervalType === $sourceBudgetType) {
  422. // Same interval type: direct mapping of equivalent periods
  423. foreach ($previousAllocations as $allocation) {
  424. $allocationDate = Carbon::parse($allocation->start_date);
  425. // Create an equivalent date in the new budget's time range
  426. $equivalentMonth = $allocationDate->month;
  427. $equivalentDay = $allocationDate->day;
  428. $equivalentYear = $newBudgetStartDate->year;
  429. // Adjust year if the budget spans multiple years
  430. if ($newBudgetStartDate->month > $newBudgetEndDate->month &&
  431. $equivalentMonth < $newBudgetStartDate->month) {
  432. $equivalentYear++;
  433. }
  434. $equivalentDate = Carbon::create($equivalentYear, $equivalentMonth, $equivalentDay);
  435. // Only include if the date falls within our new budget period
  436. if ($equivalentDate->between($newBudgetStartDate, $newBudgetEndDate)) {
  437. $periodLabel = $this->determinePeriod($equivalentDate, $intervalType);
  438. $amounts[$periodLabel] = $allocation->getRawOriginal('amount');
  439. }
  440. }
  441. } else {
  442. // Handle conversion between different interval types
  443. $newBudgetPeriods = $this->generateZeroAmounts(
  444. $this->data['start_date'],
  445. $this->data['end_date'],
  446. $intervalType
  447. );
  448. // Fill with zeros initially
  449. $amounts = array_fill_keys(array_keys($newBudgetPeriods), 0);
  450. // Group previous allocations by their date range
  451. $allocationsByRange = [];
  452. foreach ($previousAllocations as $allocation) {
  453. $allocationsByRange[] = [
  454. 'start' => Carbon::parse($allocation->start_date),
  455. 'end' => Carbon::parse($allocation->end_date),
  456. 'amount' => $allocation->getRawOriginal('amount'),
  457. ];
  458. }
  459. // Create new allocations based on interval type
  460. $currentDate = Carbon::parse($this->data['start_date']);
  461. $endDate = Carbon::parse($this->data['end_date']);
  462. while ($currentDate->lte($endDate)) {
  463. $periodStart = $currentDate->copy();
  464. $periodEnd = self::calculateEndDate($currentDate, $intervalType);
  465. if ($periodEnd->gt($endDate)) {
  466. $periodEnd = $endDate->copy();
  467. }
  468. $periodLabel = $this->determinePeriod($periodStart, $intervalType);
  469. // Calculate the proportional amount from the source budget
  470. $weightedAmount = 0;
  471. foreach ($allocationsByRange as $allocation) {
  472. // Find overlapping days between new period and source allocation
  473. $overlapStart = max($periodStart, $allocation['start']);
  474. $overlapEnd = min($periodEnd, $allocation['end']);
  475. if ($overlapStart <= $overlapEnd) {
  476. // Calculate overlapping days
  477. $overlapDays = $overlapStart->diffInDays($overlapEnd) + 1;
  478. $allocationTotalDays = $allocation['start']->diffInDays($allocation['end']) + 1;
  479. // Calculate proportional amount based on days
  480. $proportion = $overlapDays / $allocationTotalDays;
  481. $proportionalAmount = (int) ($allocation['amount'] * $proportion);
  482. $weightedAmount += $proportionalAmount;
  483. }
  484. }
  485. // Assign the calculated amount to the period
  486. if (array_key_exists($periodLabel, $amounts)) {
  487. $amounts[$periodLabel] = $weightedAmount;
  488. }
  489. // Move to the next period
  490. $currentDate = $periodEnd->copy()->addDay();
  491. }
  492. }
  493. return $amounts;
  494. }
  495. private function generateZeroAmounts(string $startDate, string $endDate, BudgetIntervalType $intervalType): array
  496. {
  497. $amounts = [];
  498. $currentPeriod = Carbon::parse($startDate);
  499. while ($currentPeriod->lte(Carbon::parse($endDate))) {
  500. $period = $this->determinePeriod($currentPeriod, $intervalType);
  501. $amounts[$period] = 0;
  502. $currentPeriod->addUnit($intervalType->value);
  503. }
  504. return $amounts;
  505. }
  506. private function determinePeriod(Carbon $date, BudgetIntervalType $intervalType): string
  507. {
  508. return match ($intervalType) {
  509. BudgetIntervalType::Month => $date->format('M Y'),
  510. BudgetIntervalType::Quarter => 'Q' . $date->quarter . ' ' . $date->year,
  511. BudgetIntervalType::Year => (string) $date->year,
  512. };
  513. }
  514. private static function calculateEndDate(Carbon $startDate, BudgetIntervalType $intervalType): Carbon
  515. {
  516. return match ($intervalType) {
  517. BudgetIntervalType::Month => $startDate->copy()->endOfMonth(),
  518. BudgetIntervalType::Quarter => $startDate->copy()->endOfQuarter(),
  519. BudgetIntervalType::Year => $startDate->copy()->endOfYear(),
  520. };
  521. }
  522. }