Compare commits
36 Commits
main
...
df5cf9fbb6
| Author | SHA1 | Date | |
|---|---|---|---|
| df5cf9fbb6 | |||
| 4b046c15a5 | |||
| 02eb55995e | |||
| 4e56d86cdb | |||
| 15f76eb5f0 | |||
| 7470da6d2d | |||
| 34594215f9 | |||
| 0a92126b92 | |||
| 30cf227050 | |||
| a0e62b1bc4 | |||
| ea3b451266 | |||
| 4b4875c3f5 | |||
| 25bd882b75 | |||
| f684083496 | |||
| 0e0928af95 | |||
| 7b0b3fb615 | |||
| 38f7416942 | |||
| e82cad4f21 | |||
| 1daa90d091 | |||
| 2d0b0bc470 | |||
| 5f85abdf86 | |||
| cc7e6509d2 | |||
| 8a3ebdb1be | |||
| a36d9119bb | |||
| 67d4c85146 | |||
| 89ad8e376e | |||
| 71afc157ff | |||
| 5acbb7ccdd | |||
| 3fd20f11ab | |||
| 922d05ae37 | |||
| 1fe44abfde | |||
| 49bdb85088 | |||
| b1509fd5ab | |||
| 175ca64d1f | |||
| c9e609fee6 | |||
| 82264a5c34 |
@@ -19,6 +19,7 @@ export interface DashboardSection {
|
|||||||
title?: string;
|
title?: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
component: React.ComponentType<any>;
|
component: React.ComponentType<any>;
|
||||||
|
dataKey: string;
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
isList?: boolean;
|
isList?: boolean;
|
||||||
style?: {
|
style?: {
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ type DecoratedPeriod = ReportPeriod & {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAB_TO_KEY: Record<string, "weekly" | "monthly" | "yearly" | "fyly" | "full"> = {
|
const TAB_TO_KEY: Record<string, "weekly" | "monthly" | "yearly" | "fyly"> = {
|
||||||
Weekly: "weekly",
|
Weekly: "weekly",
|
||||||
Monthly: "monthly",
|
Monthly: "monthly",
|
||||||
Yearly: "yearly",
|
Yearly: "yearly",
|
||||||
'Financial Year': "fyly",
|
FYLY: "fyly"
|
||||||
'All Time': "full"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAmount(p: ReportPeriod, mode: "expense" | "income") {
|
function getAmount(p: ReportPeriod, mode: "expense" | "income") {
|
||||||
@@ -40,7 +39,7 @@ function mergeMetric(a: any, b: any) {
|
|||||||
|
|
||||||
function mergeBuckets(
|
function mergeBuckets(
|
||||||
buckets: any[],
|
buckets: any[],
|
||||||
key: "weekly" | "monthly" | "yearly" | "fyly" | "full"
|
key: "weekly" | "monthly" | "yearly" | "fyly"
|
||||||
): DecoratedPeriod[] {
|
): DecoratedPeriod[] {
|
||||||
const map = new Map<string, DecoratedPeriod>();
|
const map = new Map<string, DecoratedPeriod>();
|
||||||
|
|
||||||
@@ -71,38 +70,9 @@ function mergeBuckets(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachComparison(
|
|
||||||
points: ChartDataPoint[],
|
|
||||||
key: "weekly" | "monthly" | "yearly" | "fyly" | "full"
|
|
||||||
): ChartDataPoint[] {
|
|
||||||
const getCompareIndex = (i: number) => {
|
|
||||||
if (key === "weekly") return i - 4;
|
|
||||||
if (key === "monthly") return i - 12;
|
|
||||||
if (key === "yearly") return i - 1;
|
|
||||||
if (key === "fyly") return i - 1;
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
return points.map((p, i) => {
|
|
||||||
const ci = getCompareIndex(i);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...p,
|
|
||||||
compare:
|
|
||||||
ci >= 0 && points[ci]
|
|
||||||
? {
|
|
||||||
id: points[ci].id,
|
|
||||||
label: points[ci].label,
|
|
||||||
amount: points[ci].amount
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildChartData(
|
function buildChartData(
|
||||||
reportData: HistoryChartProps["reportData"],
|
reportData: HistoryChartProps["reportData"],
|
||||||
key: "weekly" | "monthly" | "yearly" | "fyly" | "full",
|
key: "weekly" | "monthly" | "yearly" | "fyly",
|
||||||
mode: "expense" | "income",
|
mode: "expense" | "income",
|
||||||
comparison: boolean
|
comparison: boolean
|
||||||
): ChartDataPoint[] {
|
): ChartDataPoint[] {
|
||||||
@@ -116,7 +86,17 @@ function buildChartData(
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (comparison) {
|
if (comparison) {
|
||||||
points = attachComparison(points, key);
|
points = points.map((p, i) => ({
|
||||||
|
...p,
|
||||||
|
compare:
|
||||||
|
i > 0
|
||||||
|
? {
|
||||||
|
id: points[i - 1].id,
|
||||||
|
label: points[i - 1].label,
|
||||||
|
amount: points[i - 1].amount
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return points;
|
return points;
|
||||||
@@ -158,8 +138,7 @@ export default function HistoryChart(props: HistoryChartProps) {
|
|||||||
weekly: 6,
|
weekly: 6,
|
||||||
monthly: 4,
|
monthly: 4,
|
||||||
yearly: 4,
|
yearly: 4,
|
||||||
fyly: 4,
|
fyly: 4
|
||||||
full: 4,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const visibleCount = visibleCountMap[activeDataKey] ?? 4;
|
const visibleCount = visibleCountMap[activeDataKey] ?? 4;
|
||||||
|
|||||||
@@ -58,28 +58,19 @@ export default function HistoryChartView(props: ViewProps) {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.palette.mode === "dark";
|
const isDark = theme.palette.mode === "dark";
|
||||||
|
|
||||||
const total = currentData.length;
|
|
||||||
const maxStartIndex = Math.max(total - visibleCount, 0);
|
|
||||||
const clampedStartIndex = Math.min(startIndex, maxStartIndex);
|
|
||||||
|
|
||||||
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
|
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
|
||||||
if (newTab !== null) setActiveTab(newTab);
|
if (newTab !== null) setActiveTab(newTab);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canGoLeft = clampedStartIndex > 0;
|
const canGoLeft = startIndex > 0;
|
||||||
const canGoRight = clampedStartIndex < maxStartIndex;
|
const canGoRight = startIndex + visibleCount < currentData.length;
|
||||||
|
|
||||||
const handlePrev = () => {
|
const handlePrev = () => {
|
||||||
if (!canGoLeft) return;
|
if (canGoLeft) setStartIndex((prev) => prev - visibleCount);
|
||||||
setStartIndex((prev) => Math.max(prev - visibleCount, 0));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (!canGoRight) return;
|
if (canGoRight) setStartIndex((prev) => prev + visibleCount);
|
||||||
setStartIndex((prev) => {
|
|
||||||
const next = prev + visibleCount;
|
|
||||||
return Math.min(next, maxStartIndex);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -157,8 +148,7 @@ export default function HistoryChartView(props: ViewProps) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
cursor: "pointer",
|
cursor: "pointer"
|
||||||
height: "100%"
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "flex-end", gap: 1, height: "100%" }}>
|
<Box sx={{ display: "flex", alignItems: "flex-end", gap: 1, height: "100%" }}>
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Box } from "@mui/material";
|
|
||||||
import { ReportData, ReportPeriod } from "../../features/report";
|
|
||||||
import ProgressCard from "./ProgressCard";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
reportData: ReportData;
|
|
||||||
mode: "expense" | "income";
|
|
||||||
selectedPeriodId?: string | null;
|
|
||||||
compact?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DecoratedPeriod = ReportPeriod & {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getAmount(p: ReportPeriod, mode: "expense" | "income") {
|
|
||||||
return mode === "expense" ? p.expenses.sum : p.incomes.sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPeriod(
|
|
||||||
periods: DecoratedPeriod[],
|
|
||||||
selectedPeriodId?: string | null
|
|
||||||
) {
|
|
||||||
if (!periods.length) return null;
|
|
||||||
|
|
||||||
if (selectedPeriodId) {
|
|
||||||
const match = periods.find((p) => p.id === selectedPeriodId);
|
|
||||||
if (match) return match;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback → latest
|
|
||||||
return periods.reduce((latest, p) =>
|
|
||||||
new Date(p.start).getTime() > new Date(latest.start).getTime()
|
|
||||||
? p
|
|
||||||
: latest
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TopTags({
|
|
||||||
reportData,
|
|
||||||
mode,
|
|
||||||
selectedPeriodId,
|
|
||||||
compact = true
|
|
||||||
}: Props) {
|
|
||||||
const { items, total } = React.useMemo(() => {
|
|
||||||
const tagMap = new Map<string, number>();
|
|
||||||
|
|
||||||
for (const bucket of reportData.buckets) {
|
|
||||||
const tags = bucket.group_key.tags;
|
|
||||||
if (!tags || tags.length === 0) continue;
|
|
||||||
|
|
||||||
// Prefer FULL if available
|
|
||||||
const fullPeriods = (bucket.periods.full || []) as DecoratedPeriod[];
|
|
||||||
|
|
||||||
const periodsToUse =
|
|
||||||
selectedPeriodId
|
|
||||||
? Object.values(bucket.periods).flat() as DecoratedPeriod[]
|
|
||||||
: fullPeriods;
|
|
||||||
|
|
||||||
const period = findPeriod(periodsToUse, selectedPeriodId);
|
|
||||||
if (!period) continue;
|
|
||||||
|
|
||||||
const amount = getAmount(period, mode);
|
|
||||||
|
|
||||||
for (const tag of tags) {
|
|
||||||
tagMap.set(tag, (tagMap.get(tag) || 0) + amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const arr = Array.from(tagMap.entries()).map(([tag, amount]) => ({
|
|
||||||
tag,
|
|
||||||
amount
|
|
||||||
}));
|
|
||||||
|
|
||||||
arr.sort((a, b) => b.amount - a.amount);
|
|
||||||
|
|
||||||
const top = arr.slice(0, 4);
|
|
||||||
const total = top.reduce((sum, t) => sum + t.amount, 0);
|
|
||||||
|
|
||||||
return { items: top, total };
|
|
||||||
}, [reportData, mode, selectedPeriodId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: {
|
|
||||||
xs: "1fr",
|
|
||||||
sm: "repeat(2, 1fr)",
|
|
||||||
md: "repeat(4, 1fr)"
|
|
||||||
},
|
|
||||||
gap: 2
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((item) => (
|
|
||||||
<ProgressCard
|
|
||||||
key={item.tag}
|
|
||||||
header={item.tag}
|
|
||||||
progressAmount={item.amount}
|
|
||||||
totalAmount={total}
|
|
||||||
compact={compact}
|
|
||||||
colorTheme={mode === "expense" ? "error" : "success"}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import HistoryChart from "./components/HistoryChart";
|
import HistoryChart from "./components/HistoryChart";
|
||||||
|
import ProgressCard from "./components/ProgressCard";
|
||||||
import LatestItems from "./components/LatestItems";
|
import LatestItems from "./components/LatestItems";
|
||||||
import { DashboardConfig } from "./components/Dashboard";
|
import { DashboardConfig } from "./components/Dashboard";
|
||||||
import TopTags from "./components/ProgressCard/TopTags";
|
|
||||||
|
|
||||||
export const configuration: DashboardConfig = {
|
export const configuration: DashboardConfig = {
|
||||||
sections: [
|
sections: [
|
||||||
@@ -10,6 +10,7 @@ export const configuration: DashboardConfig = {
|
|||||||
title: "Breakdown",
|
title: "Breakdown",
|
||||||
summary: "Interactive chronological tracking",
|
summary: "Interactive chronological tracking",
|
||||||
component: HistoryChart,
|
component: HistoryChart,
|
||||||
|
dataKey: "chartData",
|
||||||
settings: {
|
settings: {
|
||||||
tabs: ["Weekly", "Monthly"],
|
tabs: ["Weekly", "Monthly"],
|
||||||
// tabs: ["Weekly", "Monthly", "Yearly", "Financial Year", "All Time"],
|
// tabs: ["Weekly", "Monthly", "Yearly", "Financial Year", "All Time"],
|
||||||
@@ -21,7 +22,9 @@ export const configuration: DashboardConfig = {
|
|||||||
{
|
{
|
||||||
id: "top-payees",
|
id: "top-payees",
|
||||||
title: 'Top Payees',
|
title: 'Top Payees',
|
||||||
component: TopTags,
|
component: ProgressCard,
|
||||||
|
dataKey: "topPayees",
|
||||||
|
isList: true,
|
||||||
settings: {
|
settings: {
|
||||||
compact: true,
|
compact: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export interface ReportBucket {
|
|||||||
monthly?: ReportPeriod[];
|
monthly?: ReportPeriod[];
|
||||||
yearly?: ReportPeriod[];
|
yearly?: ReportPeriod[];
|
||||||
fyly?: ReportPeriod[];
|
fyly?: ReportPeriod[];
|
||||||
full?: ReportPeriod[];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +75,7 @@ export interface ReportBucket {
|
|||||||
// -----------------------------
|
// -----------------------------
|
||||||
|
|
||||||
export interface ReportData {
|
export interface ReportData {
|
||||||
periods: ("weekly" | "monthly" | "yearly" | "fyly" | "full")[];
|
periods: ("weekly" | "monthly" | "yearly" | "fyly")[];
|
||||||
|
|
||||||
rolling: boolean;
|
rolling: boolean;
|
||||||
report_date?: string;
|
report_date?: string;
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ function buildLabel(
|
|||||||
return `${dayFmt.format(start)} - ${dayFmt.format(end)}`;
|
return `${dayFmt.format(start)} - ${dayFmt.format(end)}`;
|
||||||
|
|
||||||
case "monthly":
|
case "monthly":
|
||||||
return `${monthFmt.format(start)} ${yearFmt.format(start)}`;
|
if (sameMonth(start, end)) {
|
||||||
|
return `${monthFmt.format(start)} ${yearFmt.format(start)}`;
|
||||||
|
}
|
||||||
|
return `${monthDayFmt.format(start)} - ${monthDayFmt.format(end)}`;
|
||||||
|
|
||||||
case "yearly":
|
case "yearly":
|
||||||
return yearFmt.format(start);
|
return yearFmt.format(start);
|
||||||
@@ -107,8 +110,8 @@ function decoratePeriods(
|
|||||||
): (ReportPeriod & { id: string; label: string })[] {
|
): (ReportPeriod & { id: string; label: string })[] {
|
||||||
return periods.map((p) => ({
|
return periods.map((p) => ({
|
||||||
...p,
|
...p,
|
||||||
id: buildPeriodId(type, new Date(p.start + "Z"), new Date(p.end + "Z")),
|
id: buildPeriodId(type, new Date(p.start), new Date(p.end)),
|
||||||
label: buildLabel(type, new Date(p.start + "Z"), new Date(p.end + "Z")),
|
label: buildLabel(type, new Date(p.start), new Date(p.end)),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user