Files
khata-ui/src/utils/dashboardLoader.ts

263 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, MonSun 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);