import { ReportPeriod, ReportBucket, GroupKey, PeriodType, ReportData, Transaction, } from "../features/report"; // ─── Types ──────────────────────────────────────────────────── export type PeriodKey = PeriodType; export type DecoratedPeriod = ReportPeriod & { id: string; label: string; }; // ─── Period helpers ─────────────────────────────────────────── const PREFIX_TO_KEY: Record = { D: "daily", W: "weekly", M: "monthly", ALL: "all", }; /** * Derive the period key from a decorated-period id. * E.g. `"W:2026-04-28_2026-05-04"` → `"weekly"` */ export function periodIdToKey(periodId: string): PeriodKey { const prefix = periodId.split(":")[0]; return PREFIX_TO_KEY[prefix] ?? "all"; } // ─── Metric helpers ─────────────────────────────────────────── export function getAmount(period: ReportPeriod): number { return period.metric.sum; } function mergeMetric(a: ReportPeriod["metric"], b: ReportPeriod["metric"]) { 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, }; } /** * Merge periods with the same id across all buckets, summing * their metrics and concatenating transactions. * * Returns sorted by start date ascending. */ export function mergeBucketPeriods( buckets: ReportBucket[], key: PeriodKey ): DecoratedPeriod[] { const map = new Map(); 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, metric: { ...p.metric }, }); } else { map.set(p.id, { ...existing, metric: mergeMetric(existing.metric, p.metric), }); } } } return Array.from(map.values()).sort( (a, b) => new Date(a.start).getTime() - new Date(b.start).getTime() ); } // ─── Formatting ─────────────────────────────────────────────── export const formatCurrency = (val: number) => { const absVal = Math.abs(val); if (absVal >= 100000) { return `₹ ${(val / 100000).toFixed(2)}L`; } if (absVal >= 1000) { return `₹ ${(val / 1000).toFixed(2)}k`; } return `₹ ${val.toFixed(2)}`; }; export const getPercentage = (progressAmount: number, totalAmount: number) => { if (!totalAmount) return 0; return Math.min(100, Math.max(0, (progressAmount / totalAmount) * 100)); }; // ─── Group filtering ────────────────────────────────────────── /** * Check if a bucket's group_key matches the selected GroupKey. * Every dimension present in `selected` must exist in the bucket * and contain all the selected values. */ export function matchesGroupKey( bucket: ReportBucket, selected: GroupKey ): boolean { for (const [dim, values] of Object.entries(selected)) { const bucketValues = bucket.group_key[dim]; if (!bucketValues) return false; if (!(values as string[]).every((v) => bucketValues.includes(v))) return false; } return true; } /** * Return only buckets matching the selected group key, * or all buckets if no selection. */ export function filterBuckets( buckets: ReportBucket[], selectedGroupKey: GroupKey | null ): ReportBucket[] { 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(); 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 }; }