263 lines
7.2 KiB
TypeScript
263 lines
7.2 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" }
|
||
});
|
||
|
||
// ---------------- HELPERS ----------------
|
||
const format = (d: Date) =>
|
||
`${d.getDate()} ${d.toLocaleString("default", { month: "short" })}`;
|
||
|
||
const startOfDay = (d: Date) => {
|
||
const x = new Date(d);
|
||
x.setHours(0, 0, 0, 0);
|
||
return x;
|
||
};
|
||
|
||
const endOfDay = (d: Date) => {
|
||
const x = new Date(d);
|
||
x.setHours(23, 59, 59, 999);
|
||
return x;
|
||
};
|
||
|
||
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);
|
||
};
|
||
|
||
// ---------------- LATEST ----------------
|
||
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`
|
||
};
|
||
});
|
||
}
|
||
|
||
// ---------------- TYPES ----------------
|
||
export interface AggregatedDashboardData {
|
||
chartData: Record<string, ChartDataPoint[]>;
|
||
totalAmount: number;
|
||
topPayees: Array<{ payeeName: string; amount: number }>;
|
||
}
|
||
|
||
// ---------------- AGGREGATION ----------------
|
||
// ---------------- AGGREGATION ----------------
|
||
export async function fetchAggregatedData(
|
||
type: "expense" | "income",
|
||
mode: "rolling" | "calendar" = "rolling"
|
||
): 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);
|
||
|
||
// ---------------- WEEK ----------------
|
||
const dailyBuckets: Record<string, number> = {
|
||
Mon: 0, Tue: 0, Wed: 0, Thu: 0,
|
||
Fri: 0, Sat: 0, Sun: 0
|
||
};
|
||
|
||
const weekStart = getStartOfWeek(now);
|
||
const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000));
|
||
|
||
// ---------------- MONTH (rolling 5 weeks, Mon–Sun aligned) ----------------
|
||
const weeklyBuckets = [];
|
||
|
||
if (mode === "rolling") {
|
||
const currentWeekStart = getStartOfWeek(now);
|
||
|
||
for (let i = 0; i < 5; i++) {
|
||
const start = new Date(currentWeekStart.getTime() - i * 7 * 86400000);
|
||
const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
|
||
|
||
weeklyBuckets.push({
|
||
label: `${format(start)} - ${format(end)}`,
|
||
start,
|
||
end,
|
||
amount: 0
|
||
});
|
||
}
|
||
} else {
|
||
// calendar weeks within current month
|
||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||
let cursor = getStartOfWeek(startOfMonth);
|
||
|
||
while (cursor <= now) {
|
||
const start = new Date(cursor);
|
||
const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
|
||
|
||
weeklyBuckets.push({
|
||
label: `${format(start)} - ${format(end)}`,
|
||
start,
|
||
end,
|
||
amount: 0
|
||
});
|
||
|
||
cursor = new Date(cursor.getTime() + 7 * 86400000);
|
||
}
|
||
}
|
||
|
||
// ---------------- YEAR (rolling 12 months) ----------------
|
||
const monthlyBuckets = [];
|
||
|
||
if (mode === "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 =
|
||
i === 0
|
||
? endOfDay(now) // current month → till now
|
||
: endOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0));
|
||
monthlyBuckets.push({
|
||
label: `${d.toLocaleString("default", { month: "short" })}-${String(d.getFullYear()).slice(2)}`,
|
||
start,
|
||
end,
|
||
amount: 0
|
||
});
|
||
}
|
||
} else {
|
||
// calendar year (Jan → current month)
|
||
for (let i = 0; i <= now.getMonth(); i++) {
|
||
const start = new Date(now.getFullYear(), i, 1);
|
||
const end = endOfDay(new Date(now.getFullYear(), i + 1, 0));
|
||
|
||
monthlyBuckets.push({
|
||
label: `${start.toLocaleString("default", { month: "short" })}-${String(start.getFullYear()).slice(2)}`,
|
||
start,
|
||
end,
|
||
amount: 0
|
||
});
|
||
}
|
||
}
|
||
|
||
// ---------------- 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;
|
||
|
||
// WEEK
|
||
if (d >= weekStart && d <= weekEnd) {
|
||
const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
|
||
if (dailyBuckets[day] !== undefined) {
|
||
dailyBuckets[day] += amt;
|
||
}
|
||
}
|
||
|
||
// MONTH (rolling weeks)
|
||
for (const b of weeklyBuckets) {
|
||
if (d >= b.start && d <= b.end) {
|
||
b.amount += amt;
|
||
}
|
||
}
|
||
|
||
// YEAR (rolling months)
|
||
for (const b of monthlyBuckets) {
|
||
if (d >= b.start && d <= b.end) {
|
||
b.amount += amt;
|
||
}
|
||
}
|
||
}
|
||
|
||
const toPoints = (b: any): ChartDataPoint[] =>
|
||
Array.isArray(b)
|
||
? b.map((x) => ({
|
||
id: x.label,
|
||
amount: x.amount
|
||
}))
|
||
: Object.entries(b).map(([k, v]: any) => ({
|
||
id: k,
|
||
amount: v
|
||
}));
|
||
|
||
const chartData = {
|
||
daily: toPoints(dailyBuckets),
|
||
weekly: toPoints(weeklyBuckets),
|
||
monthly: toPoints(monthlyBuckets)
|
||
};
|
||
|
||
// 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 = (
|
||
mode: "rolling" | "calendar"
|
||
) =>
|
||
fetchAggregatedData("expense", mode);
|
||
|
||
export const fetchAggregatedIncome = (
|
||
mode: "rolling" | "calendar"
|
||
) =>
|
||
fetchAggregatedData("income", mode); |