Reviewed-on: #2 Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com> Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
import {
|
|
ReportPeriod,
|
|
ReportBucket,
|
|
GroupKey,
|
|
} from "../features/report";
|
|
|
|
// ─── Types ────────────────────────────────────────────────────
|
|
|
|
export type PeriodKey = "weekly" | "monthly" | "yearly" | "fyly" | "full";
|
|
|
|
export type DecoratedPeriod = ReportPeriod & {
|
|
id: string;
|
|
label: string;
|
|
};
|
|
|
|
// ─── Period helpers ───────────────────────────────────────────
|
|
|
|
const PREFIX_TO_KEY: Record<string, PeriodKey> = {
|
|
W: "weekly",
|
|
M: "monthly",
|
|
Y: "yearly",
|
|
FY: "fyly",
|
|
FULL: "full",
|
|
};
|
|
|
|
/**
|
|
* 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] ?? "full";
|
|
}
|
|
|
|
// ─── Metric helpers ───────────────────────────────────────────
|
|
|
|
export function getAmount(
|
|
period: ReportPeriod,
|
|
mode: "expense" | "income"
|
|
): number {
|
|
return mode === "expense" ? period.expenses.sum : period.incomes.sum;
|
|
}
|
|
|
|
function mergeMetric(a: ReportPeriod["expenses"], b: ReportPeriod["expenses"]) {
|
|
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<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()
|
|
);
|
|
}
|
|
|
|
// ─── 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 as keyof GroupKey];
|
|
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));
|
|
}
|