From 1fe44abfde339cc536277bf4b3fe1ae6a0593ebd Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 24 Apr 2026 14:27:20 +0530 Subject: [PATCH] removed client data massaging with backend report using feature/report --- src/features/report/report.api.ts | 9 ++ src/features/report/report.mapper.ts | 96 ++++++++++++++ src/features/report/report.service.ts | 114 +++++++++++++++++ src/features/report/useReport.ts | 17 +++ src/utils/dashboardLoader.ts | 142 ++------------------ src/utils/dateUtils.ts | 33 ----- src/utils/periodBuilders.ts | 178 -------------------------- 7 files changed, 244 insertions(+), 345 deletions(-) create mode 100644 src/features/report/report.api.ts create mode 100644 src/features/report/report.mapper.ts create mode 100644 src/features/report/report.service.ts create mode 100644 src/features/report/useReport.ts delete mode 100644 src/utils/dateUtils.ts delete mode 100644 src/utils/periodBuilders.ts diff --git a/src/features/report/report.api.ts b/src/features/report/report.api.ts new file mode 100644 index 0000000..528b844 --- /dev/null +++ b/src/features/report/report.api.ts @@ -0,0 +1,9 @@ +import { api } from "../../../react-openapi"; + +export async function fetchReport(params: { + period: "weekly" | "monthly" | "yearly" | "fyly"; + rolling?: boolean; +}) { + const res = await api.get("/reports", { params }); + return res.data; +} \ No newline at end of file diff --git a/src/features/report/report.mapper.ts b/src/features/report/report.mapper.ts new file mode 100644 index 0000000..ddd469a --- /dev/null +++ b/src/features/report/report.mapper.ts @@ -0,0 +1,96 @@ +import { + AggregatedDashboardData, + ChartData, + ChartDataPoint, +} from "../../components/HistoryChart"; + +type ReportBucket = any; + +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") => { + 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 toPoints = ( + buckets: ReportBucket[], + type: "weekly" | "monthly", + flow: "expenses" | "incomes" +): ChartDataPoint[] => { + return buckets.map((b, i) => { + const amount = sumBucket(b, flow); + const prev = buckets[i - 1]; + + 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[], + type: "expense" | "income" +): AggregatedDashboardData { + const flow = type === "expense" ? "expenses" : "incomes"; + + const chartData: ChartData = { + daily: [], + + weekly: { + rolling: toPoints(weekly, "weekly", flow), + calendar: toPoints(weekly, "weekly", flow), + }, + + monthly: { + rolling: toPoints(monthly, "monthly", flow), + calendar: toPoints(monthly, "monthly", flow), + }, + }; + + const totalAmount = weekly.reduce( + (acc, b) => acc + sumBucket(b, flow), + 0 + ); + + const payeeMap: Record = {}; + + for (const b of weekly) { + 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) + .map(([payeeName, amount]) => ({ payeeName, amount })) + .sort((a, b) => b.amount - a.amount) + .slice(0, 5); + + return { + chartData, + totalAmount, + topPayees, + }; +} diff --git a/src/features/report/report.service.ts b/src/features/report/report.service.ts new file mode 100644 index 0000000..b5726ad --- /dev/null +++ b/src/features/report/report.service.ts @@ -0,0 +1,114 @@ +import { fetchReport } from "./report.api"; +import { + AggregatedDashboardData, + ChartData, + ChartDataPoint, +} from "../components/HistoryChart"; + +type ReportBucket = any; // replace with generated type if available + +function sumBucket(bucket: ReportBucket, flow: "expenses" | "incomes") { + return bucket.groups.reduce( + (acc: number, g: any) => acc + (g?.[flow]?.sum || 0), + 0 + ); +} + +function toLabel(start: string, end: string, type: "weekly" | "monthly") { + const s = new Date(start); + const e = new Date(end); + + if (type === "monthly") { + return s.toLocaleString("default", { month: "short" }); + } + + const sd = s.getDate(); + const ed = e.getDate(); + const m = e.toLocaleString("default", { month: "short" }); + return `${sd}–${ed} ${m}`; +} + +function toChartPoints( + buckets: ReportBucket[], + type: "weekly" | "monthly", + flow: "expenses" | "incomes" +): ChartDataPoint[] { + return buckets.map((b, i) => { + const amount = sumBucket(b, flow); + + const prev = buckets[i - 1]; + const compareAmount = prev ? sumBucket(prev, flow) : 0; + + return { + id: toLabel(b.start, b.end, type), + amount, + compare: prev + ? { + id: toLabel(prev.start, prev.end, type), + amount: compareAmount, + } + : undefined, + }; + }); +} + +function buildChartData( + weekly: ReportBucket[], + monthly: ReportBucket[], + flow: "expenses" | "incomes" +): ChartData { + return { + daily: [], // not supported by /reports → keep empty or drop + weekly: { + rolling: toChartPoints(weekly, "weekly", flow), + calendar: toChartPoints(weekly, "weekly", flow), // same unless backend differentiates + }, + monthly: { + rolling: toChartPoints(monthly, "monthly", flow), + calendar: toChartPoints(monthly, "monthly", flow), + }, + }; +} + +function getTopPayees(buckets: ReportBucket[], flow: "expenses" | "incomes") { + const map: Record = {}; + + for (const b of buckets) { + for (const g of b.groups) { + const key = g.group_key || "Unknown"; + const amt = g?.[flow]?.sum || 0; + map[key] = (map[key] || 0) + amt; + } + } + + return Object.entries(map) + .map(([payeeName, amount]) => ({ payeeName, amount })) + .sort((a, b) => b.amount - a.amount) + .slice(0, 5); +} + +export async function getDashboardData( + type: "expense" | "income" +): Promise { + const flow = type === "expense" ? "expenses" : "incomes"; + + const [weeklyBuckets, monthlyBuckets] = await Promise.all([ + fetchReport({ period: "weekly", rolling: true }), + fetchReport({ period: "monthly", rolling: true }), + ]); + + const chartData = buildChartData(weeklyBuckets, monthlyBuckets, flow); + + const totalAmount = weeklyBuckets.reduce( + (acc: number, b: any) => acc + sumBucket(b, flow), + 0 + ); + + const topPayees = getTopPayees(weeklyBuckets, flow); + + return { + chartData, + totalAmount, + topPayees, + }; +} \ No newline at end of file diff --git a/src/features/report/useReport.ts b/src/features/report/useReport.ts new file mode 100644 index 0000000..bd2e039 --- /dev/null +++ b/src/features/report/useReport.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import { fetchReport } from "./report.api"; + +export interface ReportParams { + period: "weekly" | "monthly" | "yearly" | "fyly"; + rolling?: boolean; + report_date?: string; + group_by?: ("flow" | "payee" | "tags")[]; + ignore_self?: boolean; +} + +export function useReport(params: ReportParams) { + return useQuery({ + queryKey: ["report", params], + queryFn: () => fetchReport(params), + }); +} \ No newline at end of file diff --git a/src/utils/dashboardLoader.ts b/src/utils/dashboardLoader.ts index 1630b98..654c961 100644 --- a/src/utils/dashboardLoader.ts +++ b/src/utils/dashboardLoader.ts @@ -1,17 +1,10 @@ import { api } from "../../react-openapi"; -import { LatestItem } from "../components/LatestItemsList"; -import { ChartDataPoint } from "../types/historyChart"; +import { LatestItem } from "../components/LatestItems"; import * as React from "react"; -import { format } from "./dateUtils"; import MonetizationOnIcon from "@mui/icons-material/MonetizationOn"; -import { - buildDailyBuckets, - buildWeeklyRolling, - buildWeeklyCalendar, - buildMonthlyRolling, - buildMonthlyCalendar -} from "./periodBuilders"; +import { fetchReport } from "../features/report/report.api"; +import { mapReportToDashboard } from "../features/report/report.mapper"; const DEFAULT_ICON = React.createElement(MonetizationOnIcon, { sx: { color: "#388e3c" } @@ -58,131 +51,12 @@ export async function fetchLatestTransactions( export async function fetchAggregatedData( type: "expense" | "income" ) { - const res = await api.get("/expenses", { params: { limit: 0 } }); - const all: any[] = res.data?.items || res.data || []; + const [weekly, monthly] = await Promise.all([ + fetchReport({ period: "weekly", rolling: true }), + fetchReport({ period: "monthly", rolling: true }), + ]); - const now = new Date(); - - let totalAmount = 0; - const payeeMap: Record = {}; - - const isValid = (amt: number) => - type === "expense" ? amt < 0 : amt > 0; - - const normalize = (amt: number) => Math.abs(amt); - - const { - buckets: dailyBuckets, - weekStart, - weekEnd, - prevWeekStart, - prevWeekEnd - } = buildDailyBuckets(now); - - const weeklyRolling = buildWeeklyRolling(now); - const weeklyCalendar = buildWeeklyCalendar(now); - const monthlyRolling = buildMonthlyRolling(now); - const monthlyCalendar = buildMonthlyCalendar(now); - - for (const item of all) { - const d = new Date( - item.occurred_at || item.created_at || Date.now() - ); - - const amtRaw = Number(item.amount) || 0; - if (!isValid(amtRaw)) continue; - - const amt = normalize(amtRaw); - totalAmount += amt; - - const payee = item.payee?.name || item.payee || "Unknown"; - payeeMap[payee] = (payeeMap[payee] || 0) + amt; - - const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()]; - - if (d >= weekStart && d <= weekEnd) { - if (dailyBuckets[day]) { - dailyBuckets[day].amount += amt; - } - } - - if (d >= prevWeekStart && d <= prevWeekEnd) { - if (dailyBuckets[day]) { - dailyBuckets[day].compare += amt; - } - } - - const apply = (arr: any[]) => { - for (const b of arr) { - if (d >= b.start && d <= b.end) b.amount += amt; - if (d >= b.prevStart && d <= b.prevEnd) - b.compare += amt; - } - }; - - apply(weeklyRolling); - apply(weeklyCalendar); - apply(monthlyRolling); - apply(monthlyCalendar); - } - - const toPoints = (arr: any[], type: "weekly" | "monthly"): ChartDataPoint[] => - arr.map((x) => { - let compareLabel: string | undefined; - - if (x.prevStart && x.prevEnd) { - if (type === "monthly") { - const year = String(x.prevStart.getFullYear()).slice(2); - compareLabel = `${x.prevStart.toLocaleString("default", { - month: "short" - })}-${year}`; - } else { - const year = String(x.prevEnd.getFullYear()).slice(2); - compareLabel = `${format(x.prevStart)} - ${format(x.prevEnd)} ${year}`; - } - } - - return { - id: x.label, - amount: x.amount, - compareAmount: x.compare, - compareLabel - }; - }); - - const chartData = { - daily: Object.entries(dailyBuckets).map(([k, v]: any) => ({ - id: k, - amount: v.amount, - compareAmount: v.compare - })), - weekly: { - rolling: toPoints(weeklyRolling, "weekly"), - calendar: toPoints(weeklyCalendar, "weekly") - }, - monthly: { - rolling: toPoints(monthlyRolling, "monthly"), - calendar: toPoints(monthlyCalendar, "monthly") - } - }; - - Object.values(chartData).forEach((group: any) => { - const arr = Array.isArray(group) ? group : group.rolling; - if (!arr?.length) return; - - let max = arr[0]; - for (const g of arr) { - if (g.amount > max.amount) max = g; - } - if (max.amount > 0) max.highlighted = true; - }); - - const topPayees = Object.entries(payeeMap) - .map(([name, amt]) => ({ payeeName: name, amount: amt })) - .sort((a, b) => b.amount - a.amount) - .slice(0, 5); - - return { chartData, totalAmount, topPayees }; + return mapReportToDashboard(weekly.buckets, monthly.buckets, type); } export const fetchAggregatedExpenses = () => diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts deleted file mode 100644 index 0652304..0000000 --- a/src/utils/dateUtils.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const format = (d: Date) => - `${d.getDate()} ${d.toLocaleString("default", { month: "short" })}`; - -export const startOfDay = (d: Date) => { - const x = new Date(d); - x.setHours(0, 0, 0, 0); - return x; -}; - -export const endOfDay = (d: Date) => { - const x = new Date(d); - x.setHours(23, 59, 59, 999); - return x; -}; - -export const getStartOfWeek = (d: Date) => { - const date = new Date(d); - const day = date.getDay() || 7; - if (day !== 1) date.setDate(date.getDate() - (day - 1)); - return startOfDay(date); -}; - -export const shiftDate = (d: Date, days: number) => - new Date(d.getTime() + days * 86400000); - -export const getWeekIndex = (date: Date) => { - const firstDay = new Date(date.getFullYear(), date.getMonth(), 1); - const firstWeekStart = getStartOfWeek(firstDay); - return Math.floor( - (startOfDay(date).getTime() - firstWeekStart.getTime()) / - (7 * 86400000) - ); -}; diff --git a/src/utils/periodBuilders.ts b/src/utils/periodBuilders.ts deleted file mode 100644 index daa444e..0000000 --- a/src/utils/periodBuilders.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - format, - endOfDay, - getStartOfWeek, - shiftDate, - getWeekIndex -} from "./dateUtils"; - -export const buildDailyBuckets = (now: Date) => { - const buckets: Record = { - Mon: { amount: 0, compare: 0 }, - Tue: { amount: 0, compare: 0 }, - Wed: { amount: 0, compare: 0 }, - Thu: { amount: 0, compare: 0 }, - Fri: { amount: 0, compare: 0 }, - Sat: { amount: 0, compare: 0 }, - Sun: { amount: 0, compare: 0 } - }; - - const weekStart = getStartOfWeek(now); - const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000)); - const prevWeekStart = shiftDate(weekStart, -7); - const prevWeekEnd = shiftDate(weekEnd, -7); - - return { buckets, weekStart, weekEnd, prevWeekStart, prevWeekEnd }; -}; - -const getPrevMonthWeek = (start: Date) => { - const prevMonthDate = new Date(start); - prevMonthDate.setMonth(prevMonthDate.getMonth() - 1); - - const prevMonthFirst = new Date( - prevMonthDate.getFullYear(), - prevMonthDate.getMonth(), - 1 - ); - - const prevFirstWeekStart = getStartOfWeek(prevMonthFirst); - const weekIndex = getWeekIndex(start); - - const prevStart = new Date( - prevFirstWeekStart.getTime() + weekIndex * 7 * 86400000 - ); - const prevEnd = endOfDay(new Date(prevStart.getTime() + 6 * 86400000)); - - return { prevStart, prevEnd }; -}; - -export const buildWeeklyRolling = (now: Date) => { - const arr: any[] = []; - const currentWeekStart = getStartOfWeek(now); - - for (let i = 4; i >= 0; i--) { - const start = new Date( - currentWeekStart.getTime() - i * 7 * 86400000 - ); - const end = endOfDay(new Date(start.getTime() + 6 * 86400000)); - - const { prevStart, prevEnd } = getPrevMonthWeek(start); - - arr.push({ - label: `${format(start)} - ${format(end)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -}; - -export const buildWeeklyCalendar = (now: Date) => { - const arr: any[] = []; - - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); - const firstWeekStart = getStartOfWeek(startOfMonth); - - const totalWeeks = - Math.ceil( - (endOfMonth.getTime() - firstWeekStart.getTime()) / - (7 * 86400000) - ) + 1; - - for (let i = 0; i < totalWeeks; i++) { - const start = new Date( - firstWeekStart.getTime() + i * 7 * 86400000 - ); - const end = endOfDay(new Date(start.getTime() + 6 * 86400000)); - - const { prevStart, prevEnd } = getPrevMonthWeek(start); - - arr.push({ - label: `${format(start)} - ${format(end)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -}; - -export const buildMonthlyRolling = (now: Date) => { - const arr: any[] = []; - - for (let i = 11; i >= 0; i--) { - const d = new Date(now); - d.setMonth(d.getMonth() - i); - - const start = new Date(d.getFullYear(), d.getMonth(), 1); - const end = - i === 0 - ? endOfDay(now) - : endOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0)); - - const prevStart = new Date(start); - prevStart.setFullYear(prevStart.getFullYear() - 1); - - let prevEnd = new Date(end); - prevEnd.setFullYear(prevEnd.getFullYear() - 1); - - if (i === 0) { - prevEnd = new Date(prevStart); - prevEnd.setDate(now.getDate()); - prevEnd = endOfDay(prevEnd); - } - - arr.push({ - label: `${d.toLocaleString("default", { - month: "short" - })}-${String(d.getFullYear()).slice(2)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -}; - -export const buildMonthlyCalendar = (now: Date) => { - const arr: any[] = []; - - for (let i = 0; i < 12; i++) { - const start = new Date(now.getFullYear(), i, 1); - const end = endOfDay(new Date(now.getFullYear(), i + 1, 0)); - - const prevStart = new Date(start); - prevStart.setFullYear(prevStart.getFullYear() - 1); - - const prevEnd = new Date(end); - prevEnd.setFullYear(prevEnd.getFullYear() - 1); - - arr.push({ - label: `${start.toLocaleString("default", { - month: "short" - })}-${String(start.getFullYear()).slice(2)}`, - start, - end, - amount: 0, - compare: 0, - prevStart, - prevEnd - }); - } - - return arr; -};