148 lines
5.5 KiB
TypeScript
148 lines
5.5 KiB
TypeScript
import { AmountDisplay } from '@/components/ui/amount-display';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { CashflowSummary } from '@/hooks/use-cashflow-data';
|
|
import { cn } from '@/lib/utils';
|
|
import { __ } from '@/utils/i18n';
|
|
import { ArrowDown, ArrowUp, TrendingDown, TrendingUp } from 'lucide-react';
|
|
|
|
interface NetCashflowCardProps {
|
|
current: CashflowSummary;
|
|
previous: CashflowSummary;
|
|
loading?: boolean;
|
|
currency?: string;
|
|
}
|
|
|
|
export function NetCashflowCard({
|
|
current,
|
|
previous,
|
|
loading,
|
|
currency = 'USD',
|
|
}: NetCashflowCardProps) {
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
{__('Net Cashflow')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-12 w-32 animate-pulse rounded bg-gray-200 dark:bg-gray-700" />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const isPositive = current.net >= 0;
|
|
const diff = current.net - previous.net;
|
|
const diffIsPositive = diff >= 0;
|
|
const hasPreviousData = previous.income > 0 || previous.expense > 0;
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
{__('Net Cashflow')}
|
|
</CardTitle>
|
|
<CardDescription>{__('Income minus expenses')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-baseline gap-2">
|
|
<div className={cn('flex items-center gap-1')}>
|
|
{isPositive ? (
|
|
<ArrowUp
|
|
className={cn(
|
|
'size-4',
|
|
'text-green-600 dark:text-green-400',
|
|
)}
|
|
/>
|
|
) : (
|
|
<ArrowDown
|
|
className={cn(
|
|
'size-4',
|
|
'text-red-600 dark:text-red-400',
|
|
)}
|
|
/>
|
|
)}
|
|
<AmountDisplay
|
|
amountInCents={Math.abs(current.net)}
|
|
currencyCode={currency}
|
|
size="2xl"
|
|
weight="bold"
|
|
minimumFractionDigits={0}
|
|
maximumFractionDigits={0}
|
|
highlightPositive
|
|
/>
|
|
</div>
|
|
</div>
|
|
{hasPreviousData && (
|
|
<div className={cn('mt-2 flex items-center gap-1 text-sm')}>
|
|
{diffIsPositive ? (
|
|
<TrendingUp
|
|
className={cn(
|
|
'size-4',
|
|
'text-green-600 dark:text-green-400',
|
|
)}
|
|
/>
|
|
) : (
|
|
<TrendingDown
|
|
className={cn(
|
|
'size-4',
|
|
'text-red-600 dark:text-red-400',
|
|
)}
|
|
/>
|
|
)}
|
|
<span>
|
|
{diffIsPositive ? '+' : ''}
|
|
<AmountDisplay
|
|
amountInCents={diff}
|
|
currencyCode={currency}
|
|
minimumFractionDigits={0}
|
|
maximumFractionDigits={0}
|
|
className="text-sm"
|
|
highlightPositive
|
|
/>
|
|
</span>
|
|
<span className="text-muted-foreground">
|
|
{__('vs last period')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="mt-3 grid grid-cols-2 gap-4 border-t pt-3">
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{__('Income')}
|
|
</p>
|
|
<AmountDisplay
|
|
amountInCents={current.income}
|
|
currencyCode={currency}
|
|
minimumFractionDigits={0}
|
|
maximumFractionDigits={0}
|
|
weight="medium"
|
|
highlightPositive
|
|
/>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{__('Expenses')}
|
|
</p>
|
|
<AmountDisplay
|
|
amountInCents={current.expense}
|
|
currencyCode={currency}
|
|
minimumFractionDigits={0}
|
|
maximumFractionDigits={0}
|
|
weight="medium"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|