diff --git a/app/Http/Controllers/CashflowController.php b/app/Http/Controllers/CashflowController.php index 368ede6a..5a162696 100644 --- a/app/Http/Controllers/CashflowController.php +++ b/app/Http/Controllers/CashflowController.php @@ -34,10 +34,16 @@ class CashflowController extends Controller ->orderBy('name') ->get(['id', 'name', 'logo']); + $period = $request->query('period'); + $validPeriod = is_string($period) && preg_match('/^\d{4}-\d{2}$/', $period) === 1 + ? $period + : null; + return Inertia::render('cashflow/index', [ 'categories' => $categories, 'accounts' => $accounts, 'banks' => $banks, + 'period' => $validPeriod, ]); } } diff --git a/resources/js/pages/cashflow/index.tsx b/resources/js/pages/cashflow/index.tsx index 1d3ef713..4add27bc 100644 --- a/resources/js/pages/cashflow/index.tsx +++ b/resources/js/pages/cashflow/index.tsx @@ -22,18 +22,17 @@ const breadcrumbs: BreadcrumbItem[] = [ ]; export default function CashflowPage() { - const { auth } = usePage<{ auth: { user: { currency_code: string } } }>() - .props; + const { auth, period: initialPeriod } = usePage<{ + auth: { user: { currency_code: string } }; + period: string | null; + }>().props; - // Initialize currentDate from URL query param or default to current month + // Initialize currentDate from server-provided period prop or default to current month const [currentDate, setCurrentDate] = useState(() => { - const urlParams = new URLSearchParams(window.location.search); - const periodParam = urlParams.get('period'); - - if (periodParam) { + if (initialPeriod) { try { // Parse YYYY-MM format - const parsedDate = parse(periodParam, 'yyyy-MM', new Date()); + const parsedDate = parse(initialPeriod, 'yyyy-MM', new Date()); // Validate it's a valid date if (!isNaN(parsedDate.getTime())) { return parsedDate; @@ -63,19 +62,16 @@ export default function CashflowPage() { // Update URL when currentDate changes useEffect(() => { const periodParam = format(currentDate, 'yyyy-MM'); - const currentPeriodParam = new URLSearchParams( - window.location.search, - ).get('period'); // Only update if the period has changed - if (currentPeriodParam !== periodParam) { + if (initialPeriod !== periodParam) { router.visit(cashflow({ query: { period: periodParam } }).url, { preserveScroll: true, preserveState: true, replace: true, }); } - }, [currentDate]); + }, [currentDate, initialPeriod]); return ( diff --git a/tests/Feature/CashflowPageTest.php b/tests/Feature/CashflowPageTest.php new file mode 100644 index 00000000..1ad36781 --- /dev/null +++ b/tests/Feature/CashflowPageTest.php @@ -0,0 +1,56 @@ +get(route('cashflow'))->assertRedirect(route('login')); +}); + +test('period prop is null when no query param given', function () { + $this->actingAs(User::factory()->onboarded()->create()); + + $this->get(route('cashflow')) + ->assertOk() + ->assertInertia( + fn (AssertableInertia $page) => $page + ->component('cashflow/index') + ->where('period', null) + ); +}); + +test('valid period query param is passed to page props', function () { + $this->actingAs(User::factory()->onboarded()->create()); + + $this->get(route('cashflow', ['period' => '2025-03'])) + ->assertOk() + ->assertInertia( + fn (AssertableInertia $page) => $page + ->component('cashflow/index') + ->where('period', '2025-03') + ); +}); + +test('invalid period query param is sanitized to null', function () { + $this->actingAs(User::factory()->onboarded()->create()); + + $this->get(route('cashflow', ['period' => 'not-a-date'])) + ->assertOk() + ->assertInertia( + fn (AssertableInertia $page) => $page + ->component('cashflow/index') + ->where('period', null) + ); +}); + +test('malformed period format is rejected', function () { + $this->actingAs(User::factory()->onboarded()->create()); + + $this->get(route('cashflow', ['period' => '2025-3'])) + ->assertOk() + ->assertInertia( + fn (AssertableInertia $page) => $page + ->component('cashflow/index') + ->where('period', null) + ); +});