common component props
This commit is contained in:
@@ -16,6 +16,13 @@ export interface DashboardState {
|
||||
comparison: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardStateSetters {
|
||||
setSelectedPeriodId: (id: DashboardSelectedPeriodId) => void;
|
||||
setSelectedGroupKey: (groupKey: GroupKey | null) => void;
|
||||
togglePeriodType: () => void;
|
||||
toggleComparison: () => void;
|
||||
}
|
||||
|
||||
export interface DashboardSection {
|
||||
id: string;
|
||||
title?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import DashboardView from "./Dashboard.view";
|
||||
import { DashboardProps, DashboardState } from "./Dashboard.models";
|
||||
import { DashboardProps, DashboardState, DashboardStateSetters } from "./Dashboard.models";
|
||||
|
||||
export default function Dashboard(props: DashboardProps) {
|
||||
const [state, setState] = React.useState<DashboardState>({
|
||||
@@ -49,16 +49,20 @@ export default function Dashboard(props: DashboardProps) {
|
||||
setState(prev => ({ ...prev, selectedGroupKey: groupKey }));
|
||||
};
|
||||
|
||||
const stateSetters: DashboardStateSetters = {
|
||||
togglePeriodType,
|
||||
toggleComparison,
|
||||
setSelectedPeriodId,
|
||||
setSelectedGroupKey,
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardView
|
||||
{...props}
|
||||
state={state}
|
||||
setState={setState}
|
||||
toggleFlow={toggleFlow}
|
||||
togglePeriodType={togglePeriodType}
|
||||
toggleComparison={toggleComparison}
|
||||
setSelectedPeriodId={setSelectedPeriodId}
|
||||
setSelectedGroupKey={setSelectedGroupKey}
|
||||
stateSetters={stateSetters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,16 +10,13 @@ import {
|
||||
} from "@mui/material";
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { GroupKey } from "../../features/report";
|
||||
import { DashboardProps, DashboardState } from "./Dashboard.models";
|
||||
import { DashboardProps, DashboardState, DashboardStateSetters } from "./Dashboard.models";
|
||||
|
||||
interface ViewProps extends DashboardProps {
|
||||
state: DashboardState;
|
||||
setState: React.Dispatch<React.SetStateAction<DashboardState>>;
|
||||
toggleFlow: (event: React.MouseEvent<HTMLElement>, newFlow: "outflows" | "inflows" | null) => void;
|
||||
togglePeriodType: () => void;
|
||||
setSelectedPeriodId: (id: string | null) => void;
|
||||
setSelectedGroupKey: (groupKey: GroupKey | null) => void;
|
||||
toggleComparison: () => void;
|
||||
stateSetters: DashboardStateSetters;
|
||||
}
|
||||
|
||||
export default function DashboardView({
|
||||
@@ -28,14 +25,12 @@ export default function DashboardView({
|
||||
state,
|
||||
setState,
|
||||
toggleFlow,
|
||||
togglePeriodType,
|
||||
toggleComparison,
|
||||
setSelectedPeriodId,
|
||||
setSelectedGroupKey,
|
||||
stateSetters,
|
||||
}: ViewProps) {
|
||||
const theme = useTheme();
|
||||
const themeMode = theme.palette.mode;
|
||||
const { flow, periodType, comparison, selectedPeriodId, selectedGroupKey } = state;
|
||||
const { flow, selectedGroupKey } = state;
|
||||
const { setSelectedGroupKey } = stateSetters;
|
||||
|
||||
// Resolve colors with fallbacks
|
||||
const colors = React.useMemo(() => {
|
||||
@@ -121,17 +116,8 @@ export default function DashboardView({
|
||||
colorScheme={colors}
|
||||
|
||||
// State management
|
||||
flow={flow}
|
||||
|
||||
periodType={periodType}
|
||||
comparison={comparison}
|
||||
selectedPeriodId={selectedPeriodId}
|
||||
selectedGroupKey={selectedGroupKey}
|
||||
|
||||
togglePeriodType={togglePeriodType}
|
||||
toggleComparison={toggleComparison}
|
||||
setSelectedPeriodId={setSelectedPeriodId}
|
||||
setSelectedGroupKey={setSelectedGroupKey}
|
||||
state={state}
|
||||
stateSetters={stateSetters}
|
||||
isFetching={arguments[0].isFetching}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
DashboardFlow,
|
||||
DashboardPeriodType,
|
||||
DashboardSelectedPeriodId
|
||||
} from "../Dashboard";
|
||||
import { ReportData } from "../../features/report";
|
||||
import { ComponentProps } from "../report.props";
|
||||
|
||||
export interface _ChartDataPoint {
|
||||
id: string;
|
||||
@@ -16,27 +11,6 @@ export interface ChartDataPoint extends _ChartDataPoint {
|
||||
compare?: _ChartDataPoint;
|
||||
}
|
||||
|
||||
export interface HistoryChartProps {
|
||||
header: string;
|
||||
summary?: string;
|
||||
export interface HistoryChartProps extends ComponentProps {
|
||||
tabs: string[];
|
||||
|
||||
reportData: ReportData;
|
||||
|
||||
colorScheme: {
|
||||
primary: string;
|
||||
light: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
flow: DashboardFlow;
|
||||
periodType: DashboardPeriodType;
|
||||
selectedPeriodId: DashboardSelectedPeriodId;
|
||||
comparison: boolean;
|
||||
|
||||
togglePeriodType: () => void;
|
||||
setSelectedPeriodId: (id: string | null) => void;
|
||||
toggleComparison: () => void;
|
||||
|
||||
isFetching?: boolean;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ export default function HistoryChart(props: HistoryChartProps) {
|
||||
const {
|
||||
tabs,
|
||||
reportData,
|
||||
flow,
|
||||
comparison,
|
||||
selectedPeriodId,
|
||||
setSelectedPeriodId
|
||||
state,
|
||||
stateSetters,
|
||||
} = props;
|
||||
|
||||
const { flow, comparison, selectedPeriodId } = state;
|
||||
const { setSelectedPeriodId } = stateSetters;
|
||||
|
||||
const [activeTab, setActiveTab] = React.useState<string>(tabs[0] || "");
|
||||
const [startIndex, setStartIndex] = React.useState(0);
|
||||
|
||||
|
||||
@@ -35,14 +35,8 @@ export default function HistoryChartView(props: ViewProps) {
|
||||
tabs,
|
||||
colorScheme,
|
||||
|
||||
flow,
|
||||
periodType,
|
||||
selectedPeriodId,
|
||||
comparison,
|
||||
|
||||
togglePeriodType,
|
||||
setSelectedPeriodId,
|
||||
toggleComparison,
|
||||
state,
|
||||
stateSetters,
|
||||
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
@@ -55,6 +49,9 @@ export default function HistoryChartView(props: ViewProps) {
|
||||
activeDataKey,
|
||||
} = props;
|
||||
|
||||
const { flow, periodType, selectedPeriodId, comparison } = state;
|
||||
const { togglePeriodType, setSelectedPeriodId, toggleComparison } = stateSetters;
|
||||
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
|
||||
|
||||
@@ -1,67 +1,19 @@
|
||||
import { ReportData, Transaction, GroupKey } from "../../features/report";
|
||||
import { ReportData, GroupKey } from "../../features/report";
|
||||
import {
|
||||
mergeBucketPeriods,
|
||||
periodIdToKey,
|
||||
formatCurrency,
|
||||
filterBuckets,
|
||||
extractFilteredTransactions,
|
||||
} from "../report.helpers";
|
||||
import { LatestItem } from "./LatestItems.models";
|
||||
|
||||
// ─── Transaction extraction ─────────────────────────────────
|
||||
|
||||
function extractTransactions(
|
||||
reportData: ReportData,
|
||||
selectedPeriodId: string | null,
|
||||
selectedGroupKey: GroupKey | null,
|
||||
): Transaction[] {
|
||||
// 1. Get raw transactions
|
||||
let rawTxns: Transaction[] = [];
|
||||
|
||||
if (selectedPeriodId) {
|
||||
const key = periodIdToKey(selectedPeriodId);
|
||||
const periods = mergeBucketPeriods(reportData.buckets, key);
|
||||
const selected = periods.find((p) => p.id === selectedPeriodId);
|
||||
rawTxns = selected?.metric.transactions || [];
|
||||
} else {
|
||||
const periods = mergeBucketPeriods(reportData.buckets, "all");
|
||||
if (periods.length > 0) {
|
||||
rawTxns = periods[0].metric.transactions || [];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Filter by group key
|
||||
if (selectedGroupKey) {
|
||||
rawTxns = rawTxns.filter(txn => {
|
||||
let match = true;
|
||||
if (selectedGroupKey.tags && selectedGroupKey.tags.length > 0) {
|
||||
if (!txn.tags) match = false;
|
||||
else {
|
||||
const txnTags = txn.tags.map(t => typeof t === "string" ? t : t.name);
|
||||
if (!selectedGroupKey.tags.every(selectedTag => txnTags.includes(selectedTag))) match = false;
|
||||
}
|
||||
}
|
||||
if (match && selectedGroupKey.payee && selectedGroupKey.payee.length > 0) {
|
||||
if (!txn.payee || !txn.payee.name) match = false;
|
||||
else {
|
||||
if (!selectedGroupKey.payee.includes(txn.payee.name)) match = false;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
return rawTxns;
|
||||
}
|
||||
|
||||
// ─── Main adapter ────────────────────────────────────────────
|
||||
|
||||
export function buildLatestItems(
|
||||
reportData: ReportData,
|
||||
selectedPeriodId: string | null,
|
||||
selectedGroupKey: GroupKey | null,
|
||||
selectedPeriodId: string | null | undefined,
|
||||
selectedGroupKey: GroupKey | null | undefined,
|
||||
flow: "outflows" | "inflows"
|
||||
): LatestItem[] {
|
||||
const txns = extractTransactions(reportData, selectedPeriodId, selectedGroupKey);
|
||||
const txns = extractFilteredTransactions(reportData, selectedPeriodId, selectedGroupKey);
|
||||
|
||||
return txns
|
||||
.sort(
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { ReportData, GroupKey } from "../../features/report";
|
||||
import { ComponentProps } from "../report.props";
|
||||
import { buildLatestItems } from "./LatestItems.adapter";
|
||||
import LatestItemsView from "./LatestItems.view";
|
||||
|
||||
type Props = {
|
||||
reportData: ReportData;
|
||||
flow: "outflows" | "inflows";
|
||||
header: string;
|
||||
selectedPeriodId: string | null;
|
||||
selectedGroupKey?: GroupKey | null;
|
||||
accentColor: string;
|
||||
isFetching?: boolean;
|
||||
};
|
||||
type Props = ComponentProps;
|
||||
|
||||
export default function LatestItems({
|
||||
reportData,
|
||||
flow,
|
||||
state,
|
||||
stateSetters,
|
||||
header,
|
||||
selectedPeriodId,
|
||||
selectedGroupKey = null,
|
||||
accentColor,
|
||||
accentColor = "",
|
||||
isFetching,
|
||||
}: Props) {
|
||||
const { flow, selectedPeriodId, selectedGroupKey } = state;
|
||||
const [visibleCount, setVisibleCount] = React.useState(5);
|
||||
|
||||
const allItems = React.useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { mergeBucketPeriods, periodIdToKey } from "../report.helpers";
|
||||
import { GroupKey, ReportData } from "../../features/report";
|
||||
import {
|
||||
extractFilteredTransactions,
|
||||
aggregateTransactions,
|
||||
} from "../report.helpers";
|
||||
|
||||
export interface PayeeItem {
|
||||
name: string;
|
||||
@@ -12,54 +15,17 @@ export function extractTopPayees(
|
||||
selectedPeriodId?: string | null,
|
||||
selectedGroupKey?: GroupKey | null
|
||||
): { items: PayeeItem[]; total: number } {
|
||||
const payeeMap = new Map<string, number>();
|
||||
const txns = extractFilteredTransactions(reportData, selectedPeriodId, selectedGroupKey);
|
||||
|
||||
let targetPeriods = [];
|
||||
|
||||
if (selectedPeriodId) {
|
||||
const key = periodIdToKey(selectedPeriodId);
|
||||
const periods = mergeBucketPeriods(reportData.buckets, key);
|
||||
const selected = periods.find((p) => p.id === selectedPeriodId);
|
||||
if (selected) {
|
||||
targetPeriods.push(selected);
|
||||
const { items, total } = aggregateTransactions(txns, (txn) => {
|
||||
if (txn.payee && txn.payee.name) {
|
||||
return [txn.payee.name];
|
||||
}
|
||||
} else {
|
||||
// If no specific period is selected, aggregate over the "all" period bucket
|
||||
targetPeriods = mergeBucketPeriods(reportData.buckets, "all");
|
||||
}
|
||||
|
||||
for (const p of targetPeriods) {
|
||||
let txns = p.metric.transactions || [];
|
||||
|
||||
if (selectedGroupKey?.tags && selectedGroupKey.tags.length > 0) {
|
||||
txns = txns.filter(txn => {
|
||||
if (!txn.tags) return false;
|
||||
const txnTags = txn.tags.map(t => typeof t === "string" ? t : t.name);
|
||||
return selectedGroupKey.tags!.every(selectedTag => txnTags.includes(selectedTag));
|
||||
});
|
||||
}
|
||||
|
||||
for (const txn of txns) {
|
||||
if (txn.payee && txn.payee.name) {
|
||||
const current = payeeMap.get(txn.payee.name) || 0;
|
||||
payeeMap.set(txn.payee.name, current + txn.amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let items: PayeeItem[] = [];
|
||||
let total = 0;
|
||||
|
||||
for (const [name, amount] of payeeMap.entries()) {
|
||||
items.push({ name, amount });
|
||||
total += amount;
|
||||
}
|
||||
|
||||
// Sort descending by amount
|
||||
items.sort((a, b) => b.amount - a.amount);
|
||||
return [];
|
||||
});
|
||||
|
||||
return {
|
||||
items: items.slice(0, 4), // Top 4
|
||||
items,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import * as React from "react";
|
||||
import { Box, Paper, Typography } from "@mui/material";
|
||||
import { ReportData, GroupKey } from "../../features/report";
|
||||
import { ComponentProps } from "../report.props";
|
||||
import ProgressCard from "./ProgressCard";
|
||||
import { extractTopPayees } from "./TopPayees.adapter";
|
||||
|
||||
type Props = {
|
||||
reportData: ReportData;
|
||||
flow: "outflows" | "inflows";
|
||||
header: string;
|
||||
selectedPeriodId?: string | null;
|
||||
selectedGroupKey?: GroupKey | null;
|
||||
setSelectedGroupKey?: (key: GroupKey | null) => void;
|
||||
interface Props extends ComponentProps {
|
||||
compact?: boolean;
|
||||
isFetching?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function TopPayees({
|
||||
reportData,
|
||||
flow,
|
||||
state,
|
||||
stateSetters,
|
||||
header,
|
||||
selectedPeriodId,
|
||||
selectedGroupKey,
|
||||
setSelectedGroupKey,
|
||||
compact = true,
|
||||
isFetching,
|
||||
}: Props) {
|
||||
const { flow, selectedPeriodId, selectedGroupKey } = state;
|
||||
const { setSelectedGroupKey } = stateSetters;
|
||||
|
||||
const { items, total } = React.useMemo(() => {
|
||||
return extractTopPayees(reportData, flow, selectedPeriodId, selectedGroupKey);
|
||||
}, [reportData, flow, selectedPeriodId, selectedGroupKey]);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ReportData } from "../../features/report";
|
||||
import { ReportData, GroupKey } from "../../features/report";
|
||||
import {
|
||||
mergeBucketPeriods,
|
||||
periodIdToKey,
|
||||
extractFilteredTransactions,
|
||||
aggregateTransactions,
|
||||
} from "../report.helpers";
|
||||
|
||||
import { GroupKey } from "../../features/report";
|
||||
|
||||
export interface TagItem {
|
||||
tag: string;
|
||||
amount: number;
|
||||
@@ -17,55 +15,17 @@ export function extractTopTags(
|
||||
selectedPeriodId?: string | null,
|
||||
selectedGroupKey?: GroupKey | null
|
||||
): { items: TagItem[]; total: number } {
|
||||
const tagMap = new Map<string, number>();
|
||||
const txns = extractFilteredTransactions(reportData, selectedPeriodId, selectedGroupKey);
|
||||
|
||||
let periodKey: ReturnType<typeof periodIdToKey> = "all";
|
||||
if (selectedPeriodId) {
|
||||
periodKey = periodIdToKey(selectedPeriodId);
|
||||
}
|
||||
|
||||
const periods = mergeBucketPeriods(reportData.buckets, periodKey);
|
||||
|
||||
let period = periods[0];
|
||||
if (selectedPeriodId) {
|
||||
period = periods.find(p => p.id === selectedPeriodId) || period;
|
||||
} else if (periods.length > 0) {
|
||||
period = periods.reduce((latest, p) =>
|
||||
new Date(p.start).getTime() > new Date(latest.start).getTime()
|
||||
? p
|
||||
: latest
|
||||
);
|
||||
}
|
||||
|
||||
if (period && period.metric && period.metric.transactions) {
|
||||
let txns = period.metric.transactions;
|
||||
if (selectedGroupKey?.payee && selectedGroupKey.payee.length > 0) {
|
||||
txns = txns.filter(txn =>
|
||||
txn.payee?.name && selectedGroupKey.payee!.includes(txn.payee.name)
|
||||
);
|
||||
const { items, total } = aggregateTransactions(txns, (txn) => {
|
||||
if (txn.tags && txn.tags.length > 0) {
|
||||
return txn.tags.map((t) => (typeof t === "string" ? t : t.name));
|
||||
}
|
||||
return ["Untagged"];
|
||||
});
|
||||
|
||||
for (const txn of txns) {
|
||||
if (txn.tags && txn.tags.length > 0) {
|
||||
for (const tagObj of txn.tags) {
|
||||
const tagName = typeof tagObj === "string" ? tagObj : tagObj.name;
|
||||
tagMap.set(tagName, (tagMap.get(tagName) || 0) + txn.amount);
|
||||
}
|
||||
} else {
|
||||
tagMap.set("Untagged", (tagMap.get("Untagged") || 0) + txn.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 };
|
||||
return {
|
||||
items: items.map((item) => ({ tag: item.name, amount: item.amount })),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import * as React from "react";
|
||||
import { Box, Paper, Typography } from "@mui/material";
|
||||
import { ReportData, GroupKey } from "../../features/report";
|
||||
import { ComponentProps } from "../report.props";
|
||||
import ProgressCard from "./ProgressCard";
|
||||
import { extractTopTags } from "./TopTags.adapter";
|
||||
|
||||
type Props = {
|
||||
reportData: ReportData;
|
||||
flow: "outflows" | "inflows";
|
||||
header: string;
|
||||
selectedPeriodId?: string | null;
|
||||
selectedGroupKey?: GroupKey | null;
|
||||
setSelectedGroupKey?: (key: GroupKey | null) => void;
|
||||
interface Props extends ComponentProps {
|
||||
compact?: boolean;
|
||||
isFetching?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function TopTags({
|
||||
reportData,
|
||||
flow,
|
||||
state,
|
||||
stateSetters,
|
||||
header,
|
||||
selectedPeriodId,
|
||||
selectedGroupKey,
|
||||
setSelectedGroupKey,
|
||||
compact = true,
|
||||
isFetching,
|
||||
}: Props) {
|
||||
const { flow, selectedPeriodId, selectedGroupKey } = state;
|
||||
const { setSelectedGroupKey } = stateSetters;
|
||||
|
||||
const { items, total } = React.useMemo(() => {
|
||||
return extractTopTags(reportData, flow, selectedPeriodId, selectedGroupKey);
|
||||
}, [reportData, flow, selectedPeriodId, selectedGroupKey]);
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
ReportBucket,
|
||||
GroupKey,
|
||||
PeriodType,
|
||||
ReportData,
|
||||
Transaction,
|
||||
} from "../features/report";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────
|
||||
@@ -140,3 +142,89 @@ export function filterBuckets(
|
||||
if (!selectedGroupKey) return buckets;
|
||||
return buckets.filter((b) => matchesGroupKey(b, selectedGroupKey));
|
||||
}
|
||||
|
||||
export function extractFilteredTransactions(
|
||||
reportData: ReportData,
|
||||
selectedPeriodId: string | null | undefined,
|
||||
selectedGroupKey: GroupKey | null | undefined
|
||||
): Transaction[] {
|
||||
let txns: Transaction[] = [];
|
||||
|
||||
if (selectedPeriodId) {
|
||||
const key = periodIdToKey(selectedPeriodId);
|
||||
const periods = mergeBucketPeriods(reportData.buckets, key);
|
||||
const selected = periods.find((p) => p.id === selectedPeriodId);
|
||||
txns = selected?.metric.transactions || [];
|
||||
} else {
|
||||
const periods = mergeBucketPeriods(reportData.buckets, "all");
|
||||
if (periods.length > 0) {
|
||||
const period = periods.reduce((latest, p) =>
|
||||
new Date(p.start).getTime() > new Date(latest.start).getTime()
|
||||
? p
|
||||
: latest
|
||||
, periods[0]);
|
||||
txns = period?.metric.transactions || [];
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedGroupKey) {
|
||||
txns = txns.filter((txn) => {
|
||||
let match = true;
|
||||
if (selectedGroupKey.tags && selectedGroupKey.tags.length > 0) {
|
||||
if (!txn.tags) {
|
||||
match = false;
|
||||
} else {
|
||||
const txnTags = txn.tags.map((t: any) =>
|
||||
typeof t === "string" ? t : t.name
|
||||
);
|
||||
if (
|
||||
!selectedGroupKey.tags.every((selectedTag) =>
|
||||
txnTags.includes(selectedTag)
|
||||
)
|
||||
) {
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match && selectedGroupKey.payee && selectedGroupKey.payee.length > 0) {
|
||||
if (!txn.payee || !txn.payee.name) {
|
||||
match = false;
|
||||
} else {
|
||||
if (!selectedGroupKey.payee.includes(txn.payee.name)) {
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
return txns;
|
||||
}
|
||||
|
||||
export function aggregateTransactions(
|
||||
transactions: Transaction[],
|
||||
keyExtractor: (txn: Transaction) => string[],
|
||||
limit = 4
|
||||
): { items: { name: string; amount: number }[]; total: number } {
|
||||
const map = new Map<string, number>();
|
||||
|
||||
for (const txn of transactions) {
|
||||
const keys = keyExtractor(txn);
|
||||
for (const key of keys) {
|
||||
map.set(key, (map.get(key) || 0) + txn.amount);
|
||||
}
|
||||
}
|
||||
|
||||
const items = Array.from(map.entries()).map(([name, amount]) => ({
|
||||
name,
|
||||
amount,
|
||||
}));
|
||||
|
||||
items.sort((a, b) => b.amount - a.amount);
|
||||
|
||||
const top = items.slice(0, limit);
|
||||
const total = top.reduce((sum, item) => sum + item.amount, 0);
|
||||
|
||||
return { items: top, total };
|
||||
}
|
||||
|
||||
17
src/components/report.props.ts
Normal file
17
src/components/report.props.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ReportData } from "../features/report";
|
||||
import { DashboardState, DashboardStateSetters } from "./Dashboard";
|
||||
|
||||
export interface ComponentProps {
|
||||
reportData: ReportData;
|
||||
state: DashboardState;
|
||||
stateSetters: DashboardStateSetters;
|
||||
isFetching?: boolean;
|
||||
header: string;
|
||||
summary?: string;
|
||||
accentColor?: string;
|
||||
colorScheme?: {
|
||||
primary: string;
|
||||
light: string;
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user