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 { 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
);
|
||||||
])} />;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' : ''
|
||||||
|
|
|
||||||
|
|
@ -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({
|
export function PercentageTrendIndicator({
|
||||||
trend,
|
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 {
|
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(
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue