From 79808d6d3e8bd35bbc09a40cb552e1102cc09bfb Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Thu, 20 Aug 2026 17:10:48 +0530 Subject: [PATCH] feat: redesign per-group stats strip in accordion summary Replace the dated First/Last pills with a Cadence metric computed from consecutive transaction dates (mirrors backend ReportMetrics.cadence_days) and render the group stats as spaced StatCards (Sum/Avg/Min/Max/Cadence) in the collapsed accordion header, matching the Outflows/Inflows layout. --- src/common/components/TransactionList.tsx | 27 +++++++++-------------- src/common/utils/transactions.ts | 25 ++++++++++++++------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/common/components/TransactionList.tsx b/src/common/components/TransactionList.tsx index db26e34..80c84b1 100644 --- a/src/common/components/TransactionList.tsx +++ b/src/common/components/TransactionList.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { Box, Typography, - Paper, Accordion, AccordionSummary, AccordionDetails, @@ -18,6 +17,7 @@ import type { ExpenseItem, TxnFieldConfigs } from "../types"; import { computeTxnMetrics, groupByDate, groupByPeriod } from "../utils/transactions"; import type { PeriodGranularity } from "../utils/transactions"; import { TransactionRow } from "./TransactionRow"; +import { StatCard } from "./StatCard"; interface TransactionListProps { items: ExpenseItem[]; @@ -30,24 +30,21 @@ function GroupMetrics({ items, currency }: { items: ExpenseItem[]; currency: str const m = computeTxnMetrics(items); const rows: { label: string; value: string }[] = [ { label: "Sum", value: formatCurrency(m.sum, currency) }, - { label: "Count", value: m.count.toLocaleString("en-IN") }, { label: "Avg", value: m.avg == null ? "—" : formatCurrency(m.avg, currency) }, { label: "Min", value: m.min == null ? "—" : formatCurrency(m.min, currency) }, { label: "Max", value: m.max == null ? "—" : formatCurrency(m.max, currency) }, - { label: "First", value: m.firstDate ?? "—" }, - { label: "Last", value: m.lastDate ?? "—" }, + { + label: "Cadence", + value: + m.cadenceDays == null + ? "—" + : `${Number.isInteger(m.cadenceDays) ? m.cadenceDays : m.cadenceDays.toFixed(2)} days`, + }, ]; return ( - + {rows.map((row) => ( - - - {row.label} - - - {row.value} - - + ))} ); @@ -190,9 +187,7 @@ export function TransactionList({ items, fields, granularity = "monthly", showMe {formatCurrency(group.income, group.currency)} - - {showMetrics && } - + {showMetrics && } diff --git a/src/common/utils/transactions.ts b/src/common/utils/transactions.ts index c72e13b..dd9996e 100644 --- a/src/common/utils/transactions.ts +++ b/src/common/utils/transactions.ts @@ -114,27 +114,36 @@ export interface TxnMetrics { avg: number | null; min: number | null; max: number | null; - firstDate: string | null; - lastDate: string | null; + cadenceDays: number | null; } /** Mirrors backend ReportMetrics.compute_metrics over a list of transactions. */ export function computeTxnMetrics(items: ExpenseItem[]): TxnMetrics { const amounts = items.filter((it) => typeof it.amount === "number").map((it) => it.amount as number); + const empty = { sum: 0, count: 0, avg: null, min: null, max: null, cadenceDays: null }; if (amounts.length === 0) { - return { sum: 0, count: 0, avg: null, min: null, max: null, firstDate: null, lastDate: null }; + return empty; } - const sorted = [...items].sort( - (a, b) => parseOccurredAt(a.occurred_at).getTime() - parseOccurredAt(b.occurred_at).getTime(), - ); + const dates = items + .map((it) => it.occurred_at) + .filter((d): d is string => !!d) + .map((d) => parseOccurredAt(d).getTime()) + .sort((a, b) => a - b); const sum = amounts.reduce((s, a) => s + a, 0); + let cadenceDays: number | null = null; + if (dates.length >= 2) { + const gaps: number[] = []; + for (let i = 0; i < dates.length - 1; i += 1) { + gaps.push((dates[i + 1] - dates[i]) / 86400000); + } + cadenceDays = Math.round((gaps.reduce((s, g) => s + g, 0) / gaps.length) * 100) / 100; + } return { sum, count: amounts.length, avg: sum / amounts.length, min: Math.min(...amounts), max: Math.max(...amounts), - firstDate: sorted[0]?.occurred_at ?? null, - lastDate: sorted[sorted.length - 1]?.occurred_at ?? null, + cadenceDays, }; } \ No newline at end of file