221 lines
6.4 KiB
TypeScript
221 lines
6.4 KiB
TypeScript
import { api } from "../../react-openapi";
|
|
import { LatestItem } from "../components/LatestItemsList";
|
|
import { ChartDataPoint } from "../components/HistoryChart";
|
|
import * as React from "react";
|
|
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
|
|
|
// ---------------- ICON ----------------
|
|
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
|
sx: { color: "#388e3c" }
|
|
});
|
|
|
|
// ---------------- LATEST ----------------
|
|
export async function fetchLatestExpenses(): Promise<LatestItem[]> {
|
|
const res = await api.get('/expenses', {
|
|
params: { limit: 10, sort: '-occurred_at' }
|
|
});
|
|
|
|
const items = res.data?.items || res.data || [];
|
|
|
|
return items.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: "#e8f5e9",
|
|
title: exp.payee?.name || exp.payee || "Unknown Payee",
|
|
subtitle: exp.category?.name || exp.account?.name || "Transaction",
|
|
amount: `Rs ${exp.amount || 0}`,
|
|
timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago`
|
|
};
|
|
});
|
|
}
|
|
|
|
// ---------------- TYPES ----------------
|
|
export interface AggregatedDashboardData {
|
|
chartData: Record<string, ChartDataPoint[]>;
|
|
totalAmount: number;
|
|
topPayees: Array<{ payeeName: string; amount: number }>;
|
|
}
|
|
|
|
// ---------------- GENERIC AGGREGATOR ----------------
|
|
export async function fetchAggregatedData(
|
|
type: "expense" | "income"
|
|
): Promise<AggregatedDashboardData> {
|
|
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);
|
|
|
|
// ---------------- BUCKETS ----------------
|
|
const todayBuckets: Record<string, number> = {
|
|
"12am":0,"3am":0,"6am":0,"9am":0,
|
|
"12pm":0,"3pm":0,"6pm":0,"9pm":0
|
|
};
|
|
|
|
const weekBuckets: Record<string, number> = {
|
|
"Mon":0,"Tue":0,"Wed":0,"Thu":0,
|
|
"Fri":0,"Sat":0,"Sun":0
|
|
};
|
|
|
|
const monthBuckets: Record<string, { amount: number; range: string }> = {};
|
|
const yearBuckets: Record<string, { amount: number; range: string }> = {};
|
|
|
|
const getStartOfWeek = (d: Date) => {
|
|
const date = new Date(d);
|
|
const day = date.getDay() || 7;
|
|
if (day !== 1) date.setDate(date.getDate() - (day - 1));
|
|
date.setHours(0,0,0,0);
|
|
return date;
|
|
};
|
|
|
|
const format = (d: Date) =>
|
|
`${d.getDate()} ${d.toLocaleString('default', { month: 'short' })}`;
|
|
|
|
// -------- MONTH (5 rolling weeks) --------
|
|
for (let i = 0; i < 5; i++) {
|
|
const end = new Date(now);
|
|
end.setDate(end.getDate() - i * 7);
|
|
|
|
const start = new Date(end);
|
|
start.setDate(start.getDate() - 6);
|
|
|
|
const key = `${format(start)} - ${format(end)}`;
|
|
|
|
monthBuckets[key] = { amount: 0, range: key };
|
|
}
|
|
|
|
// -------- YEAR (12 months rolling) --------
|
|
for (let i = 0; i < 12; i++) {
|
|
const d = new Date(now);
|
|
d.setMonth(d.getMonth() - i);
|
|
|
|
const start = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
|
|
|
const label = d.toLocaleString('default', { month: 'short' });
|
|
const range = `${format(start)} - ${format(end)}`;
|
|
|
|
yearBuckets[label] = { amount: 0, range };
|
|
}
|
|
|
|
const weekStart = getStartOfWeek(now);
|
|
const weekEnd = new Date(weekStart);
|
|
weekEnd.setDate(weekStart.getDate() + 6);
|
|
|
|
// ---------------- LOOP ----------------
|
|
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;
|
|
|
|
// ---- TODAY
|
|
if (
|
|
d.getDate() === now.getDate() &&
|
|
d.getMonth() === now.getMonth() &&
|
|
d.getFullYear() === now.getFullYear()
|
|
) {
|
|
const hr = d.getHours();
|
|
let label = "12am";
|
|
if (hr >= 3 && hr < 6) label = "3am";
|
|
else if (hr >= 6 && hr < 9) label = "6am";
|
|
else if (hr >= 9 && hr < 12) label = "9am";
|
|
else if (hr >= 12 && hr < 15) label = "12pm";
|
|
else if (hr >= 15 && hr < 18) label = "3pm";
|
|
else if (hr >= 18 && hr < 21) label = "6pm";
|
|
else if (hr >= 21) label = "9pm";
|
|
|
|
todayBuckets[label] += amt;
|
|
}
|
|
|
|
// ---- WEEK
|
|
if (d >= weekStart && d <= weekEnd) {
|
|
const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
|
|
if (weekBuckets[day] !== undefined) {
|
|
weekBuckets[day] += amt;
|
|
}
|
|
}
|
|
|
|
// ---- MONTH
|
|
Object.entries(monthBuckets).forEach(([_, obj]) => {
|
|
const [startStr, endStr] = obj.range.split(" - ");
|
|
const start = new Date(startStr);
|
|
const end = new Date(endStr);
|
|
|
|
if (d >= start && d <= end) {
|
|
obj.amount += amt;
|
|
}
|
|
});
|
|
|
|
// ---- YEAR
|
|
Object.entries(yearBuckets).forEach(([_, obj]) => {
|
|
const [startStr, endStr] = obj.range.split(" - ");
|
|
const start = new Date(startStr);
|
|
const end = new Date(endStr);
|
|
|
|
if (d >= start && d <= end) {
|
|
obj.amount += amt;
|
|
}
|
|
});
|
|
}
|
|
|
|
const toPoints = (b: any): ChartDataPoint[] =>
|
|
Object.entries(b).map(([k, v]: any) => ({
|
|
id: k,
|
|
amount: typeof v === "number" ? v : v.amount,
|
|
subLabel: typeof v === "number" ? undefined : v.range
|
|
}));
|
|
|
|
const chartData = {
|
|
today: toPoints(todayBuckets),
|
|
week: toPoints(weekBuckets),
|
|
month: toPoints(monthBuckets),
|
|
year: toPoints(yearBuckets)
|
|
};
|
|
|
|
// highlight max
|
|
Object.values(chartData).forEach(group => {
|
|
let max = group[0];
|
|
for (const g of group) 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 };
|
|
}
|
|
|
|
// ---------------- EXPORTS ----------------
|
|
export const fetchAggregatedExpenses = () =>
|
|
fetchAggregatedData("expense");
|
|
|
|
export const fetchAggregatedIncome = () =>
|
|
fetchAggregatedData("income"); |