From f8f3b060733fae8e085c1d656f49e1276f1a1f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Tue, 12 May 2026 12:35:25 +0100 Subject: [PATCH] Add yearly budget period (#384) ## Summary - Add yearly budget period option on backend and frontend - Generate yearly budget periods for Jan 1 through Dec 31 - Add feature coverage for yearly budget creation and period generation ## Tests - vendor/bin/pint --dirty --format agent - php artisan test --compact tests/Feature/BudgetPeriodServiceTest.php tests/Feature/BudgetTest.php ## Notes - npm run types fails on existing unrelated TypeScript errors: missing generated Wayfinder modules and pre-existing type mismatches --- app/Enums/BudgetPeriodType.php | 2 + app/Services/BudgetPeriodService.php | 4 ++ database/factories/BudgetFactory.php | 8 +++ .../budgets/create-budget-dialog.tsx | 70 +++++++++--------- .../components/budgets/edit-budget-dialog.tsx | 72 ++++++++++--------- resources/js/types/budget.ts | 8 ++- tests/Feature/BudgetPeriodServiceTest.php | 39 ++++++++++ tests/Feature/BudgetTest.php | 24 +++++++ 8 files changed, 161 insertions(+), 66 deletions(-) diff --git a/app/Enums/BudgetPeriodType.php b/app/Enums/BudgetPeriodType.php index 1765d0de..834d5922 100644 --- a/app/Enums/BudgetPeriodType.php +++ b/app/Enums/BudgetPeriodType.php @@ -7,6 +7,7 @@ enum BudgetPeriodType: string case Monthly = 'monthly'; case Weekly = 'weekly'; case Biweekly = 'biweekly'; + case Yearly = 'yearly'; public function label(): string { @@ -14,6 +15,7 @@ enum BudgetPeriodType: string self::Monthly => 'Monthly', self::Weekly => 'Weekly', self::Biweekly => 'Bi-weekly', + self::Yearly => 'Yearly', }; } } diff --git a/app/Services/BudgetPeriodService.php b/app/Services/BudgetPeriodService.php index 76628776..7b7a2bb1 100644 --- a/app/Services/BudgetPeriodService.php +++ b/app/Services/BudgetPeriodService.php @@ -80,6 +80,10 @@ class BudgetPeriodService $endDate = $startDate->copy()->addWeeks(2)->subDay(); break; + case BudgetPeriodType::Yearly: + $startDate->startOfYear(); + $endDate = $startDate->copy()->addYear()->subDay(); + break; } return [$startDate, $endDate]; diff --git a/database/factories/BudgetFactory.php b/database/factories/BudgetFactory.php index 51552e19..2707e200 100644 --- a/database/factories/BudgetFactory.php +++ b/database/factories/BudgetFactory.php @@ -42,4 +42,12 @@ class BudgetFactory extends Factory 'period_start_day' => fake()->numberBetween(0, 6), ]); } + + public function yearly(): static + { + return $this->state(fn (array $attributes) => [ + 'period_type' => BudgetPeriodType::Yearly, + 'period_start_day' => 1, + ]); + } } diff --git a/resources/js/components/budgets/create-budget-dialog.tsx b/resources/js/components/budgets/create-budget-dialog.tsx index 546c561b..3b6a1065 100644 --- a/resources/js/components/budgets/create-budget-dialog.tsx +++ b/resources/js/components/budgets/create-budget-dialog.tsx @@ -88,7 +88,7 @@ export function CreateBudgetDialog({ { name, period_type: periodType, - period_start_day: periodStartDay, + period_start_day: periodType === 'yearly' ? 1 : periodStartDay, category_id: selectedCategoryId || null, label_id: selectedLabelId || null, rollover_type: rolloverType, @@ -164,9 +164,10 @@ export function CreateBudgetDialog({ -
- - {periodType === 'monthly' - ? __('Start Day of Month') - : __('Start Day')} - - - setPeriodStartDay(parseInt(e.target.value)) - } - /> + {periodType !== 'yearly' && ( +
+ + {periodType === 'monthly' + ? __('Start Day of Month') + : __('Start Day')} + + + setPeriodStartDay( + parseInt(e.target.value), + ) + } + /> -

- {periodType === 'monthly' - ? __( - 'Day of the month when the period starts (1-31)', - ) - : periodType === 'weekly' || - periodType === 'biweekly' - ? __('Day of week (0=Sunday, 6=Saturday)') - : __('Starting day')} -

-
+

+ {periodType === 'monthly' + ? __( + 'Day of the month when the period starts (1-31)', + ) + : __( + 'Day of week (0=Sunday, 6=Saturday)', + )} +

+
+ )}
{errors.selection && ( @@ -308,7 +312,9 @@ export function CreateBudgetDialog({ )}

- {__('Select at least a category or a label to track.')} + {__( + 'Select at least a category or a label to track.', + )}

diff --git a/resources/js/components/budgets/edit-budget-dialog.tsx b/resources/js/components/budgets/edit-budget-dialog.tsx index 053eafa1..cd8472c5 100644 --- a/resources/js/components/budgets/edit-budget-dialog.tsx +++ b/resources/js/components/budgets/edit-budget-dialog.tsx @@ -80,7 +80,7 @@ export function EditBudgetDialog({ { name, period_type: periodType, - period_start_day: periodStartDay, + period_start_day: periodType === 'yearly' ? 1 : periodStartDay, allocated_amount: allocatedAmount, rollover_type: rolloverType, }, @@ -127,9 +127,12 @@ export function EditBudgetDialog({ - setPeriodStartDay(parseInt(e.target.value)) - } - /> + {periodType !== 'yearly' && ( +
+ + + setPeriodStartDay( + parseInt(e.target.value), + ) + } + /> -

- {periodType === 'monthly' - ? __( - 'Day of the month when the period starts (1-31)', - ) - : periodType === 'weekly' || - periodType === 'biweekly' - ? __('Day of week (0=Sunday, 6=Saturday)') - : __('Starting day')} -

-
+

+ {periodType === 'monthly' + ? __( + 'Day of the month when the period starts (1-31)', + ) + : __( + 'Day of week (0=Sunday, 6=Saturday)', + )} +

+ + )}