From 30cc4da6c627a154b4f9bb9cfa0ceec43ec573b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 11 May 2026 17:54:26 +0100 Subject: [PATCH] Fix cashflow null category rows (#382) ## Sentry - Issue: PHP-LARAVEL-1T - URL: https://whisper-money.sentry.io/issues/PHP-LARAVEL-1T ## Root cause Cashflow breakdown UI assumed every API row has a category object. Production returned a breakdown row with `category: null`, so rendering read `item.category.icon` and crashed. ## Fix - Allow breakdown rows to carry nullable category/category_id. - Render null categories as Uncategorized with HelpCircle/gray fallback. - Add focused Vitest regression. - Enable Wayfinder Vite plugin in Vitest so components importing generated actions resolve in tests. ## Verification - `npm test -- resources/js/components/cashflow/breakdown-card.test.tsx` - `npx prettier --write resources/js/components/cashflow/breakdown-card.tsx resources/js/components/cashflow/breakdown-card.test.tsx resources/js/hooks/use-cashflow-data.ts vitest.config.ts` - `npx eslint resources/js/components/cashflow/breakdown-card.tsx resources/js/components/cashflow/breakdown-card.test.tsx resources/js/hooks/use-cashflow-data.ts vitest.config.ts --fix` ## Notes - `npm run types` currently fails on existing unrelated TypeScript errors across account, transaction, chart, auth, crypto, and dexie files. --- .../cashflow/breakdown-card.test.tsx | 48 +++++++++++++++++++ .../js/components/cashflow/breakdown-card.tsx | 21 ++++++-- resources/js/hooks/use-cashflow-data.ts | 4 +- vitest.config.ts | 3 +- 4 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 resources/js/components/cashflow/breakdown-card.test.tsx diff --git a/resources/js/components/cashflow/breakdown-card.test.tsx b/resources/js/components/cashflow/breakdown-card.test.tsx new file mode 100644 index 00000000..08901dac --- /dev/null +++ b/resources/js/components/cashflow/breakdown-card.test.tsx @@ -0,0 +1,48 @@ +import { render, screen } from '@testing-library/react'; +import type React from 'react'; +import { describe, expect, it, vi } from 'vitest'; +import { BreakdownCard } from './breakdown-card'; + +vi.mock('@/components/ui/amount-display', () => ({ + AmountDisplay: ({ amountInCents }: { amountInCents: number }) => ( + {amountInCents} + ), +})); + +vi.mock('@/actions/App/Http/Controllers/TransactionController', () => ({ + index: () => ({ url: '/transactions' }), +})); + +vi.mock('@inertiajs/react', () => ({ + Link: ({ children, href }: { children: React.ReactNode; href: string }) => ( + {children} + ), + usePage: () => ({ props: { chartColorScheme: 'colorful' } }), +})); + +describe('BreakdownCard', () => { + it('renders uncategorized rows when the API returns a null category', () => { + render( + , + ); + + expect(screen.getByText('Uncategorized')).toBeInTheDocument(); + expect(screen.getByText('100%')).toBeInTheDocument(); + }); +}); diff --git a/resources/js/components/cashflow/breakdown-card.tsx b/resources/js/components/cashflow/breakdown-card.tsx index 6c7b5fe9..88ba5236 100644 --- a/resources/js/components/cashflow/breakdown-card.tsx +++ b/resources/js/components/cashflow/breakdown-card.tsx @@ -12,7 +12,11 @@ import { Progress } from '@/components/ui/progress'; import { BreakdownData } from '@/hooks/use-cashflow-data'; import { useChartColors } from '@/hooks/use-chart-color-scheme'; import { cn } from '@/lib/utils'; -import { getCategoryColorClasses } from '@/types/category'; +import { + type CategoryColor, + type CategoryIcon, + getCategoryColorClasses, +} from '@/types/category'; import { __ } from '@/utils/i18n'; import { Link } from '@inertiajs/react'; import { format } from 'date-fns'; @@ -27,6 +31,12 @@ interface BreakdownCardProps { period?: { from: Date; to: Date }; } +const fallbackCategory = { + name: __('Uncategorized'), + icon: 'HelpCircle' as CategoryIcon, + color: 'gray' as CategoryColor, +}; + export function BreakdownCard({ type, data, @@ -89,8 +99,9 @@ export function BreakdownCard({
{data.data.map((item, index) => { + const category = item.category ?? fallbackCategory; const Icon = (Icons[ - item.category.icon as keyof typeof Icons + category.icon as keyof typeof Icons ] || Icons.HelpCircle) as LucideIcon; const percentageChange = @@ -101,10 +112,10 @@ export function BreakdownCard({ : null; const categoryColor = getCategoryColorClasses( - item.category.color, + category.color, ); const chartColor = categoryBarColor( - item.category.color, + category.color, index, ); @@ -134,7 +145,7 @@ export function BreakdownCard({
- {item.category.name} + {category.name}
diff --git a/resources/js/hooks/use-cashflow-data.ts b/resources/js/hooks/use-cashflow-data.ts index 06971f69..9e859a1d 100644 --- a/resources/js/hooks/use-cashflow-data.ts +++ b/resources/js/hooks/use-cashflow-data.ts @@ -30,8 +30,8 @@ export interface TrendDataPoint { } export interface BreakdownItem { - category: Category; - category_id: string; + category: Category | null; + category_id: string | null; amount: number; percentage: number; previous_amount: number; diff --git a/vitest.config.ts b/vitest.config.ts index 9425f53a..2ef88467 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,10 @@ +import { wayfinder } from '@laravel/vite-plugin-wayfinder'; import react from '@vitejs/plugin-react'; import { resolve } from 'path'; import { defineConfig } from 'vitest/config'; export default defineConfig({ - plugins: [react()], + plugins: [react(), wayfinder({ formVariants: true })], test: { environment: 'jsdom', globals: true,