Refactor: Update dashboard components and remove unused cards (#8)

This commit is contained in:
Víctor Falcón 2025-12-03 11:56:41 +01:00 committed by GitHub
parent 4b11843916
commit 8202ebe139
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 62 additions and 261 deletions

View File

@ -1,9 +1,9 @@
import { EncryptedText } from '@/components/encrypted-text'; import { EncryptedText } from '@/components/encrypted-text';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Account } from '@/types/account'; import { Account } from '@/types/account';
import { AmountTrendIndicator } from './amount-trend-indicator';
import { AccountTypeIcon } from './account-type-icon';
import { Line, LineChart, ResponsiveContainer, Tooltip } from 'recharts'; import { Line, LineChart, ResponsiveContainer, Tooltip } from 'recharts';
import { AccountTypeIcon } from './account-type-icon';
import { AmountTrendIndicator } from './amount-trend-indicator';
interface AccountBalanceCardProps { interface AccountBalanceCardProps {
account: Account & { account: Account & {
@ -43,8 +43,14 @@ export function AccountBalanceCard({
return ( return (
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center"> <CardTitle className="flex items-center text-sm font-medium">
{account.bank.logo && <img src={account.bank.logo} alt={account.bank.name} className="inline-block size-5 mr-2 rounded-full object-contain" />} {account.bank.logo && (
<img
src={account.bank.logo}
alt={account.bank.name}
className="mr-2 inline-block size-5 rounded-full object-contain"
/>
)}
<EncryptedText <EncryptedText
encryptedText={account.name} encryptedText={account.name}
@ -53,23 +59,28 @@ export function AccountBalanceCard({
/> />
</CardTitle> </CardTitle>
<div className="text-xs font-medium text-muted-foreground"> <div className="text-xs font-medium text-muted-foreground">
<AccountTypeIcon type={account.type} className="inline-block mr-1" /> <AccountTypeIcon
type={account.type}
className="mr-1 inline-block"
/>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex gap-6 items-center justify-between"> <div className="flex items-center justify-between gap-6">
<div className='flex flex-col gap-1'> <div className="flex flex-col gap-1">
<div className="text-2xl font-medium"> <div className="text-2xl font-medium">
{formatter.format(account.currentBalance / 100)} {formatter.format(account.currentBalance / 100)}
</div> </div>
<AmountTrendIndicator <AmountTrendIndicator
isPositive={isPositive} isPositive={isPositive}
trend={formatter.format(Math.abs(account.diff) / 100)} trend={formatter.format(
Math.abs(account.diff) / 100,
)}
label="vs last month" label="vs last month"
className='text-sm' className="text-sm"
/> />
</div> </div>
<div className="h-[70px] max-w-[250px] w-full flex-1"> <div className="h-[70px] w-full max-w-[250px] flex-1">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={account.history}> <LineChart data={account.history}>
<Tooltip <Tooltip

View File

@ -1,20 +1,33 @@
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
import { AccountType } from "@/types/account"; import { AccountType } from '@/types/account';
import { BadgeQuestionMarkIcon, Building2, CreditCard, FolderKanban, Landmark, LucideIcon, PiggyBank } from "lucide-react"; import {
BadgeQuestionMarkIcon,
Building2,
CreditCard,
FolderKanban,
Landmark,
LucideIcon,
PiggyBank,
} from 'lucide-react';
export function AccountTypeIcon({ type, className }: { type: AccountType, className?: string }) { export function AccountTypeIcon({
type,
className,
}: {
type: AccountType;
className?: string;
}) {
const typeMap: Record<AccountType, LucideIcon> = { const typeMap: Record<AccountType, LucideIcon> = {
checking: Building2, // 🏦 - bank / institution checking: Building2, // 🏦 - bank / institution
credit_card: CreditCard, // 💳 - card credit_card: CreditCard, // 💳 - card
loan: Landmark, // 🏠 - "institution/loan", or use Home if it's a mortgage loan: Landmark, // 🏠 - "institution/loan", or use Home if it's a mortgage
savings: PiggyBank, // 💰 - savings savings: PiggyBank, // 💰 - savings
others: FolderKanban, // 📁 - miscellaneous/other others: FolderKanban, // 📁 - miscellaneous/other
}; };
const Icon = typeMap[type] || BadgeQuestionMarkIcon; const Icon = typeMap[type] || BadgeQuestionMarkIcon;
return <Icon className={cn([ return (
"h-5 w-5 text-muted-foreground", <Icon className={cn(['h-5 w-5 text-muted-foreground', className])} />
className );
])} />;
} }

View File

@ -1,5 +1,5 @@
import { cn } from "@/lib/utils"; import { cn } from '@/lib/utils';
import { TrendingUp, TrendingDown } from "lucide-react"; import { TrendingDown, TrendingUp } from 'lucide-react';
export function AmountTrendIndicator({ export function AmountTrendIndicator({
trend, trend,
@ -20,10 +20,7 @@ export function AmountTrendIndicator({
: 'text-red-600/70 dark:text-red-400/70'; : 'text-red-600/70 dark:text-red-400/70';
return ( return (
<div className={cn([ <div className={cn(['flex items-center gap-1', className])}>
"flex items-center gap-1",
className
])}>
<span <span
className={ className={
isPositive ? 'bg-green-100/25 dark:bg-green-900/25' : '' isPositive ? 'bg-green-100/25 dark:bg-green-900/25' : ''

View File

@ -1,53 +0,0 @@
import { StatCard } from '@/components/dashboard/stat-card';
import { ArrowLeftRightIcon } from 'lucide-react';
interface CashFlowCardProps {
income: number;
expense: number;
previous?: {
income: number;
expense: number;
};
loading?: boolean;
}
export function CashFlowCard({
income,
expense,
previous,
loading,
}: CashFlowCardProps) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
const currentNet = income - expense;
const calculateTrend = () => {
if (!previous) return undefined;
const previousNet = previous.income - previous.expense;
if (previousNet === 0) return undefined;
const diff = currentNet - previousNet;
const percentage = (diff / Math.abs(previousNet)) * 100;
return {
value: percentage,
label: 'from previous period',
};
};
return (
<StatCard
title="Cash Flow"
value={formatter.format(currentNet / 100)}
icon={ArrowLeftRightIcon}
trend={calculateTrend()}
description={`Income: ${formatter.format(income / 100)} • Expense: ${formatter.format(expense / 100)}`}
loading={loading}
/>
);
}

View File

@ -1,42 +0,0 @@
import { StatCard } from '@/components/dashboard/stat-card';
import { WalletIcon } from 'lucide-react';
interface NetWorthCardProps {
current: number;
previous: number;
loading?: boolean;
}
export function NetWorthCard({
current,
previous,
loading,
}: NetWorthCardProps) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
const calculateTrend = () => {
if (!previous) return undefined;
const diff = current - previous;
const percentage = (diff / previous) * 100;
return {
value: percentage,
label: 'from previous period',
};
};
return (
<StatCard
title="Net Worth"
value={formatter.format(current / 100)}
icon={WalletIcon}
trend={calculateTrend()}
loading={loading}
/>
);
}

View File

@ -1,4 +1,4 @@
import { TrendingUp, TrendingDown } from "lucide-react"; import { TrendingDown, TrendingUp } from 'lucide-react';
export function PercentageTrendIndicator({ export function PercentageTrendIndicator({
trend, trend,

View File

@ -1,43 +0,0 @@
import { StatCard } from '@/components/dashboard/stat-card';
import { CreditCardIcon } from 'lucide-react';
interface SpendingSummaryCardProps {
current: number;
previous?: number;
loading?: boolean;
}
export function SpendingSummaryCard({
current,
previous,
loading,
}: SpendingSummaryCardProps) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
const calculateTrend = () => {
if (!previous) return undefined;
const diff = current - previous;
const percentage = (diff / previous) * 100;
return {
value: percentage,
label: 'from previous period',
};
};
return (
<StatCard
title="Monthly Spending"
value={formatter.format(current / 100)}
icon={CreditCardIcon}
trend={calculateTrend()}
description="Total spending this month"
loading={loading}
/>
);
}

View File

@ -17,24 +17,6 @@ export interface NetWorthEvolutionData {
} }
export interface DashboardData { export interface DashboardData {
netWorth: {
current: number;
previous: number;
diff: number;
};
monthlySpending: {
current: number;
limit: null;
previous: number;
};
cashFlow: {
income: number;
expense: number;
previous: {
income: number;
expense: number;
};
};
netWorthEvolution: NetWorthEvolutionData; netWorthEvolution: NetWorthEvolutionData;
accounts: Array< accounts: Array<
Account & { Account & {
@ -52,13 +34,6 @@ export interface DashboardData {
export function useDashboardData(): DashboardData { export function useDashboardData(): DashboardData {
const [data, setData] = useState<Omit<DashboardData, 'isLoading'>>({ const [data, setData] = useState<Omit<DashboardData, 'isLoading'>>({
netWorth: { current: 0, previous: 0, diff: 0 },
monthlySpending: { current: 0, limit: null, previous: 0 },
cashFlow: {
income: 0,
expense: 0,
previous: { income: 0, expense: 0 },
},
netWorthEvolution: { data: [], accounts: {} }, netWorthEvolution: { data: [], accounts: {} },
accounts: [], accounts: [],
topCategories: [], topCategories: [],
@ -72,7 +47,6 @@ export function useDashboardData(): DashboardData {
const now = new Date(); const now = new Date();
const to = format(now, 'yyyy-MM-dd'); const to = format(now, 'yyyy-MM-dd');
// Last 12 months for evolution charts
const from12Months = format(subMonths(now, 12), 'yyyy-MM-dd'); const from12Months = format(subMonths(now, 12), 'yyyy-MM-dd');
const params12Months = new URLSearchParams({ const params12Months = new URLSearchParams({
from: from12Months, from: from12Months,
@ -80,7 +54,6 @@ export function useDashboardData(): DashboardData {
}); });
const query12Months = `?${params12Months.toString()}`; const query12Months = `?${params12Months.toString()}`;
// Last 30 days for top blocks and categories
const from30Days = format(subDays(now, 30), 'yyyy-MM-dd'); const from30Days = format(subDays(now, 30), 'yyyy-MM-dd');
const params30Days = new URLSearchParams({ const params30Days = new URLSearchParams({
from: from30Days, from: from30Days,
@ -88,50 +61,20 @@ export function useDashboardData(): DashboardData {
}); });
const query30Days = `?${params30Days.toString()}`; const query30Days = `?${params30Days.toString()}`;
const [ const [netWorthEvolution, accountBalances, topCategories] =
netWorth, await Promise.all([
monthlySpending, fetch(
cashFlow, `/api/dashboard/net-worth-evolution${query12Months}`,
netWorthEvolution, ).then((r) => r.json()),
accountBalances, fetch(
topCategories, `/api/dashboard/account-balances${query12Months}`,
] = await Promise.all([ ).then((r) => r.json()),
fetch(`/api/dashboard/net-worth${query30Days}`).then((r) => fetch(
r.json(), `/api/dashboard/top-categories${query30Days}`,
), ).then((r) => r.json()),
fetch(`/api/dashboard/monthly-spending${query30Days}`).then( ]);
(r) => r.json(),
),
fetch(`/api/dashboard/cash-flow${query30Days}`).then((r) =>
r.json(),
),
fetch(
`/api/dashboard/net-worth-evolution${query12Months}`,
).then((r) => r.json()),
fetch(
`/api/dashboard/account-balances${query12Months}`,
).then((r) => r.json()),
fetch(`/api/dashboard/top-categories${query30Days}`).then(
(r) => r.json(),
),
]);
setData({ setData({
netWorth: {
current: netWorth.current,
previous: netWorth.previous,
diff: netWorth.current - netWorth.previous,
},
monthlySpending: {
current: monthlySpending.current,
previous: monthlySpending.previous,
limit: null,
},
cashFlow: {
income: cashFlow.current.income,
expense: cashFlow.current.expense,
previous: cashFlow.previous,
},
netWorthEvolution: netWorthEvolution:
netWorthEvolution as NetWorthEvolutionData, netWorthEvolution as NetWorthEvolutionData,
accounts: accountBalances.map( accounts: accountBalances.map(

View File

@ -1,8 +1,5 @@
import { AccountBalanceCard } from '@/components/dashboard/account-balance-card'; import { AccountBalanceCard } from '@/components/dashboard/account-balance-card';
import { CashFlowCard } from '@/components/dashboard/cash-flow-card';
import { NetWorthCard } from '@/components/dashboard/net-worth-card';
import { NetWorthChart as NetWorthChartComponent } from '@/components/dashboard/net-worth-chart'; import { NetWorthChart as NetWorthChartComponent } from '@/components/dashboard/net-worth-chart';
import { SpendingSummaryCard } from '@/components/dashboard/spending-summary-card';
import { TopCategoriesCard } from '@/components/dashboard/top-categories-card'; import { TopCategoriesCard } from '@/components/dashboard/top-categories-card';
import HeadingSmall from '@/components/heading-small'; import HeadingSmall from '@/components/heading-small';
import { useDashboardData } from '@/hooks/use-dashboard-data'; import { useDashboardData } from '@/hooks/use-dashboard-data';
@ -20,9 +17,6 @@ const breadcrumbs: BreadcrumbItem[] = [
export default function Dashboard() { export default function Dashboard() {
const { const {
netWorth,
monthlySpending,
cashFlow,
netWorthEvolution, netWorthEvolution,
accounts: accountMetrics, accounts: accountMetrics,
topCategories, topCategories,
@ -39,25 +33,6 @@ export default function Dashboard() {
description="Overview of your financial health" description="Overview of your financial health"
/> />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<NetWorthCard
current={netWorth.current}
previous={netWorth.previous}
loading={isLoading}
/>
<SpendingSummaryCard
current={monthlySpending.current}
previous={monthlySpending.previous}
loading={isLoading}
/>
<CashFlowCard
income={cashFlow.income}
expense={cashFlow.expense}
previous={cashFlow.previous}
loading={isLoading}
/>
</div>
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-1 lg:grid-cols-3">
<NetWorthChartComponent <NetWorthChartComponent
data={netWorthEvolution} data={netWorthEvolution}