rolling and calender toggle

This commit is contained in:
2026-04-06 18:27:05 +05:30
parent 787324260c
commit fc88703a38
3 changed files with 133 additions and 108 deletions

View File

@@ -56,8 +56,8 @@ export default function Dashboard() {
] = await Promise.all([ ] = await Promise.all([
fetchLatestTransactions("expense"), fetchLatestTransactions("expense"),
fetchLatestTransactions("income"), fetchLatestTransactions("income"),
fetchAggregatedExpenses(period), fetchAggregatedExpenses(),
fetchAggregatedIncome(period) fetchAggregatedIncome()
]); ]);
setLatest({ setLatest({
@@ -79,7 +79,7 @@ export default function Dashboard() {
} }
loadData(); loadData();
}, [period]); }, []);
const currentData = aggregated[mode]; const currentData = aggregated[mode];
const currentLatest = latest[mode]; const currentLatest = latest[mode];

View File

@@ -4,6 +4,7 @@ import { Box, Typography, ToggleButtonGroup, ToggleButton, Paper } from "@mui/ma
export interface ChartDataPoint { export interface ChartDataPoint {
id: string; id: string;
amount: number; amount: number;
compareAmount?: number;
count?: number; count?: number;
highlighted?: boolean; highlighted?: boolean;
} }
@@ -34,7 +35,13 @@ export default function HistoryChart({
}; };
const activeDataKey = activeTab.toLowerCase(); const activeDataKey = activeTab.toLowerCase();
const rawData = data[activeDataKey] || data[activeTab] || []; let rawData;
if (activeDataKey === "daily") {
rawData = data.daily;
} else {
// @ts-ignore
rawData = data[activeDataKey]?.[period] || [];
}
const currentData = [...rawData].reverse(); const currentData = [...rawData].reverse();
const maxAmount = const maxAmount =

View File

