14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-dashboard-charts-and-layout | 03 | execute | 2 |
|
|
true |
|
|
Purpose: This is the integration plan that ties together the month navigation (Plan 01) and chart components (Plan 02) into the refactored dashboard. Replaces the existing flat pie chart and progress bars with the 3-column chart grid, adds URL-based month navigation, and updates the loading skeleton.
Output: Refactored DashboardPage.tsx and updated DashboardSkeleton.tsx.
<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-dashboard-charts-and-layout/02-CONTEXT.md @.planning/phases/02-dashboard-charts-and-layout/02-RESEARCH.md @.planning/phases/02-dashboard-charts-and-layout/02-01-SUMMARY.md @.planning/phases/02-dashboard-charts-and-layout/02-02-SUMMARY.mdFrom src/hooks/useMonthParam.ts (Plan 01):
export function useMonthParam(): {
month: string // "YYYY-MM"
setMonth: (newMonth: string) => void
navigateMonth: (delta: number) => void
}
From src/components/dashboard/MonthNavigator.tsx (Plan 01):
interface MonthNavigatorProps {
availableMonths: string[] // "YYYY-MM"[]
t: (key: string) => string
}
export function MonthNavigator({ availableMonths, t }: MonthNavigatorProps): JSX.Element
From src/components/dashboard/charts/ChartEmptyState.tsx (Plan 01):
export function ChartEmptyState({ message, className }: { message: string; className?: string }): JSX.Element
From src/components/dashboard/charts/ExpenseDonutChart.tsx (Plan 02):
interface ExpenseDonutChartProps {
data: Array<{ type: string; value: number; label: string }>
totalExpenses: number
currency: string
emptyMessage: string
}
export function ExpenseDonutChart(props: ExpenseDonutChartProps): JSX.Element
From src/components/dashboard/charts/IncomeBarChart.tsx (Plan 02):
interface IncomeBarChartProps {
data: Array<{ label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
export function IncomeBarChart(props: IncomeBarChartProps): JSX.Element
From src/components/dashboard/charts/SpendBarChart.tsx (Plan 02):
interface SpendBarChartProps {
data: Array<{ type: string; label: string; budgeted: number; actual: number }>
currency: string
emptyMessage: string
}
export function SpendBarChart(props: SpendBarChartProps): JSX.Element
From src/components/shared/PageShell.tsx:
export function PageShell({ title, description, action, children }: PageShellProps): JSX.Element
// action slot renders top-right, ideal for MonthNavigator
From src/hooks/useBudgets.ts:
export function useBudgets(): { budgets: Budget[]; loading: boolean; createBudget: UseMutationResult; generateFromTemplate: UseMutationResult; ... }
export function useBudgetDetail(id: string): { budget: Budget | null; items: BudgetItem[]; loading: boolean }
From src/lib/types.ts:
export type CategoryType = "income" | "bill" | "variable_expense" | "debt" | "saving" | "investment"
export interface Budget { id: string; start_date: string; end_date: string; currency: string; carryover_amount: number; ... }
export interface BudgetItem { id: string; budget_id: string; category_id: string; budgeted_amount: number; actual_amount: number; category?: Category; ... }
DashboardPage (outer component):
- Remove the hardcoded
currentMonthStarthelper and thenow/year/month/monthPrefixdate logic - Import
useMonthParamfrom@/hooks/useMonthParam - Import
MonthNavigatorfrom@/components/dashboard/MonthNavigator - Call
const { month } = useMonthParam()to get the selected month asYYYY-MM - Call
const { budgets, loading } = useBudgets() - Derive
availableMonthsfrom budgets:useMemo(() => budgets.map(b => b.start_date.slice(0, 7)), [budgets])-- array ofYYYY-MMstrings - Find current budget:
useMemo(() => budgets.find(b => b.start_date.startsWith(month)), [budgets, month])-- usesstartsWithprefix matching (per Pitfall 7) - Pass
MonthNavigatorinto PageShellactionslot:<PageShell title={t("dashboard.title")} action={<MonthNavigator availableMonths={availableMonths} t={t} />}> - Loading state: show
DashboardSkeletoninside PageShell (same as current) - No budget for month: show empty prompt with
t("dashboard.noBudgetForMonth")text and two buttons:- "Create Budget" button calling
createBudget.mutate({ month: parsedMonth, year: parsedYear, currency: "EUR" }) - "Generate from Template" button calling
generateFromTemplate.mutate({ month: parsedMonth, year: parsedYear, currency: "EUR" }) - Parse month/year from the
monthstring (split on "-") - Per user decision: empty prompt when navigating to month with no budget, with create/generate option
- "Create Budget" button calling
- When budget exists: render
<DashboardContent budgetId={currentBudget.id} />
DashboardContent (inner component):
-
Keep
useBudgetDetail(budgetId)call and the loading/null guards -
Keep existing derived totals logic (totalIncome, totalExpenses, availableBalance, budgetedIncome, budgetedExpenses)
-
Memoize all derived data with
useMemo(wrap existing reduce operations) -
Derive pieData (same as existing but memoized):
const pieData = useMemo(() =>
EXPENSE_TYPES.map(type => {
const total = items.filter(i => i.category?.type === type).reduce((sum, i) => sum + i.actual_amount, 0)
return { type, value: total, label: t(`categories.types.${type}`) }
}).filter(d => d.value > 0),
[items, t])
- Derive incomeBarData (NEW):
const incomeBarData = useMemo(() => {
const budgeted = items.filter(i => i.category?.type === "income").reduce((sum, i) => sum + i.budgeted_amount, 0)
const actual = items.filter(i => i.category?.type === "income").reduce((sum, i) => sum + i.actual_amount, 0)
if (budgeted === 0 && actual === 0) return []
return [{ label: t("categories.types.income"), budgeted, actual }]
}, [items, t])
- Derive spendBarData (NEW):
const spendBarData = useMemo(() =>
EXPENSE_TYPES.map(type => {
const groupItems = items.filter(i => i.category?.type === type)
if (groupItems.length === 0) return null
const budgeted = groupItems.reduce((sum, i) => sum + i.budgeted_amount, 0)
const actual = groupItems.reduce((sum, i) => sum + i.actual_amount, 0)
return { type, label: t(`categories.types.${type}`), budgeted, actual }
}).filter(Boolean) as Array<{ type: string; label: string; budgeted: number; actual: number }>,
[items, t])
-
Layout (JSX): Replace the entire existing chart/progress section with:
SummaryStrip(same as current -- first row)- Chart row:
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">containing threeCardwrappers:- Card 1:
<Card><CardHeader><CardTitle className="text-base">{t("dashboard.expenseDonut")}</CardTitle></CardHeader><CardContent><ExpenseDonutChart data={pieData} totalExpenses={totalExpenses} currency={currency} emptyMessage={t("dashboard.noData")} /></CardContent></Card> - Card 2:
<Card><CardHeader><CardTitle className="text-base">{t("dashboard.incomeChart")}</CardTitle></CardHeader><CardContent><IncomeBarChart data={incomeBarData} currency={currency} emptyMessage={t("dashboard.noData")} /></CardContent></Card> - Card 3:
<Card><CardHeader><CardTitle className="text-base">{t("dashboard.spendChart")}</CardTitle></CardHeader><CardContent><SpendBarChart data={spendBarData} currency={currency} emptyMessage={t("dashboard.noData")} /></CardContent></Card>
- Card 1:
QuickAddPickerrow (moved below charts per user decision)
-
Remove: The old
PieChart,Pie,Cell,ResponsiveContainer,Tooltipimports from recharts (replaced by chart components). Remove the oldprogressGroupsderivation. Remove the old 2-column grid layout. Remove old inline pie chart and progress bar JSX. -
Keep: EXPENSE_TYPES constant (still used for data derivation), all SummaryStrip logic, QuickAddPicker integration. cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build DashboardPage uses URL search params for month selection, MonthNavigator in PageShell action slot, and a 3-column chart grid (donut, vertical bar, horizontal bar) replacing the old pie chart + progress bars. Empty month prompt shows create/generate buttons. Build passes.
Changes:
- Keep the 3-card summary skeleton row unchanged:
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">with 3SkeletonStatCardcomponents - Replace the 2-column chart skeleton with a 3-column grid:
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> - Each chart skeleton card:
<Card><CardHeader><Skeleton className="h-5 w-40" /></CardHeader><CardContent><Skeleton className="h-[250px] w-full rounded-md" /></CardContent></Card> - Three skeleton chart cards (matching the donut, bar, bar layout)
- Keep the
SkeletonStatCardhelper component unchanged cd /home/jlmak/Projects/jlmak/SimpleFinanceDash && bun run build DashboardSkeleton mirrors the new 3-column chart layout with 3 skeleton chart cards. Build passes.
<success_criteria>
- User can navigate between months using prev/next arrows and month dropdown
- Month is stored in URL search params (
?month=YYYY-MM) - Dashboard shows SummaryStrip, then 3-column chart row, then QuickAdd
- Charts and summary cards update when month changes
- Empty month shows create/generate prompt
- DashboardSkeleton mirrors new layout
bun run build && bun run lintpasses </success_criteria>