Revamp dashboard: YTD default, expenses chart, budget table

- Default period changed from month to year-to-date
- Remove recent transactions section
- Add expenses over time stacked bar chart (by category/month)
- Add budget vs actual table (current month)
- Reorganize layout: cards, pie + budget table, full-width chart

Closes #15

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-03-07 09:15:04 -05:00
parent 861d78eca2
commit 0bbbcc541b
4 changed files with 45 additions and 17 deletions

View file

@ -3,18 +3,21 @@ import type {
DashboardPeriod, DashboardPeriod,
DashboardSummary, DashboardSummary,
CategoryBreakdownItem, CategoryBreakdownItem,
RecentTransaction, CategoryOverTimeData,
BudgetVsActualRow,
} from "../shared/types"; } from "../shared/types";
import { import {
getDashboardSummary, getDashboardSummary,
getExpensesByCategory, getExpensesByCategory,
getRecentTransactions,
} from "../services/dashboardService"; } from "../services/dashboardService";
import { getCategoryOverTime } from "../services/reportService";
import { getBudgetVsActualData } from "../services/budgetService";
interface DashboardState { interface DashboardState {
summary: DashboardSummary; summary: DashboardSummary;
categoryBreakdown: CategoryBreakdownItem[]; categoryBreakdown: CategoryBreakdownItem[];
recentTransactions: RecentTransaction[]; categoryOverTime: CategoryOverTimeData;
budgetVsActual: BudgetVsActualRow[];
period: DashboardPeriod; period: DashboardPeriod;
customDateFrom: string; customDateFrom: string;
customDateTo: string; customDateTo: string;
@ -30,7 +33,8 @@ type DashboardAction =
payload: { payload: {
summary: DashboardSummary; summary: DashboardSummary;
categoryBreakdown: CategoryBreakdownItem[]; categoryBreakdown: CategoryBreakdownItem[];
recentTransactions: RecentTransaction[]; categoryOverTime: CategoryOverTimeData;
budgetVsActual: BudgetVsActualRow[];
}; };
} }
| { type: "SET_PERIOD"; payload: DashboardPeriod } | { type: "SET_PERIOD"; payload: DashboardPeriod }
@ -38,14 +42,15 @@ type DashboardAction =
const now = new Date(); const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
const monthStartStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`; const yearStartStr = `${now.getFullYear()}-01-01`;
const initialState: DashboardState = { const initialState: DashboardState = {
summary: { totalCount: 0, totalAmount: 0, incomeTotal: 0, expenseTotal: 0 }, summary: { totalCount: 0, totalAmount: 0, incomeTotal: 0, expenseTotal: 0 },
categoryBreakdown: [], categoryBreakdown: [],
recentTransactions: [], categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
period: "month", budgetVsActual: [],
customDateFrom: monthStartStr, period: "year",
customDateFrom: yearStartStr,
customDateTo: todayStr, customDateTo: todayStr,
isLoading: false, isLoading: false,
error: null, error: null,
@ -62,7 +67,8 @@ function reducer(state: DashboardState, action: DashboardAction): DashboardState
...state, ...state,
summary: action.payload.summary, summary: action.payload.summary,
categoryBreakdown: action.payload.categoryBreakdown, categoryBreakdown: action.payload.categoryBreakdown,
recentTransactions: action.payload.recentTransactions, categoryOverTime: action.payload.categoryOverTime,
budgetVsActual: action.payload.budgetVsActual,
isLoading: false, isLoading: false,
}; };
case "SET_PERIOD": case "SET_PERIOD":
@ -129,14 +135,17 @@ export function useDashboard() {
try { try {
const { dateFrom, dateTo } = computeDateRange(period, customFrom, customTo); const { dateFrom, dateTo } = computeDateRange(period, customFrom, customTo);
const [summary, categoryBreakdown, recentTransactions] = await Promise.all([ const currentMonth = new Date().getMonth() + 1;
const currentYear = new Date().getFullYear();
const [summary, categoryBreakdown, categoryOverTime, budgetVsActual] = await Promise.all([
getDashboardSummary(dateFrom, dateTo), getDashboardSummary(dateFrom, dateTo),
getExpensesByCategory(dateFrom, dateTo), getExpensesByCategory(dateFrom, dateTo),
getRecentTransactions(10), getCategoryOverTime(dateFrom, dateTo),
getBudgetVsActualData(currentYear, currentMonth),
]); ]);
if (fetchId !== fetchIdRef.current) return; if (fetchId !== fetchIdRef.current) return;
dispatch({ type: "SET_DATA", payload: { summary, categoryBreakdown, recentTransactions } }); dispatch({ type: "SET_DATA", payload: { summary, categoryBreakdown, categoryOverTime, budgetVsActual } });
} catch (e) { } catch (e) {
if (fetchId !== fetchIdRef.current) return; if (fetchId !== fetchIdRef.current) return;
dispatch({ dispatch({

View file

@ -26,6 +26,8 @@
"noData": "No data available. Start by importing your bank statements.", "noData": "No data available. Start by importing your bank statements.",
"expensesByCategory": "Expenses by Category", "expensesByCategory": "Expenses by Category",
"recentTransactions": "Recent Transactions", "recentTransactions": "Recent Transactions",
"budgetVsActual": "Budget vs Actual",
"expensesOverTime": "Expenses Over Time",
"period": { "period": {
"month": "This month", "month": "This month",
"3months": "3 months", "3months": "3 months",

View file

@ -26,6 +26,8 @@
"noData": "Aucune donnée disponible. Commencez par importer vos relevés bancaires.", "noData": "Aucune donnée disponible. Commencez par importer vos relevés bancaires.",
"expensesByCategory": "Dépenses par catégorie", "expensesByCategory": "Dépenses par catégorie",
"recentTransactions": "Transactions récentes", "recentTransactions": "Transactions récentes",
"budgetVsActual": "Budget vs Réel",
"expensesOverTime": "Dépenses dans le temps",
"period": { "period": {
"month": "Ce mois", "month": "Ce mois",
"3months": "3 mois", "3months": "3 mois",

View file

@ -5,7 +5,8 @@ import { useDashboard } from "../hooks/useDashboard";
import { PageHelp } from "../components/shared/PageHelp"; import { PageHelp } from "../components/shared/PageHelp";
import PeriodSelector from "../components/dashboard/PeriodSelector"; import PeriodSelector from "../components/dashboard/PeriodSelector";
import CategoryPieChart from "../components/dashboard/CategoryPieChart"; import CategoryPieChart from "../components/dashboard/CategoryPieChart";
import RecentTransactionsList from "../components/dashboard/RecentTransactionsList"; import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart";
import BudgetVsActualTable from "../components/reports/BudgetVsActualTable";
import TransactionDetailModal from "../components/shared/TransactionDetailModal"; import TransactionDetailModal from "../components/shared/TransactionDetailModal";
import type { CategoryBreakdownItem, DashboardPeriod } from "../shared/types"; import type { CategoryBreakdownItem, DashboardPeriod } from "../shared/types";
@ -41,7 +42,7 @@ function computeDateRange(
export default function DashboardPage() { export default function DashboardPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { state, setPeriod, setCustomDates } = useDashboard(); const { state, setPeriod, setCustomDates } = useDashboard();
const { summary, categoryBreakdown, recentTransactions, period, isLoading } = state; const { summary, categoryBreakdown, categoryOverTime, budgetVsActual, period, isLoading } = state;
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set()); const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set());
const [detailModal, setDetailModal] = useState<CategoryBreakdownItem | null>(null); const [detailModal, setDetailModal] = useState<CategoryBreakdownItem | null>(null);
@ -108,7 +109,7 @@ export default function DashboardPage() {
/> />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{cards.map((card) => ( {cards.map((card) => (
<div <div
key={card.labelKey} key={card.labelKey}
@ -125,7 +126,7 @@ export default function DashboardPage() {
))} ))}
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
<CategoryPieChart <CategoryPieChart
data={categoryBreakdown} data={categoryBreakdown}
hiddenCategories={hiddenCategories} hiddenCategories={hiddenCategories}
@ -133,7 +134,21 @@ export default function DashboardPage() {
onShowAll={showAll} onShowAll={showAll}
onViewDetails={viewDetails} onViewDetails={viewDetails}
/> />
<RecentTransactionsList transactions={recentTransactions} /> <div>
<h2 className="text-lg font-semibold mb-3">{t("dashboard.budgetVsActual")}</h2>
<BudgetVsActualTable data={budgetVsActual} />
</div>
</div>
<div className="mb-6">
<h2 className="text-lg font-semibold mb-3">{t("dashboard.expensesOverTime")}</h2>
<CategoryOverTimeChart
data={categoryOverTime}
hiddenCategories={hiddenCategories}
onToggleHidden={toggleHidden}
onShowAll={showAll}
onViewDetails={viewDetails}
/>
</div> </div>
{detailModal && ( {detailModal && (