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 && (
+
+ )}