rolling and calender toggle
This commit is contained in:
@@ -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];
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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, Mon–Sun 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);
|
|
||||||
|
|||||||
Reference in New Issue
Block a user