diff --git a/resources/js/components/cashflow/net-cashflow-card.test.tsx b/resources/js/components/cashflow/net-cashflow-card.test.tsx new file mode 100644 index 00000000..babf4c32 --- /dev/null +++ b/resources/js/components/cashflow/net-cashflow-card.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { NetCashflowCard } from './net-cashflow-card'; + +const summary = (income: number, expense: number, net: number) => ({ + income, + expense, + net, + savings_rate: 0, + savings: 0, + investments: 0, +}); + +vi.mock('@/components/ui/amount-display', () => ({ + AmountDisplay: ({ amountInCents }: { amountInCents: number }) => ( + {amountInCents} + ), +})); + +vi.mock('lucide-react', () => ({ + TrendingDown: () => , + TrendingUp: () => , +})); + +describe('NetCashflowCard', () => { + it('only shows a trend arrow for the previous period comparison', () => { + render( + , + ); + + expect(screen.getAllByTestId('comparison-trend-down')).toHaveLength(2); + expect(screen.getAllByTestId('comparison-trend-up')).toHaveLength(1); + expect(screen.getByText('-1876400')).toBeTruthy(); + expect(screen.getByText('-1800000')).toBeTruthy(); + expect(screen.getByText('76400')).toBeTruthy(); + }); + + it('keeps the signed net amount visible when current net cashflow is negative', () => { + render( + , + ); + + expect(screen.queryByTestId('comparison-trend-down')).toBeNull(); + expect(screen.queryByTestId('comparison-trend-up')).toBeNull(); + expect(screen.getByText('-100000')).toBeTruthy(); + }); +}); diff --git a/resources/js/components/cashflow/net-cashflow-card.tsx b/resources/js/components/cashflow/net-cashflow-card.tsx index 99b60c8c..be9dfc28 100644 --- a/resources/js/components/cashflow/net-cashflow-card.tsx +++ b/resources/js/components/cashflow/net-cashflow-card.tsx @@ -9,7 +9,7 @@ import { import { CashflowSummary } from '@/hooks/use-cashflow-data'; import { cn } from '@/lib/utils'; import { __ } from '@/utils/i18n'; -import { ArrowDown, ArrowUp, TrendingDown, TrendingUp } from 'lucide-react'; +import { TrendingDown, TrendingUp } from 'lucide-react'; interface NetCashflowCardProps { current: CashflowSummary; @@ -18,6 +18,48 @@ interface NetCashflowCardProps { currency?: string; } +interface PeriodComparisonProps { + diff: number; + isFavorable: boolean; + currency: string; +} + +function PeriodComparison({ + diff, + isFavorable, + currency, +}: PeriodComparisonProps) { + const isIncrease = diff >= 0; + const Icon = isIncrease ? TrendingUp : TrendingDown; + + return ( +
+ + + {isIncrease ? '+' : ''} + + + + {__('vs last period')} + +
+ ); +} + export function NetCashflowCard({ current, previous, @@ -39,9 +81,10 @@ export function NetCashflowCard({ ); } - const isPositive = current.net >= 0; const diff = current.net - previous.net; const diffIsPositive = diff >= 0; + const incomeDiff = current.income - previous.income; + const expenseDiff = current.expense - previous.expense; const hasPreviousData = previous.income > 0 || previous.expense > 0; return ( @@ -55,23 +98,8 @@ export function NetCashflowCard({
- {isPositive ? ( - - ) : ( - - )} + {hasPreviousData && ( + = 0} + currency={currency} + /> + )}

@@ -139,6 +174,13 @@ export function NetCashflowCard({ maximumFractionDigits={0} weight="medium" /> + {hasPreviousData && ( + + )}

diff --git a/resources/js/components/cashflow/savings-rate-card.test.tsx b/resources/js/components/cashflow/savings-rate-card.test.tsx new file mode 100644 index 00000000..24f64702 --- /dev/null +++ b/resources/js/components/cashflow/savings-rate-card.test.tsx @@ -0,0 +1,66 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { SavingsRateCard } from './savings-rate-card'; + +const summary = ( + income: number, + expense: number, + net: number, + savingsRate: number, + savings: number, + investments: number, +) => ({ + income, + expense, + net, + savings_rate: savingsRate, + savings, + investments, +}); + +vi.mock('@/components/ui/amount-display', () => ({ + AmountDisplay: ({ amountInCents }: { amountInCents: number }) => ( + {amountInCents} + ), +})); + +vi.mock('lucide-react', () => ({ + TrendingDown: () => , + TrendingUp: () => , +})); + +describe('SavingsRateCard', () => { + it('shows previous period comparisons below rate, saved, and invested values', () => { + render( + , + ); + + expect(screen.getByText('40.0%')).toBeTruthy(); + expect(screen.getByText('+10.0%')).toBeTruthy(); + expect(screen.getByText('50000')).toBeTruthy(); + expect(screen.getByText('30000')).toBeTruthy(); + expect(screen.getByText('10000')).toBeTruthy(); + expect(screen.getByText('-20000')).toBeTruthy(); + expect(screen.getAllByText('vs last period')).toHaveLength(3); + expect(screen.getAllByTestId('comparison-trend-up')).toHaveLength(2); + expect(screen.getAllByTestId('comparison-trend-down')).toHaveLength(1); + }); + + it('hides comparisons when there is no previous period data', () => { + render( + , + ); + + expect(screen.queryByText('vs last period')).toBeNull(); + expect(screen.queryByTestId('comparison-trend-up')).toBeNull(); + expect(screen.queryByTestId('comparison-trend-down')).toBeNull(); + }); +}); diff --git a/resources/js/components/cashflow/savings-rate-card.tsx b/resources/js/components/cashflow/savings-rate-card.tsx index 30ecc673..6b7c366b 100644 --- a/resources/js/components/cashflow/savings-rate-card.tsx +++ b/resources/js/components/cashflow/savings-rate-card.tsx @@ -18,6 +18,72 @@ interface SavingsRateCardProps { currency?: string; } +interface PercentageComparisonProps { + diff: number; +} + +function PercentageComparison({ diff }: PercentageComparisonProps) { + const isPositive = diff >= 0; + const Icon = isPositive ? TrendingUp : TrendingDown; + + return ( +
+ + + {isPositive ? '+' : ''} + {diff.toFixed(1)}% + + + {__('vs last period')} + +
+ ); +} + +interface AmountComparisonProps { + diff: number; + currency: string; +} + +function AmountComparison({ diff, currency }: AmountComparisonProps) { + const isPositive = diff >= 0; + const Icon = isPositive ? TrendingUp : TrendingDown; + + return ( +
+ + + {isPositive ? '+' : ''} + + + + {__('vs last period')} + +
+ ); +} + export function SavingsRateCard({ current, previous, @@ -40,7 +106,8 @@ export function SavingsRateCard({ } const diff = current.savings_rate - previous.savings_rate; - const isPositive = diff >= 0; + const savingsDiff = current.savings - previous.savings; + const investmentsDiff = current.investments - previous.investments; const hasPreviousData = previous.income > 0; // Determine color based on savings rate @@ -73,20 +140,8 @@ export function SavingsRateCard({ > {current.savings_rate.toFixed(1)}% - {hasPreviousData && ( -
- {isPositive ? ( - - ) : ( - - )} - - {isPositive ? '+' : ''} - {diff.toFixed(1)}% - -
- )} + {hasPreviousData && }

@@ -100,6 +155,12 @@ export function SavingsRateCard({ weight="medium" highlightPositive /> + {hasPreviousData && ( + + )}

@@ -113,6 +174,12 @@ export function SavingsRateCard({ weight="medium" highlightPositive /> + {hasPreviousData && ( + + )}