211 lines
4.9 KiB
TypeScript
211 lines
4.9 KiB
TypeScript
import * as React from "react";
|
|
import { HistoryChartProps, ChartDataPoint } from "./HistoryChart.models";
|
|
import HistoryChartView from "./HistoryChart.view";
|
|
import { ReportPeriod } from "../../features/report";
|
|
|
|
type DecoratedPeriod = ReportPeriod & {
|
|
id: string;
|
|
label: string;
|
|
};
|
|
|
|
const TAB_TO_KEY: Record<string, "weekly" | "monthly" | "yearly" | "fyly"> = {
|
|
Weekly: "weekly",
|
|
Monthly: "monthly",
|
|
Yearly: "yearly",
|
|
FYLY: "fyly"
|
|
};
|
|
|
|
function getAmount(p: ReportPeriod, mode: "expense" | "income") {
|
|
return mode === "expense" ? p.expenses.sum : p.incomes.sum;
|
|
}
|
|
|
|
function mergeMetric(a: any, b: any) {
|
|
const sum = a.sum + b.sum;
|
|
const count = a.count + b.count;
|
|
|
|
return {
|
|
...a,
|
|
sum,
|
|
count,
|
|
average: count > 0 ? sum / count : 0,
|
|
transactions: a.transactions || b.transactions
|
|
? [
|
|
...(a.transactions || []),
|
|
...(b.transactions || [])
|
|
]
|
|
: undefined
|
|
};
|
|
}
|
|
|
|
function mergeBuckets(
|
|
buckets: any[],
|
|
key: "weekly" | "monthly" | "yearly" | "fyly"
|
|
): DecoratedPeriod[] {
|
|
const map = new Map<string, DecoratedPeriod>();
|
|
|
|
for (const bucket of buckets) {
|
|
const periods = (bucket.periods[key] || []) as DecoratedPeriod[];
|
|
|
|
for (const p of periods) {
|
|
const existing = map.get(p.id);
|
|
|
|
if (!existing) {
|
|
map.set(p.id, {
|
|
...p,
|
|
expenses: { ...p.expenses },
|
|
incomes: { ...p.incomes }
|
|
});
|
|
} else {
|
|
map.set(p.id, {
|
|
...existing,
|
|
expenses: mergeMetric(existing.expenses, p.expenses),
|
|
incomes: mergeMetric(existing.incomes, p.incomes)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(map.values()).sort(
|
|
(a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()
|
|
);
|
|
}
|
|
|
|
function attachComparison(
|
|
points: ChartDataPoint[],
|
|
key: "weekly" | "monthly" | "yearly" | "fyly"
|
|
): 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",
|
|
mode: "expense" | "income",
|
|
comparison: boolean
|
|
): ChartDataPoint[] {
|
|
const merged = mergeBuckets(reportData.buckets, key);
|
|
console.log("Merged periods:", merged);
|
|
|
|
let points: ChartDataPoint[] = merged.map((p) => ({
|
|
id: p.id,
|
|
label: p.label,
|
|
amount: getAmount(p, mode)
|
|
}));
|
|
|
|
if (comparison) {
|
|
points = attachComparison(points, key);
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
export default function HistoryChart(props: HistoryChartProps) {
|
|
const {
|
|
tabs,
|
|
reportData,
|
|
mode,
|
|
periodType,
|
|
comparison,
|
|
selectedPeriodId,
|
|
setSelectedPeriodId
|
|
} = props;
|
|
|
|
const [activeTab, setActiveTab] = React.useState<string>(tabs[0] || "");
|
|
const [startIndex, setStartIndex] = React.useState(0);
|
|
|
|
const activeDataKey = TAB_TO_KEY[activeTab];
|
|
|
|
const currentData = React.useMemo(() => {
|
|
return buildChartData(reportData, activeDataKey, mode, comparison);
|
|
}, [reportData, activeDataKey, mode, comparison]);
|
|
|
|
const maxAmount =
|
|
currentData.length > 0
|
|
? Math.max(
|
|
...currentData.flatMap((d) =>
|
|
comparison
|
|
? [d.amount, ...(d.compare ? [d.compare.amount] : [])]
|
|
: [d.amount]
|
|
),
|
|
1
|
|
)
|
|
: 1;
|
|
|
|
const visibleCountMap = {
|
|
weekly: 6,
|
|
monthly: 4,
|
|
yearly: 4,
|
|
fyly: 4
|
|
};
|
|
|
|
const visibleCount = visibleCountMap[activeDataKey] ?? 4;
|
|
|
|
const total = currentData.length;
|
|
|
|
const clampedStartIndex = Math.min(
|
|
startIndex,
|
|
Math.max(total - visibleCount, 0)
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (startIndex !== clampedStartIndex) {
|
|
setStartIndex(clampedStartIndex);
|
|
}
|
|
}, [startIndex, clampedStartIndex]);
|
|
|
|
const visibleData = currentData.slice(
|
|
clampedStartIndex,
|
|
clampedStartIndex + visibleCount
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
setSelectedPeriodId(null);
|
|
}, [activeTab, periodType]);
|
|
|
|
React.useEffect(() => {
|
|
if (
|
|
selectedPeriodId &&
|
|
!visibleData.some((p) => p.id === selectedPeriodId)
|
|
) {
|
|
setSelectedPeriodId(null);
|
|
}
|
|
}, [visibleData, selectedPeriodId]);
|
|
|
|
return (
|
|
<HistoryChartView
|
|
{...props}
|
|
activeTab={activeTab}
|
|
setActiveTab={setActiveTab}
|
|
currentData={currentData}
|
|
visibleData={visibleData}
|
|
maxAmount={maxAmount}
|
|
visibleCount={visibleCount}
|
|
startIndex={clampedStartIndex}
|
|
setStartIndex={setStartIndex}
|
|
activeDataKey={activeDataKey}
|
|
/>
|
|
);
|
|
}
|