dashboard using report feature
This commit is contained in:
@@ -1,10 +1,15 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
import MonetizationOnIcon from "@mui/icons-material/MonetizationOn";
|
||||||
import { LatestItem } from "../../components/LatestItems";
|
import { LatestItem } from "../../components/LatestItems";
|
||||||
|
import {
|
||||||
|
ChartData,
|
||||||
|
ChartDataPoint,
|
||||||
|
} from "../../components/HistoryChart";
|
||||||
|
|
||||||
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
const DEFAULT_ICON = React.createElement(MonetizationOnIcon, {
|
||||||
sx: { color: "#388e3c" }
|
sx: { color: "#388e3c" }
|
||||||
});
|
});
|
||||||
|
type ReportBucket = any;
|
||||||
|
|
||||||
export function mapToLatestItems(
|
export function mapToLatestItems(
|
||||||
items: any[],
|
items: any[],
|
||||||
@@ -17,9 +22,7 @@ export function mapToLatestItems(
|
|||||||
.filter((item: any) => isValid(Number(item.amount) || 0))
|
.filter((item: any) => isValid(Number(item.amount) || 0))
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map((exp: any, index: number) => {
|
.map((exp: any, index: number) => {
|
||||||
const time = new Date(
|
const time = new Date(exp.occurred_at).getTime();
|
||||||
exp.occurred_at || exp.created_at || Date.now()
|
|
||||||
).getTime();
|
|
||||||
|
|
||||||
const diffDays = Math.floor(
|
const diffDays = Math.floor(
|
||||||
Math.abs(Date.now() - time) / (1000 * 60 * 60 * 24)
|
Math.abs(Date.now() - time) / (1000 * 60 * 60 * 24)
|
||||||
@@ -30,11 +33,144 @@ export function mapToLatestItems(
|
|||||||
icon: DEFAULT_ICON,
|
icon: DEFAULT_ICON,
|
||||||
iconBgColor:
|
iconBgColor:
|
||||||
type === "expense" ? "#ffebee" : "#e8f5e9",
|
type === "expense" ? "#ffebee" : "#e8f5e9",
|
||||||
title: exp.payee?.name || exp.payee || "Unknown Payee",
|
title: exp.payee.name,
|
||||||
subtitle:
|
subtitle: exp.account.name,
|
||||||
exp.category?.name || exp.account?.name || "Transaction",
|
|
||||||
amount: `Rs ${Math.abs(exp.amount || 0)}`,
|
amount: `Rs ${Math.abs(exp.amount || 0)}`,
|
||||||
timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago`
|
timeAgo: diffDays === 0 ? "Today" : `${diffDays} days ago`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sumBucket = (bucket: ReportBucket, flow: "expenses" | "incomes") =>
|
||||||
|
bucket.groups.reduce(
|
||||||
|
(acc: number, g: any) => acc + (g?.[flow]?.sum || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const toLabel = (start: string, end: string, type: "weekly" | "monthly") => {
|
||||||
|
const s = new Date(start);
|
||||||
|
const e = new Date(end);
|
||||||
|
|
||||||
|
if (type === "monthly") {
|
||||||
|
return s.toLocaleString("default", { month: "short" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${s.getDate()}–${e.getDate()} ${e.toLocaleString("default", {
|
||||||
|
month: "short",
|
||||||
|
})}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeekOfMonth = (date: Date) => {
|
||||||
|
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||||
|
return Math.ceil((date.getDate() + firstDay.getDay()) / 7);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCompareBucket = (
|
||||||
|
current: ReportBucket,
|
||||||
|
buckets: ReportBucket[],
|
||||||
|
type: "weekly" | "monthly"
|
||||||
|
): ReportBucket | undefined => {
|
||||||
|
const start = new Date(current.start);
|
||||||
|
|
||||||
|
if (type === "monthly") {
|
||||||
|
const targetYear = start.getFullYear() - 1;
|
||||||
|
const targetMonth = start.getMonth();
|
||||||
|
|
||||||
|
return buckets.find(b => {
|
||||||
|
const d = new Date(b.start);
|
||||||
|
return (
|
||||||
|
d.getFullYear() === targetYear &&
|
||||||
|
d.getMonth() === targetMonth
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "weekly") {
|
||||||
|
const weekIndex = getWeekOfMonth(start); // you must define this
|
||||||
|
const target = new Date(start);
|
||||||
|
target.setMonth(target.getMonth() - 1);
|
||||||
|
|
||||||
|
return buckets.find(b => {
|
||||||
|
const d = new Date(b.start);
|
||||||
|
return (
|
||||||
|
d.getFullYear() === target.getFullYear() &&
|
||||||
|
d.getMonth() === target.getMonth() &&
|
||||||
|
getWeekOfMonth(d) === weekIndex
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toPoints = (
|
||||||
|
buckets: ReportBucket[],
|
||||||
|
type: "weekly" | "monthly",
|
||||||
|
flow: "expenses" | "incomes"
|
||||||
|
): ChartDataPoint[] => {
|
||||||
|
return buckets.map((b) => {
|
||||||
|
const amount = sumBucket(b, flow);
|
||||||
|
const prev = findCompareBucket(b, buckets, type);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: toLabel(b.start, b.end, type),
|
||||||
|
amount,
|
||||||
|
compare: prev
|
||||||
|
? {
|
||||||
|
id: toLabel(prev.start, prev.end, type),
|
||||||
|
amount: sumBucket(prev, flow),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mapReportToDashboard(
|
||||||
|
weekly: ReportBucket[],
|
||||||
|
monthly: ReportBucket[],
|
||||||
|
payeeBuckets: ReportBucket[],
|
||||||
|
type: "expense" | "income"
|
||||||
|
) {
|
||||||
|
const flow = type === "expense" ? "expenses" : "incomes";
|
||||||
|
|
||||||
|
const chartData: ChartData = {
|
||||||
|
weekly: {
|
||||||
|
rolling: toPoints(weekly, "weekly", flow),
|
||||||
|
calendar: toPoints(weekly, "weekly", flow),
|
||||||
|
},
|
||||||
|
|
||||||
|
monthly: {
|
||||||
|
rolling: toPoints(monthly, "monthly", flow),
|
||||||
|
calendar: toPoints(monthly, "monthly", flow),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalAmount = weekly.reduce(
|
||||||
|
(acc, b) => acc + sumBucket(b, flow),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const payeeMap: Record<string, number> = {};
|
||||||
|
|
||||||
|
const sourceForPayees = (payeeBuckets && payeeBuckets.length > 0) ? payeeBuckets : weekly;
|
||||||
|
|
||||||
|
for (const b of sourceForPayees) {
|
||||||
|
for (const g of b.groups) {
|
||||||
|
const key = g.group_key || "Unknown";
|
||||||
|
const amt = g?.[flow]?.sum || 0;
|
||||||
|
payeeMap[key] = (payeeMap[key] || 0) + amt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const topPayees = Object.entries(payeeMap)
|
||||||
|
// .filter(([name]) => name !== "Unknown")
|
||||||
|
.map(([payeeName, amount]) => ({ payeeName, amount }))
|
||||||
|
.sort((a, b) => b.amount - a.amount)
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartData,
|
||||||
|
totalAmount,
|
||||||
|
topPayees,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,38 +1,22 @@
|
|||||||
import { useResourceByName } from "../../../react-openapi";
|
import { useReport } from "../report";
|
||||||
import { mapToLatestItems } from "./dashboard.mapper";
|
import { mapReportToDashboard } from "./dashboard.mapper";
|
||||||
import { mapReportToDashboard } from "../report/report.mapper";
|
|
||||||
|
|
||||||
export function useDashboardData(type: "expense" | "income") {
|
export function useDashboardData(type: "expense" | "income") {
|
||||||
const { useList: useExpenseList } = useResourceByName("expenses");
|
|
||||||
const { useList: useReportList } = useResourceByName("reports");
|
|
||||||
|
|
||||||
// Fetch latest transactions
|
|
||||||
const latestQuery = useExpenseList({
|
|
||||||
limit: 100,
|
|
||||||
sort: "-occurred_at"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch reports for aggregation
|
// Fetch reports for aggregation
|
||||||
const weeklyReport = useReportList({ period: "weekly", rolling: true });
|
const weeklyReport = useReport({ period: "weekly", rolling: true });
|
||||||
const monthlyReport = useReportList({ period: "monthly", rolling: true });
|
const monthlyReport = useReport({ period: "monthly", rolling: true });
|
||||||
const payeeReport = useReportList({ period: "full", rolling: true, group_by: "payee" });
|
const payeeReport = useReport({ period: "full", rolling: true, group_by: ["payee"] });
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
latestQuery.isLoading ||
|
weeklyReport.isLoading ||
|
||||||
weeklyReport.isLoading ||
|
|
||||||
monthlyReport.isLoading ||
|
monthlyReport.isLoading ||
|
||||||
payeeReport.isLoading;
|
payeeReport.isLoading;
|
||||||
|
|
||||||
const error =
|
const error =
|
||||||
latestQuery.error ||
|
weeklyReport.error ||
|
||||||
weeklyReport.error ||
|
|
||||||
monthlyReport.error ||
|
monthlyReport.error ||
|
||||||
payeeReport.error;
|
payeeReport.error;
|
||||||
|
|
||||||
const latest = latestQuery.data?.data
|
|
||||||
? mapToLatestItems(latestQuery.data.data, type)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const aggregatedData =
|
const aggregatedData =
|
||||||
weeklyReport.data?.data && monthlyReport.data?.data && payeeReport.data?.data
|
weeklyReport.data?.data && monthlyReport.data?.data && payeeReport.data?.data
|
||||||
? mapReportToDashboard(
|
? mapReportToDashboard(
|
||||||
@@ -45,7 +29,6 @@ export function useDashboardData(type: "expense" | "income") {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: aggregatedData,
|
data: aggregatedData,
|
||||||
latest: latest,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user