Refactor: Update dashboard components and remove unused cards (#8)
This commit is contained in:
parent
4b11843916
commit
8202ebe139
|
|
@ -1,9 +1,9 @@
|
|||
import { EncryptedText } from '@/components/encrypted-text';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
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 { AccountTypeIcon } from './account-type-icon';
|
||||
import { AmountTrendIndicator } from './amount-trend-indicator';
|
||||
|
||||
interface AccountBalanceCardProps {
|
||||
account: Account & {
|
||||
|
|
@ -43,8 +43,14 @@ export function AccountBalanceCard({
|
|||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium flex items-center">
|
||||
{account.bank.logo && <img src={account.bank.logo} alt={account.bank.name} className="inline-block size-5 mr-2 rounded-full object-contain" />}
|
||||
<CardTitle className="flex items-center text-sm font-medium">
|
||||
{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={account.name}
|
||||
|
|
@ -53,23 +59,28 @@ export function AccountBalanceCard({
|
|||
/>
|
||||
</CardTitle>
|
||||
<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>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-6 items-center justify-between">
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-2xl font-medium">
|
||||
{formatter.format(account.currentBalance / 100)}
|
||||
</div>
|
||||
<AmountTrendIndicator
|
||||
isPositive={isPositive}
|
||||
trend={formatter.format(Math.abs(account.diff) / 100)}
|
||||
trend={formatter.format(
|
||||
Math.abs(account.diff) / 100,
|
||||
)}
|
||||
label="vs last month"
|
||||
className='text-sm'
|
||||
className="text-sm"
|
||||
/>
|
||||
</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%">
|
||||
<LineChart data={account.history}>
|
||||
<Tooltip
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { AccountType } from "@/types/account";
|
||||
import { BadgeQuestionMarkIcon, Building2, CreditCard, FolderKanban, Landmark, LucideIcon, PiggyBank } from "lucide-react";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AccountType } from '@/types/account';
|
||||
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> = {
|
||||
checking: Building2, // 🏦 - bank / institution
|
||||
checking: Building2, // 🏦 - bank / institution
|
||||
credit_card: CreditCard, // 💳 - card
|
||||
loan: Landmark, // 🏠 - "institution/loan", or use Home if it's a mortgage
|
||||
savings: PiggyBank, // 💰 - savings
|
||||
others: FolderKanban, // 📁 - miscellaneous/other
|
||||
loan: Landmark, // 🏠 - "institution/loan", or use Home if it's a mortgage
|
||||
savings: PiggyBank, // 💰 - savings
|
||||
others: FolderKanban, // 📁 - miscellaneous/other
|
||||
};
|
||||
|
||||
const Icon = typeMap[type] || BadgeQuestionMarkIcon;
|
||||
|
||||
return <Icon className={cn([
|
||||
"h-5 w-5 text-muted-foreground",
|
||||
className
|
||||
])} />;
|
||||
return (
|
||||
<Icon className={cn(['h-5 w-5 text-muted-foreground', className])} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
import { TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TrendingDown, TrendingUp } from 'lucide-react';
|
||||
|
||||
export function AmountTrendIndicator({
|
||||
trend,
|
||||
|
|
@ -20,10 +20,7 @@ export function AmountTrendIndicator({
|
|||
: 'text-red-600/70 dark:text-red-400/70';
|
||||
|
||||
return (
|
||||
<div className={cn([
|
||||
"flex items-center gap-1",
|
||||
className
|
||||
])}>
|
||||
<div className={cn(['flex items-center gap-1', className])}>
|
||||
<span
|
||||
className={
|
||||
isPositive ? 'bg-green-100/25 dark:bg-green-900/25' : ''
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { TrendingDown, TrendingUp } from 'lucide-react';
|
||||
|
||||
export function PercentageTrendIndicator({
|
||||
trend,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -17,24 +17,6 @@ export interface NetWorthEvolutionData {
|
|||
}
|
||||
|
||||
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;
|
||||
accounts: Array<
|
||||
Account & {
|
||||
|
|
@ -52,13 +34,6 @@ export interface DashboardData {
|
|||
|
||||
export function useDashboardData(): DashboardData {
|
||||
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: {} },
|
||||
accounts: [],
|
||||
topCategories: [],
|
||||
|
|
@ -72,7 +47,6 @@ export function useDashboardData(): DashboardData {
|
|||
const now = new Date();
|
||||
const to = format(now, 'yyyy-MM-dd');
|
||||
|
||||
// Last 12 months for evolution charts
|
||||
const from12Months = format(subMonths(now, 12), 'yyyy-MM-dd');
|
||||
const params12Months = new URLSearchParams({
|
||||
from: from12Months,
|
||||
|
|
@ -80,7 +54,6 @@ export function useDashboardData(): DashboardData {
|
|||
});
|
||||
const query12Months = `?${params12Months.toString()}`;
|
||||
|
||||
// Last 30 days for top blocks and categories
|
||||
const from30Days = format(subDays(now, 30), 'yyyy-MM-dd');
|
||||
const params30Days = new URLSearchParams({
|
||||
from: from30Days,
|
||||
|
|
@ -88,50 +61,20 @@ export function useDashboardData(): DashboardData {
|
|||
});
|
||||
const query30Days = `?${params30Days.toString()}`;
|
||||
|
||||
const [
|
||||
netWorth,
|
||||
monthlySpending,
|
||||
cashFlow,
|
||||
netWorthEvolution,
|
||||
accountBalances,
|
||||
topCategories,
|
||||
] = await Promise.all([
|
||||
fetch(`/api/dashboard/net-worth${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(),
|
||||
),
|
||||
]);
|
||||
const [netWorthEvolution, accountBalances, topCategories] =
|
||||
await Promise.all([
|
||||
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({
|
||||
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 as NetWorthEvolutionData,
|
||||
accounts: accountBalances.map(
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
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 { SpendingSummaryCard } from '@/components/dashboard/spending-summary-card';
|
||||
import { TopCategoriesCard } from '@/components/dashboard/top-categories-card';
|
||||
import HeadingSmall from '@/components/heading-small';
|
||||
import { useDashboardData } from '@/hooks/use-dashboard-data';
|
||||
|
|
@ -20,9 +17,6 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||
|
||||
export default function Dashboard() {
|
||||
const {
|
||||
netWorth,
|
||||
monthlySpending,
|
||||
cashFlow,
|
||||
netWorthEvolution,
|
||||
accounts: accountMetrics,
|
||||
topCategories,
|
||||
|
|
@ -39,25 +33,6 @@ export default function Dashboard() {
|
|||
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">
|
||||
<NetWorthChartComponent
|
||||
data={netWorthEvolution}
|
||||
|
|
|
|||
Loading…
Reference in New Issue