@@ -71,17 +71,26 @@ export async function fetchLatestTransactions(
} }
// ---------------- TYPES ---------------- // ---------------- TYPES ----------------
export interface ChartSeries {
rolling: ChartDataPoint[];
calendar: ChartDataPoint[];
}
export interface ChartData {
daily: ChartDataPoint[];
weekly: ChartSeries;
monthly: ChartSeries;
}
export interface AggregatedDashboardData { export interface AggregatedDashboardData {
chartData: Record<string, ChartDataPoint[]>; chartData: ChartData;
totalAmount: number; totalAmount: number;
topPayees: Array<{ payeeName: string; amount: number }>; topPayees: Array<{ payeeName: string; amount: number }>;
} }
// ---------------- AGGREGATION ----------------
// ---------------- AGGREGATION ---------------- // ---------------- AGGREGATION ----------------
export async function fetchAggregatedData( export async function fetchAggregatedData(
type: "expense" | "income", type: "expense" | "income"
mode: "rolling" | "calendar" = "rolling"
): Promise<AggregatedDashboardData> { ): Promise<AggregatedDashboardData> {
const res = await api.get("/expenses", { params: { limit: 0 } }); const res = await api.get("/expenses", { params: { limit: 0 } });
const all: any[] = res.data?.items || res.data || []; const all: any[] = res.data?.items || res.data || [];
@@ -96,7 +105,7 @@ export async function fetchAggregatedData(
const normalize = (amt: number) => Math.abs(amt); const normalize = (amt: number) => Math.abs(amt);
// ---------------- WEEK ---------------- // ---------------- DAILY ----------------
const dailyBuckets: Record<string, number> = { const dailyBuckets: Record<string, number> = {
Mon: 0, Tue: 0, Wed: 0, Thu: 0, Mon: 0, Tue: 0, Wed: 0, Thu: 0,
Fri: 0, Sat: 0, Sun: 0 Fri: 0, Sat: 0, Sun: 0
@@ -105,77 +114,81 @@ export async function fetchAggregatedData(
const weekStart = getStartOfWeek(now); const weekStart = getStartOfWeek(now);
const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000)); const weekEnd = endOfDay(new Date(weekStart.getTime() + 6 * 86400000));
// ---------------- MONTH (rolling 5 weeks, MonSun aligned) ---------------- // ---------------- WEEKLY ----------------
const weeklyBuckets = []; const weeklyRolling: any[] = [];
const weeklyCalendar: any[] = [];
if (mode === "rolling") { const currentWeekStart = getStartOfWeek(now);
const currentWeekStart = getStartOfWeek(now);
for (let i = 0; i < 5; i++) { // rolling (last 5 weeks, oldest → newest)
const start = new Date(currentWeekStart.getTime() - i * 7 * 86400000); for (let i = 4; i >= 0; i--) {
const end = endOfDay(new Date(start.getTime() + 6 * 86400000)); const start = new Date(currentWeekStart.getTime() - i * 7 * 86400000);
const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
weeklyBuckets.push({ weeklyRolling.push({
label: `${format(start)} - ${format(end)}`, label: `${format(start)} - ${format(end)}`,
start, start,
end, end,
amount: 0 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) ---------------- // calendar (full weeks covering current month)
const monthlyBuckets = []; const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
if (mode === "rolling") { const firstWeekStart = getStartOfWeek(startOfMonth);
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 totalWeeks = Math.ceil(
(endOfMonth.getTime() - firstWeekStart.getTime()) / (7 * 86400000)
) + 1;
const end = for (let i = 0; i < totalWeeks; i++) {
i === 0 const start = new Date(firstWeekStart.getTime() + i * 7 * 86400000);
? endOfDay(now) // current month → till now const end = endOfDay(new Date(start.getTime() + 6 * 86400000));
: 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({ weeklyCalendar.push({
label: `${start.toLocaleString("default", { month: "short" })}-${String(start.getFullYear()).slice(2)}`, label: `${format(start)} - ${format(end)}`,
start, start,
end, end,
amount: 0 amount: 0
}); });
} }
// ---------------- MONTHLY ----------------
const monthlyRolling: any[] = [];
const monthlyCalendar: any[] = [];
// rolling (last 12 months, oldest → newest)
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));
monthlyRolling.push({
label: `${d.toLocaleString("default", { month: "short" })}-${String(d.getFullYear()).slice(2)}`,
start,
end,
amount: 0
});
}
// calendar (full year Jan → Dec)
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));
monthlyCalendar.push({
label: `${start.toLocaleString("default", { month: "short" })}-${String(start.getFullYear()).slice(2)}`,
start,
end,
amount: 0
});
} }
// ---------------- LOOP ---------------- // ---------------- LOOP ----------------
@@ -194,7 +207,7 @@ export async function fetchAggregatedData(
const payee = item.payee?.name || item.payee || "Unknown"; const payee = item.payee?.name || item.payee || "Unknown";
payeeMap[payee] = (payeeMap[payee] || 0) + amt; payeeMap[payee] = (payeeMap[payee] || 0) + amt;
// WEEK // DAILY
if (d >= weekStart && d <= weekEnd) { if (d >= weekStart && d <= weekEnd) {
const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()]; const day = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()];
if (dailyBuckets[day] !== undefined) { if (dailyBuckets[day] !== undefined) {
@@ -202,42 +215,51 @@ export async function fetchAggregatedData(
} }
} }
// MONTH (rolling weeks) // WEEKLY
for (const b of weeklyBuckets) { for (const b of weeklyRolling) {
if (d >= b.start && d <= b.end) { if (d >= b.start && d <= b.end) b.amount += amt;
b.amount += amt; }
} for (const b of weeklyCalendar) {
if (d >= b.start && d <= b.end) b.amount += amt;
} }
// YEAR (rolling months) // MONTHLY
for (const b of monthlyBuckets) { for (const b of monthlyRolling) {
if (d >= b.start && d <= b.end) { if (d >= b.start && d <= b.end) b.amount += amt;
b.amount += amt; }
} for (const b of monthlyCalendar) {
if (d >= b.start && d <= b.end) b.amount += amt;
} }
} }
const toPoints = (b: any): ChartDataPoint[] => const toPoints = (arr: any[]): ChartDataPoint[] =>
Array.isArray(b) arr.map((x) => ({
? b.map((x) => ({ id: x.label,
id: x.label, amount: x.amount
amount: x.amount }));
}))
: Object.entries(b).map(([k, v]: any) => ({
id: k,
amount: v
}));
const chartData = { const chartData: ChartData = {
daily: toPoints(dailyBuckets), daily: Object.entries(dailyBuckets).map(([k, v]) => ({
weekly: toPoints(weeklyBuckets), id: k,
monthly: toPoints(monthlyBuckets) amount: v
})),
weekly: {
rolling: toPoints(weeklyRolling),
calendar: toPoints(weeklyCalendar)
},
monthly: {
rolling: toPoints(monthlyRolling),
calendar: toPoints(monthlyCalendar)
}
}; };
// highlight max // highlight max (per visible set default to rolling)
Object.values(chartData).forEach(group => { Object.values(chartData).forEach((group: any) => {
let max = group[0]; const arr = Array.isArray(group) ? group : group.rolling;
for (const g of group) { if (!arr?.length) return;
let max = arr[0];
for (const g of arr) {
if (g.amount > max.amount) max = g; if (g.amount > max.amount) max = g;
} }
if (max.amount > 0) max.highlighted = true; if (max.amount > 0) max.highlighted = true;
@@ -252,12 +274,8 @@ export async function fetchAggregatedData(
} }
// ---------------- EXPORTS ---------------- // ---------------- EXPORTS ----------------
export const fetchAggregatedExpenses = ( export const fetchAggregatedExpenses = () =>
mode: "rolling" | "calendar" fetchAggregatedData("expense");
) =>
fetchAggregatedData("expense", mode);
export const fetchAggregatedIncome = ( export const fetchAggregatedIncome = () =>
mode: "rolling" | "calendar" fetchAggregatedData("income");
) =>
fetchAggregatedData("income", mode);