diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 1e5cecf..d06738a 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -11,13 +11,21 @@ import { } from "@mui/material"; import ConfigurableDashboard from "./components/Dashboard"; +import { DashboardState } from "./components/Dashboard/Dashboard.models"; import { configuration } from "./dashboard-config"; import { useReport, prepareReport, } from "./features/report"; +/** Map the internal UI mode to the API flow param */ +function modeToFlow(mode: "expense" | "income"): "outflows" | "inflows" { + return mode === "expense" ? "outflows" : "inflows"; +} + export default function Dashboard() { + const [mode, setMode] = React.useState<"expense" | "income">("expense"); + const [appliedPayees, setAppliedPayees] = React.useState([]); const [appliedTags, setAppliedTags] = React.useState([]); @@ -28,10 +36,8 @@ export default function Dashboard() { const [loadedTags, setLoadedTags] = React.useState([]); const report = useReport({ - periods: ["weekly", "monthly", "full"], - rolling: true, - include_transactions: true, - group_by: ["tags"], + periods: ["weekly", "monthly", "all"], + flow: modeToFlow(mode), payee: appliedPayees.length > 0 ? appliedPayees : undefined, tags: appliedTags.length > 0 ? appliedTags : undefined, }); @@ -43,10 +49,7 @@ export default function Dashboard() { report.data.data.buckets.forEach((b: any) => { Object.values(b.periods).forEach((periodArray: any) => { periodArray?.forEach((p: any) => { - p.expenses?.transactions?.forEach((t: any) => { - if (t.payee?.name) pSet.add(t.payee.name); - }); - p.incomes?.transactions?.forEach((t: any) => { + p.metric?.transactions?.forEach((t: any) => { if (t.payee?.name) pSet.add(t.payee.name); }); }); @@ -60,10 +63,7 @@ export default function Dashboard() { report.data.data.buckets.forEach((b: any) => { Object.values(b.periods).forEach((periodArray: any) => { periodArray?.forEach((p: any) => { - p.expenses?.transactions?.forEach((t: any) => { - t.tags?.forEach((tag: any) => tSet.add(tag.name || tag)); - }); - p.incomes?.transactions?.forEach((t: any) => { + p.metric?.transactions?.forEach((t: any) => { t.tags?.forEach((tag: any) => tSet.add(tag.name || tag)); }); }); @@ -77,6 +77,10 @@ export default function Dashboard() { const isLoading = report.isLoading; const error = report.error; + /** Callback for the ConfigurableDashboard's mode toggle */ + const handleModeChange = React.useCallback((newState: DashboardState) => { + setMode(newState.mode); + }, []); if (isLoading && !report.data) { return ( @@ -152,7 +156,7 @@ export default function Dashboard() { setAppliedTags(tagsInput); }} disabled={isLoading} - sx={{ height: 40, borderRadius: 2 }} // Changed from 56 to 40 to match minHeight of inputs + sx={{ height: 40, borderRadius: 2 }} > Apply @@ -161,6 +165,7 @@ export default function Dashboard() { ); diff --git a/src/components/Dashboard/Dashboard.models.ts b/src/components/Dashboard/Dashboard.models.ts index 58b828d..7032355 100644 --- a/src/components/Dashboard/Dashboard.models.ts +++ b/src/components/Dashboard/Dashboard.models.ts @@ -50,4 +50,5 @@ export interface DashboardConfig { export interface DashboardProps { config: DashboardConfig; data: ReportData; + onModeChange?: (state: DashboardState) => void; } diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx index e7b6cbb..66b678d 100644 --- a/src/components/Dashboard/Dashboard.tsx +++ b/src/components/Dashboard/Dashboard.tsx @@ -12,10 +12,14 @@ export default function Dashboard(props: DashboardProps) { }); const toggleMode = () => { - setState(prev => ({ - ...prev, - mode: prev.mode === "expense" ? "income" : "expense", - })); + setState(prev => { + const next = { + ...prev, + mode: prev.mode === "expense" ? "income" as const : "expense" as const, + }; + props.onModeChange?.(next); + return next; + }); }; const togglePeriodType = () => { diff --git a/src/components/HistoryChart/HistoryChart.adapter.ts b/src/components/HistoryChart/HistoryChart.adapter.ts index 8f17dca..4e10bc9 100644 --- a/src/components/HistoryChart/HistoryChart.adapter.ts +++ b/src/components/HistoryChart/HistoryChart.adapter.ts @@ -9,15 +9,14 @@ import { ChartDataPoint } from "./HistoryChart.models"; // ─── Tab → PeriodKey ───────────────────────────────────────── const TAB_TO_KEY: Record = { + Daily: "daily", Weekly: "weekly", Monthly: "monthly", - Yearly: "yearly", - "Financial Year": "fyly", - "All Time": "full", + "All Time": "all", }; export function tabToKey(tab: string): PeriodKey { - return TAB_TO_KEY[tab] ?? "full"; + return TAB_TO_KEY[tab] ?? "all"; } // ─── Comparison ────────────────────────────────────────────── @@ -27,10 +26,9 @@ function attachComparison( key: PeriodKey ): ChartDataPoint[] { const getCompareIndex = (i: number) => { + if (key === "daily") return i - 7; if (key === "weekly") return i - 4; if (key === "monthly") return i - 12; - if (key === "yearly") return i - 1; - if (key === "fyly") return i - 1; return -1; }; @@ -64,7 +62,7 @@ export function buildChartData( let points: ChartDataPoint[] = merged.map((p) => ({ id: p.id, label: p.label, - amount: getAmount(p, mode), + amount: getAmount(p), })); if (comparison) { diff --git a/src/components/HistoryChart/HistoryChart.tsx b/src/components/HistoryChart/HistoryChart.tsx index 09f8124..28584b8 100644 --- a/src/components/HistoryChart/HistoryChart.tsx +++ b/src/components/HistoryChart/HistoryChart.tsx @@ -35,11 +35,10 @@ export default function HistoryChart(props: HistoryChartProps) { : 1; const visibleCountMap = { + daily: 7, weekly: 6, monthly: 4, - yearly: 4, - fyly: 4, - full: 4, + all: 4, }; const visibleCount = visibleCountMap[activeDataKey] ?? 4; diff --git a/src/components/LatestItems/LatestItems.adapter.ts b/src/components/LatestItems/LatestItems.adapter.ts index d8236ee..998a304 100644 --- a/src/components/LatestItems/LatestItems.adapter.ts +++ b/src/components/LatestItems/LatestItems.adapter.ts @@ -13,7 +13,6 @@ function extractTransactions( reportData: ReportData, selectedPeriodId: string | null, selectedGroupKey: GroupKey | null, - mode: "expense" | "income" ): Transaction[] { const buckets = filterBuckets(reportData.buckets, selectedGroupKey); if (selectedPeriodId) { @@ -23,20 +22,16 @@ function extractTransactions( if (!selected) return []; - return mode === "expense" - ? (selected.expenses.transactions || []) - : (selected.incomes.transactions || []); + return selected.metric.transactions || []; } - const periods = mergeBucketPeriods(buckets, "full"); + const periods = mergeBucketPeriods(buckets, "all"); if (!periods.length) return []; const full = periods[0]; - return mode === "expense" - ? (full.expenses.transactions || []) - : (full.incomes.transactions || []); + return full.metric.transactions || []; } // ─── Main adapter ──────────────────────────────────────────── @@ -47,10 +42,9 @@ export function buildLatestItems( selectedGroupKey: GroupKey | null, mode: "expense" | "income" ): LatestItem[] { - const txns = extractTransactions(reportData, selectedPeriodId, selectedGroupKey, mode); + const txns = extractTransactions(reportData, selectedPeriodId, selectedGroupKey); return txns - .filter((t) => (mode === "expense" ? t.amount < 0 : t.amount >= 0)) .sort( (a, b) => new Date(b.occurred_at).getTime() - diff --git a/src/components/ProgressCard/TopTags.adapter.ts b/src/components/ProgressCard/TopTags.adapter.ts index ea0eb63..d4fca18 100644 --- a/src/components/ProgressCard/TopTags.adapter.ts +++ b/src/components/ProgressCard/TopTags.adapter.ts @@ -43,17 +43,17 @@ export function extractTopTags( const tags = bucket.group_key.tags; if (!tags || tags.length === 0) continue; - // Prefer FULL if available - const fullPeriods = (bucket.periods.full || []) as DecoratedPeriod[]; + // Prefer ALL if available + const allPeriods = (bucket.periods.all || []) as DecoratedPeriod[]; const periodsToUse = selectedPeriodId ? (Object.values(bucket.periods).flat() as DecoratedPeriod[]) - : fullPeriods; + : allPeriods; const period = findPeriod(periodsToUse, selectedPeriodId); if (!period) continue; - const amount = getAmount(period, mode); + const amount = getAmount(period); for (const tag of tags) { tagMap.set(tag, (tagMap.get(tag) || 0) + amount); diff --git a/src/components/report.helpers.ts b/src/components/report.helpers.ts index b77c3ac..0b21725 100644 --- a/src/components/report.helpers.ts +++ b/src/components/report.helpers.ts @@ -2,11 +2,12 @@ import { ReportPeriod, ReportBucket, GroupKey, + PeriodType, } from "../features/report"; // ─── Types ──────────────────────────────────────────────────── -export type PeriodKey = "weekly" | "monthly" | "yearly" | "fyly" | "full"; +export type PeriodKey = PeriodType; export type DecoratedPeriod = ReportPeriod & { id: string; @@ -16,11 +17,10 @@ export type DecoratedPeriod = ReportPeriod & { // ─── Period helpers ─────────────────────────────────────────── const PREFIX_TO_KEY: Record = { + D: "daily", W: "weekly", M: "monthly", - Y: "yearly", - FY: "fyly", - FULL: "full", + ALL: "all", }; /** @@ -29,19 +29,16 @@ const PREFIX_TO_KEY: Record = { */ export function periodIdToKey(periodId: string): PeriodKey { const prefix = periodId.split(":")[0]; - return PREFIX_TO_KEY[prefix] ?? "full"; + return PREFIX_TO_KEY[prefix] ?? "all"; } // ─── Metric helpers ─────────────────────────────────────────── -export function getAmount( - period: ReportPeriod, - mode: "expense" | "income" -): number { - return mode === "expense" ? period.expenses.sum : period.incomes.sum; +export function getAmount(period: ReportPeriod): number { + return period.metric.sum; } -function mergeMetric(a: ReportPeriod["expenses"], b: ReportPeriod["expenses"]) { +function mergeMetric(a: ReportPeriod["metric"], b: ReportPeriod["metric"]) { const sum = a.sum + b.sum; const count = a.count + b.count; @@ -78,14 +75,12 @@ export function mergeBucketPeriods( if (!existing) { map.set(p.id, { ...p, - expenses: { ...p.expenses }, - incomes: { ...p.incomes }, + metric: { ...p.metric }, }); } else { map.set(p.id, { ...existing, - expenses: mergeMetric(existing.expenses, p.expenses), - incomes: mergeMetric(existing.incomes, p.incomes), + metric: mergeMetric(existing.metric, p.metric), }); } } @@ -126,7 +121,7 @@ export function matchesGroupKey( selected: GroupKey ): boolean { for (const [dim, values] of Object.entries(selected)) { - const bucketValues = bucket.group_key[dim as keyof GroupKey]; + const bucketValues = bucket.group_key[dim]; if (!bucketValues) return false; if (!(values as string[]).every((v) => bucketValues.includes(v))) return false; diff --git a/src/features/report/index.ts b/src/features/report/index.ts index 69e51e3..851610e 100644 --- a/src/features/report/index.ts +++ b/src/features/report/index.ts @@ -6,7 +6,9 @@ export type { ReportData, ReportBucket, ReportPeriod, + ReportQuery, GroupKey, + PeriodType, } from './report.models' export { prepareReport diff --git a/src/features/report/report.models.ts b/src/features/report/report.models.ts index e393fdb..4f3ee20 100644 --- a/src/features/report/report.models.ts +++ b/src/features/report/report.models.ts @@ -1,29 +1,40 @@ export interface Payor { + id?: string; name: string; + username: string; + email: string; } export interface Payee { + type: "merchant" | "person" | "transfer" | "other"; name: string; } export interface Account { + id: string; name: string; number: string; + type: "cash" | "bank" | "credit_card" | "wallet" | "other"; + currency: string; + is_active?: boolean; } export interface Tag { + id: string; name: string; icon: string; - description: string; + parent_id?: string | null; } export interface Transaction { + id: string; payor: Payor; payee: Payee; amount: number; account: Account; tags: Tag[]; - occurred_at: Date; + occurred_at: string; + created_at: string; } // ----------------------------- @@ -41,12 +52,12 @@ export interface ReportMetric { // Period // ----------------------------- -export interface ReportPeriod { - start: Date; - end: Date; +export type PeriodType = "daily" | "weekly" | "monthly" | "all"; - expenses: ReportMetric; - incomes: ReportMetric; +export interface ReportPeriod { + start: string; + end: string; + metric: ReportMetric; } // ----------------------------- @@ -54,46 +65,48 @@ export interface ReportPeriod { // ----------------------------- export type GroupKey = { - payee?: string[]; - tags?: string[]; - flow?: string[]; + [dimension: string]: string[]; }; export interface ReportBucket { group_key: GroupKey; periods: { + daily?: ReportPeriod[]; weekly?: ReportPeriod[]; monthly?: ReportPeriod[]; - yearly?: ReportPeriod[]; - fyly?: ReportPeriod[]; - full?: ReportPeriod[]; + all?: ReportPeriod[]; }; } +// ----------------------------- +// Report Query +// ----------------------------- + +export interface ReportQuery { + accounts?: string[] | null; + ignore_self?: boolean | null; + start_date?: string | null; + end_date?: string | null; + min_amount?: number | null; + max_amount?: number | null; +} + // ----------------------------- // Final Report // ----------------------------- export interface ReportData { - periods: ("weekly" | "monthly" | "yearly" | "fyly" | "full")[]; + snapshot_id?: string | null; - rolling: boolean; - report_date?: string; + flow?: "inflows" | "outflows" | null; - group_by: ("payee" | "tags")[]; + periods: PeriodType[]; - ignore_self: boolean; - include_transactions: boolean; - - start_date?: string | null; - end_date?: string | null; - flow?: "expense" | "income" | null; - payee?: string[] | null; - account?: string[] | null; tags?: string[] | null; - min_amount?: number | null; - max_amount?: number | null; + payee?: string[] | null; buckets: ReportBucket[]; + + query: ReportQuery; } diff --git a/src/features/report/report.utils.ts b/src/features/report/report.utils.ts index 10e4b9a..81dd2a5 100644 --- a/src/features/report/report.utils.ts +++ b/src/features/report/report.utils.ts @@ -1,6 +1,7 @@ import { ReportData, - ReportPeriod + ReportPeriod, + PeriodType, } from "./report.models"; /* ---------- ID BUILDING ---------- */ @@ -13,7 +14,7 @@ function formatDate(d: Date): string { } function buildPeriodId( - type: "weekly" | "monthly" | "yearly" | "fyly" | "full", + type: PeriodType, start: Date, end: Date ): string { @@ -21,16 +22,14 @@ function buildPeriodId( const e = formatDate(end); switch (type) { + case "daily": + return `D:${s}_${e}`; case "weekly": return `W:${s}_${e}`; case "monthly": return `M:${s}_${e}`; - case "yearly": - return `Y:${s}_${e}`; - case "fyly": - return `FY:${s}_${e}`; - case "full": - return `FULL:${s}_${e}`; + case "all": + return `ALL:${s}_${e}`; default: return `${s}_${e}`; } @@ -60,19 +59,15 @@ const yearFmt = new Intl.DateTimeFormat("en-GB", { timeZone: "UTC", }); -function sameMonth(a: Date, b: Date) { - return ( - a.getUTCFullYear() === b.getUTCFullYear() && - a.getUTCMonth() === b.getUTCMonth() - ); -} - function buildLabel( - type: "weekly" | "monthly" | "yearly" | "fyly" | "full", + type: PeriodType, start: Date, end: Date ): string { switch (type) { + case "daily": + return dayFmt.format(start); + case "weekly": { const sDay = start.getUTCDate(); const m = monthFmt.format(start); @@ -82,15 +77,6 @@ function buildLabel( case "monthly": return `${monthFmt.format(start)} ${yearFmt.format(start)}`; - case "yearly": - return yearFmt.format(start); - - case "fyly": { - const startY = start.getUTCFullYear(); - const endY = end.getUTCFullYear(); - return `FY ${startY}–${String(endY).slice(-2)}`; - } - default: return `${monthDayFmt.format(start)} - ${monthDayFmt.format(end)}`; } @@ -99,7 +85,7 @@ function buildLabel( /* ---------- MAIN ---------- */ function decoratePeriods( - type: "weekly" | "monthly" | "yearly" | "fyly" | "full", + type: PeriodType, periods: ReportPeriod[] ): (ReportPeriod & { id: string; label: string })[] { return periods.map((p) => ({ diff --git a/src/features/report/useReport.ts b/src/features/report/useReport.ts index 3c8ec74..1ffea4d 100644 --- a/src/features/report/useReport.ts +++ b/src/features/report/useReport.ts @@ -1,20 +1,11 @@ import { useResourceByName } from "../../../react-openapi"; export interface ReportParams { - periods?: ("weekly" | "monthly" | "yearly" | "fyly" | "full")[]; - rolling?: boolean; - report_date?: string; - group_by?: ("payee" | "tags")[]; - ignore_self?: boolean; - include_transactions?: boolean; - start_date?: string; - end_date?: string; - flow?: "expense" | "income"; + snapshot_id?: string; + periods?: ("daily" | "weekly" | "monthly" | "all")[]; + flow?: "inflows" | "outflows"; payee?: string[]; - account?: string[]; tags?: string[]; - min_amount?: number; - max_amount?: number; } export function useReport(params: ReportParams) { @@ -23,6 +14,5 @@ export function useReport(params: ReportParams) { return useList({ ...params, periods: params.periods, - group_by: params.group_by, }); }