From 15f76eb5f0e5e90ec34465c0233d3252500c0021 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Wed, 6 May 2026 14:11:03 +0530 Subject: [PATCH] new report structure --- src/Dashboard.tsx | 18 +- src/components/Dashboard/Dashboard.models.ts | 9 +- src/components/Dashboard/Dashboard.view.tsx | 75 ++------ src/features/dashboard/dashboard.mapper.ts | 191 ------------------- src/features/dashboard/index.ts | 3 - src/features/dashboard/useDashboardData.ts | 44 ----- src/features/report/index.ts | 3 - src/features/report/report.models.ts | 84 +++++--- src/features/report/report.utils.ts | 45 +++-- src/features/report/useReport.ts | 16 +- 10 files changed, 138 insertions(+), 350 deletions(-) delete mode 100644 src/features/dashboard/dashboard.mapper.ts delete mode 100644 src/features/dashboard/index.ts delete mode 100644 src/features/dashboard/useDashboardData.ts diff --git a/src/Dashboard.tsx b/src/Dashboard.tsx index 7d93b4b..d5a98a1 100644 --- a/src/Dashboard.tsx +++ b/src/Dashboard.tsx @@ -8,10 +8,21 @@ import { import ConfigurableDashboard from "./components/Dashboard"; import { configuration } from "./dashboard-config"; -import { useDashboardData } from "./features/dashboard"; +import { + useReport, + prepareReport, +} from "./features/report"; export default function Dashboard() { - const { data, isLoading, error } = useDashboardData(); + const report = useReport({ + rolling: true, + include_transactions: true, + group_by: ["payee"], + }) + + const isLoading = report.isLoading; + const error = report.error; + if (isLoading) { return ( @@ -29,10 +40,11 @@ export default function Dashboard() { ); } - if (!data) { + if (!report) { return null; } + const data = prepareReport(report.data?.data); return ( void; - togglePeriodType: () => void; - setSelectedPeriodId: (id: string | null) => void; - toggleComparison: () => void; + data: ReportData; } diff --git a/src/components/Dashboard/Dashboard.view.tsx b/src/components/Dashboard/Dashboard.view.tsx index 32e44af..f803d9c 100644 --- a/src/components/Dashboard/Dashboard.view.tsx +++ b/src/components/Dashboard/Dashboard.view.tsx @@ -13,6 +13,10 @@ import { DashboardProps, DashboardState } from "./Dashboard.models"; interface ViewProps extends DashboardProps { state: DashboardState; setState: React.Dispatch>; + toggleMode: () => void; + togglePeriodType: () => void; + setSelectedPeriodId: (id: string | null) => void; + toggleComparison: () => void; } export default function DashboardView({ @@ -101,63 +105,26 @@ export default function DashboardView({ )} - {section.isList ? ( - - {section.title && ( - - - {section.title} - - - )} - - {(data[section.dataKey || ""] || []).map((item: any, idx: number) => ( - - - - ))} - - - ) : ( - - )} + togglePeriodType={togglePeriodType} + toggleComparison={toggleComparison} + setSelectedPeriodId={setSelectedPeriodId} + /> ); })} diff --git a/src/features/dashboard/dashboard.mapper.ts b/src/features/dashboard/dashboard.mapper.ts deleted file mode 100644 index 370dc0e..0000000 --- a/src/features/dashboard/dashboard.mapper.ts +++ /dev/null @@ -1,191 +0,0 @@ -import * as React from "react"; -import MonetizationOnIcon from "@mui/icons-material/MonetizationOn"; -import { LatestItem } from "../../components/LatestItems"; -import { - ChartData, - ChartDataPoint, -} from "../../components/HistoryChart"; - -const DEFAULT_ICON = React.createElement(MonetizationOnIcon, { - sx: { color: "#388e3c" } -}); -type ReportBucket = any; - -export function mapToLatestItems( - items: any[], - type: "expense" | "income" -): LatestItem[] { - const isValid = (amt: number) => - type === "expense" ? amt < 0 : amt > 0; - - return items - .filter((item: any) => isValid(Number(item.amount) || 0)) - .slice(0, 5) - .map((exp: any, index: number) => { - const time = new Date(exp.occurred_at).getTime(); - - const diffDays = Math.floor( - Math.abs(Date.now() - time) / (1000 * 60 * 60 * 24) - ); - - return { - id: exp.id || index, - icon: DEFAULT_ICON, - iconBgColor: - type === "expense" ? "#ffebee" : "#e8f5e9", - title: exp.payee.name, - subtitle: exp.account.name, - amount: `Rs ${Math.abs(exp.amount || 0)}`, - timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago` - }; - }); -} - -const sumBucket = (bucket: ReportBucket, flow: "expenses" | "incomes") => - bucket.groups.reduce( - (acc: number, g: any) => acc + (g?.[flow]?.sum || 0), - 0 - ); - -const toLabel = (start: string, end: string, type: "weekly" | "monthly" | "yearly" | "fyly" | "full") => { - const s = new Date(start); - const e = new Date(end); - - if (type === "monthly") { - return s.toLocaleString("default", { month: "short" }); - } - - return `${s.getDate()}–${e.getDate()} ${e.toLocaleString("default", { - month: "short", - })}`; -}; - -const getWeekOfMonth = (date: Date) => { - const firstDay = new Date(date.getFullYear(), date.getMonth(), 1); - return Math.ceil((date.getDate() + firstDay.getDay()) / 7); -}; - -const findCompareBucket = ( - current: ReportBucket, - buckets: ReportBucket[], - type: "weekly" | "monthly" | "yearly" | "fyly" | "full", -): ReportBucket | undefined => { - const start = new Date(current.start); - - if (type === "monthly") { - const targetYear = start.getFullYear() - 1; - const targetMonth = start.getMonth(); - - return buckets.find(b => { - const d = new Date(b.start); - return ( - d.getFullYear() === targetYear && - d.getMonth() === targetMonth - ); - }); - } - - if (type === "weekly") { - const weekIndex = getWeekOfMonth(start); // you must define this - const target = new Date(start); - target.setMonth(target.getMonth() - 1); - - return buckets.find(b => { - const d = new Date(b.start); - return ( - d.getFullYear() === target.getFullYear() && - d.getMonth() === target.getMonth() && - getWeekOfMonth(d) === weekIndex - ); - }); - } - - return undefined; -}; - -const toPoints = ( - buckets: ReportBucket[], - type: "weekly" | "monthly" | "yearly" | "fyly" | "full", - flow: "expenses" | "incomes" -): ChartDataPoint[] => { - return buckets.map((b) => { - const amount = sumBucket(b, flow); - const prev = findCompareBucket(b, buckets, type); - - return { - id: toLabel(b.start, b.end, type), - amount, - compare: prev - ? { - id: toLabel(prev.start, prev.end, type), - amount: sumBucket(prev, flow), - } - : undefined, - }; - }); -}; - -export function mapReportToDashboard( - weekly: ReportBucket[], - monthly: ReportBucket[], - payeeBuckets: ReportBucket[], - type: "expense" | "income" -) { - const flow = type === "expense" ? "expenses" : "incomes"; - - const chartData: ChartData = { - weekly: { - rolling: toPoints(weekly, "weekly", flow), - calendar: toPoints(weekly, "weekly", flow), - }, - - monthly: { - rolling: toPoints(monthly, "monthly", flow), - calendar: toPoints(monthly, "monthly", flow), - }, - - // yearly: { - // rolling: toPoints(yearly, "yearly", flow), - // calendar: toPoints(yearly, "yearly", flow), - // }, - // - // fyly: { - // rolling: toPoints(fyly, "fyly", flow), - // calendar: toPoints(fyly, "fyly", flow), - // }, - // - // full: { - // rolling: toPoints(full, "full", flow), - // calendar: toPoints(full, "full", flow), - // }, - }; - - const totalAmount = weekly.reduce( - (acc, b) => acc + sumBucket(b, flow), - 0 - ); - - const payeeMap: Record = {}; - - const sourceForPayees = (payeeBuckets && payeeBuckets.length > 0) ? payeeBuckets : weekly; - - for (const b of sourceForPayees) { - for (const g of b.groups) { - const key = g.group_key || "Unknown"; - const amt = g?.[flow]?.sum || 0; - payeeMap[key] = (payeeMap[key] || 0) + amt; - } - } - - const topPayees = Object.entries(payeeMap) - // .filter(([name]) => name !== "Unknown") - .map(([payeeName, amount]) => ({ payeeName, amount })) - .sort((a, b) => b.amount - a.amount) - .slice(0, 5); - - return { - chartData, - totalAmount, - topPayees, - }; -} diff --git a/src/features/dashboard/index.ts b/src/features/dashboard/index.ts deleted file mode 100644 index ddf8c37..0000000 --- a/src/features/dashboard/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { - useDashboardData -} from './useDashboardData' diff --git a/src/features/dashboard/useDashboardData.ts b/src/features/dashboard/useDashboardData.ts deleted file mode 100644 index b1e56a9..0000000 --- a/src/features/dashboard/useDashboardData.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useReport } from "../report"; -import { mapReportToDashboard } from "./dashboard.mapper"; - -export function useDashboardData() { - // Fetch reports for aggregation - const weeklyReport = useReport({ period: "weekly", rolling: true }); - const monthlyReport = useReport({ period: "monthly", rolling: true }); - const payeeReport = useReport({ period: "full", rolling: true, group_by: ["payee"] }); - - const isLoading = - weeklyReport.isLoading || - monthlyReport.isLoading || - payeeReport.isLoading; - - const error = - weeklyReport.error || - monthlyReport.error || - payeeReport.error; - - const aggregatedData = { - expense: weeklyReport.data?.data && monthlyReport.data?.data && payeeReport.data?.data - ? mapReportToDashboard( - (weeklyReport.data.data as any).buckets, - (monthlyReport.data.data as any).buckets, - (payeeReport.data.data as any).buckets, - "expense" - ) - : null, - income: weeklyReport.data?.data && monthlyReport.data?.data && payeeReport.data?.data - ? mapReportToDashboard( - (weeklyReport.data.data as any).buckets, - (monthlyReport.data.data as any).buckets, - (payeeReport.data.data as any).buckets, - "income" - ) - : null, - } - - return { - data: aggregatedData, - isLoading, - error, - }; -} diff --git a/src/features/report/index.ts b/src/features/report/index.ts index 64a342d..246b4ac 100644 --- a/src/features/report/index.ts +++ b/src/features/report/index.ts @@ -3,9 +3,6 @@ export { } from './useReport' export type { Transaction, - PeriodData, - PeriodGroup, - Period, ReportData, } from './report.models' export { diff --git a/src/features/report/report.models.ts b/src/features/report/report.models.ts index ada38ed..43f5184 100644 --- a/src/features/report/report.models.ts +++ b/src/features/report/report.models.ts @@ -18,44 +18,72 @@ export interface Tag { } export interface Transaction { - payor: Payor; - payee: Payee; - amount: number; - account: Account; - tags: Tag[] - occurred_at: Date + payor: Payor; + payee: Payee; + amount: number; + account: Account; + tags: Tag[]; + occurred_at: Date; } -export interface _PeriodData { +// ----------------------------- +// Metrics +// ----------------------------- + +export interface ReportMetric { sum: number; count: number; average: number; - txns: Transaction[]; + transactions?: Transaction[]; } -export interface PeriodData extends _PeriodData { - compare?: _PeriodData; -} +// ----------------------------- +// Period +// ----------------------------- -export interface PeriodGroup { - group_key: string[]; - expenses: PeriodData[]; - incomes: PeriodData[]; -} - -export interface Period { - id: string; - label: string; +export interface ReportPeriod { start: Date; end: Date; - groups: PeriodGroup[]; + + expenses: ReportMetric; + incomes: ReportMetric; } -export interface ReportData { - period: "weekly" | "monthly" | "yearly" | "fyly" | "full"; - rolling?: boolean; - report_date?: string; - group_by?: ("payee" | "tags")[]; - ignore_self?: boolean; - buckets: Period[]; +// ----------------------------- +// Group (bucket) +// ----------------------------- + +export type GroupKey = { + payee?: string[]; + tags?: string[]; + flow?: string[]; +}; + +export interface ReportBucket { + group_key: GroupKey; + + periods: { + weekly?: ReportPeriod[]; + monthly?: ReportPeriod[]; + yearly?: ReportPeriod[]; + fyly?: ReportPeriod[]; + }; +} + +// ----------------------------- +// Final Report +// ----------------------------- + +export interface ReportData { + periods: ("weekly" | "monthly" | "yearly" | "fyly")[]; + + rolling: boolean; + report_date?: string; + + group_by: ("payee" | "tags")[]; + + ignore_self: boolean; + include_transactions: boolean; + + buckets: ReportBucket[]; } diff --git a/src/features/report/report.utils.ts b/src/features/report/report.utils.ts index 387f3fe..40b908c 100644 --- a/src/features/report/report.utils.ts +++ b/src/features/report/report.utils.ts @@ -1,4 +1,7 @@ -import { ReportData } from "./report.models"; +import { + ReportData, + ReportPeriod +} from "./report.models"; /* ---------- ID BUILDING ---------- */ @@ -10,7 +13,7 @@ function formatDate(d: Date): string { } function buildPeriodId( - type: ReportData["period"], + type: "weekly" | "monthly" | "yearly" | "fyly" | "full", start: Date, end: Date ): string { @@ -65,7 +68,7 @@ function sameMonth(a: Date, b: Date) { } function buildLabel( - type: ReportData["period"], + type: "weekly" | "monthly" | "yearly" | "fyly" | "full", start: Date, end: Date ): string { @@ -94,9 +97,6 @@ function buildLabel( return `FY ${startY}–${String(endY).slice(-2)}`; } - case "full": - return `${monthFmt.format(start)} ${yearFmt.format(start)} - ${monthFmt.format(end)} ${yearFmt.format(end)}`; - default: return `${monthDayFmt.format(start)} - ${monthDayFmt.format(end)}`; } @@ -104,13 +104,34 @@ function buildLabel( /* ---------- MAIN ---------- */ +function decoratePeriods( + type: "weekly" | "monthly" | "yearly" | "fyly" | "full", + periods: ReportPeriod[] +): (ReportPeriod & { id: string; label: string })[] { + return periods.map((p) => ({ + ...p, + id: buildPeriodId(type, new Date(p.start), new Date(p.end)), + label: buildLabel(type, new Date(p.start), new Date(p.end)), + })); +} + export function prepareReport(reportData: ReportData): ReportData { return { ...reportData, - buckets: reportData.buckets.map((p) => ({ - ...p, - id: buildPeriodId(reportData.period, p.start, p.end), - label: buildLabel(reportData.period, p.start, p.end), - })), + buckets: reportData.buckets.map((bucket) => { + const newPeriods: typeof bucket.periods = {}; + + for (const type of reportData.periods) { + const arr = bucket.periods[type]; + if (arr) { + newPeriods[type] = decoratePeriods(type, arr); + } + } + + return { + ...bucket, + periods: newPeriods, + }; + }), }; -} +} \ No newline at end of file diff --git a/src/features/report/useReport.ts b/src/features/report/useReport.ts index e7e501d..d479d33 100644 --- a/src/features/report/useReport.ts +++ b/src/features/report/useReport.ts @@ -1,18 +1,20 @@ import { useResourceByName } from "../../../react-openapi"; export interface ReportParams { - period: "weekly" | "monthly" | "yearly" | "fyly" | "full"; + periods?: ("weekly" | "monthly" | "yearly" | "fyly" | "full")[]; rolling?: boolean; report_date?: string; group_by?: ("payee" | "tags")[]; ignore_self?: boolean; + include_transactions?: boolean; } export function useReport(params: ReportParams) { const { useList } = useResourceByName("reports"); - if (params.group_by) { - // @ts-ignore - params.group_by = params.group_by[0] - } - return useList(params); -} \ No newline at end of file + + return useList({ + ...params, + periods: params.periods, + group_by: params.group_by, + }); +}