193 lines
5.2 KiB
TypeScript
193 lines
5.2 KiB
TypeScript
import { api } from "../../react-openapi";
|
|
import { LatestItem } from "../components/LatestItemsList";
|
|
import { ChartDataPoint } from "../types/historyChart";
|
|
import * as React from "react";
|
|
import { format } from "./dateUtils";
|
|
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
|
|
|
import {
|
|
buildDailyBuckets,
|
|
buildWeeklyRolling,
|
|
buildWeeklyCalendar,
|
|
buildMonthlyRolling,
|
|
buildMonthlyCalendar
|
|
} from "./periodBuilders";
|
|
|
|
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
|
sx: { color: "#388e3c" }
|
|
});
|
|
|
|
export async function fetchLatestTransactions(
|
|
type: "expense" | "income"
|
|
): Promise<LatestItem[]> {
|
|
const res = await api.get("/expenses", {
|
|
params: { limit: 100, sort: "-occurred_at" }
|
|
});
|
|
|
|
const items = res.data?.items || res.data || [];
|
|
|
|
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 || exp.created_at || Date.now()
|
|
).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 || exp.payee || "Unknown Payee",
|
|
subtitle:
|
|
exp.category?.name || exp.account?.name || "Transaction",
|
|
amount: `Rs ${Math.abs(exp.amount || 0)}`,
|
|
timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago`
|
|
};
|
|
});
|
|
}
|
|
|
|
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 now = new Date();
|
|
|
|
let totalAmount = 0;
|
|
const payeeMap: Record<string, number> = {};
|
|
|
|
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 };
|
|
}
|
|
|
|
export const fetchAggregatedExpenses = () =>
|
|
fetchAggregatedData("expense");
|
|
|
|
export const fetchAggregatedIncome = () =>
|
|
fetchAggregatedData("income");
|