Compare commits
5 Commits
df5cf9fbb6
...
089381125f
| Author | SHA1 | Date | |
|---|---|---|---|
| 089381125f | |||
| 1a5261ab25 | |||
| 55b604f700 | |||
| 04b72d5843 | |||
| b97e350b1b |
@@ -19,7 +19,6 @@ export interface DashboardSection {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
component: React.ComponentType<any>;
|
||||
dataKey: string;
|
||||
settings?: Record<string, any>;
|
||||
isList?: boolean;
|
||||
style?: {
|
||||
|
||||
@@ -8,11 +8,12 @@ type DecoratedPeriod = ReportPeriod & {
|
||||
label: string;
|
||||
};
|
||||
|
||||
const TAB_TO_KEY: Record<string, "weekly" | "monthly" | "yearly" | "fyly"> = {
|
||||
const TAB_TO_KEY: Record<string, "weekly" | "monthly" | "yearly" | "fyly" | "full"> = {
|
||||
Weekly: "weekly",
|
||||
Monthly: "monthly",
|
||||
Yearly: "yearly",
|
||||
FYLY: "fyly"
|
||||
'Financial Year': "fyly",
|
||||
'All Time': "full"
|
||||
};
|
||||
|
||||
function getAmount(p: ReportPeriod, mode: "expense" | "income") {
|
||||
@@ -39,7 +40,7 @@ function mergeMetric(a: any, b: any) {
|
||||
|
||||
function mergeBuckets(
|
||||
buckets: any[],
|
||||
key: "weekly" | "monthly" | "yearly" | "fyly"
|
||||
key: "weekly" | "monthly" | "yearly" | "fyly" | "full"
|
||||
): DecoratedPeriod[] {
|
||||
const map = new Map<string, DecoratedPeriod>();
|
||||
|
||||
@@ -70,9 +71,38 @@ 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(
|
||||
reportData: HistoryChartProps["reportData"],
|
||||
key: "weekly" | "monthly" | "yearly" | "fyly",
|
||||
key: "weekly" | "monthly" | "yearly" | "fyly" | "full",
|
||||
mode: "expense" | "income",
|
||||
comparison: boolean
|
||||
): ChartDataPoint[] {
|
||||
@@ -86,17 +116,7 @@ function buildChartData(
|
||||
}));
|
||||
|
||||
if (comparison) {
|
||||
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
|
||||
}));
|
||||
points = attachComparison(points, key);
|
||||
}
|
||||
|
||||
return points;
|
||||
@@ -138,7 +158,8 @@ export default function HistoryChart(props: HistoryChartProps) {
|
||||
weekly: 6,
|
||||
monthly: 4,
|
||||
yearly: 4,
|
||||
fyly: 4
|
||||
fyly: 4,
|
||||
full: 4,
|
||||
};
|
||||
|
||||
const visibleCount = visibleCountMap[activeDataKey] ?? 4;
|
||||
|
||||
@@ -148,7 +148,8 @@ export default function HistoryChartView(props: ViewProps) {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
cursor: "pointer"
|
||||
cursor: "pointer",
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-end", gap: 1, height: "100%" }}>
|
||||
|
||||
98
src/components/ProgressCard/TopTags.tsx
Normal file
98
src/components/ProgressCard/TopTags.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
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, 5);
|
||||
const total = top.reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
return { items: top, total };
|
||||
}, [reportData, mode, selectedPeriodId]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: 2 }}>
|
||||
{items.map((item) => (
|
||||
<ProgressCard
|
||||
key={item.tag}
|
||||
header={item.tag}
|
||||
progressAmount={item.amount}
|
||||
totalAmount={total}
|
||||
compact={compact}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import HistoryChart from "./components/HistoryChart";
|
||||
import ProgressCard from "./components/ProgressCard";
|
||||
import LatestItems from "./components/LatestItems";
|
||||
import { DashboardConfig } from "./components/Dashboard";
|
||||
import TopTags from "./components/ProgressCard/TopTags";
|
||||
|
||||
export const configuration: DashboardConfig = {
|
||||
sections: [
|
||||
@@ -10,7 +10,6 @@ export const configuration: DashboardConfig = {
|
||||
title: "Breakdown",
|
||||
summary: "Interactive chronological tracking",
|
||||
component: HistoryChart,
|
||||
dataKey: "chartData",
|
||||
settings: {
|
||||
tabs: ["Weekly", "Monthly"],
|
||||
// tabs: ["Weekly", "Monthly", "Yearly", "Financial Year", "All Time"],
|
||||
@@ -22,8 +21,7 @@ export const configuration: DashboardConfig = {
|
||||
{
|
||||
id: "top-payees",
|
||||
title: 'Top Payees',
|
||||
component: ProgressCard,
|
||||
dataKey: "topPayees",
|
||||
component: TopTags,
|
||||
isList: true,
|
||||
settings: {
|
||||
compact: true,
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface ReportBucket {
|
||||
monthly?: ReportPeriod[];
|
||||
yearly?: ReportPeriod[];
|
||||
fyly?: ReportPeriod[];
|
||||
full?: ReportPeriod[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ export interface ReportBucket {
|
||||
// -----------------------------
|
||||
|
||||
export interface ReportData {
|
||||
periods: ("weekly" | "monthly" | "yearly" | "fyly")[];
|
||||
periods: ("weekly" | "monthly" | "yearly" | "fyly" | "full")[];
|
||||
|
||||
rolling: boolean;
|
||||
report_date?: string;
|
||||
|
||||
Reference in New Issue
Block a user