diff --git a/src/features/report/report.models.ts b/src/features/report/report.models.ts new file mode 100644 index 0000000..ada38ed --- /dev/null +++ b/src/features/report/report.models.ts @@ -0,0 +1,61 @@ +export interface Payor { + name: string; +} + +export interface Payee { + name: string; +} + +export interface Account { + name: string; + number: string; +} + +export interface Tag { + name: string; + icon: string; + description: string; +} + +export interface Transaction { + payor: Payor; + payee: Payee; + amount: number; + account: Account; + tags: Tag[] + occurred_at: Date +} + +export interface _PeriodData { + sum: number; + count: number; + average: number; + txns: Transaction[]; +} + +export interface PeriodData extends _PeriodData { + compare?: _PeriodData; +} + +export interface PeriodGroup { + group_key: string[]; + expenses: PeriodData[]; + incomes: PeriodData[]; +} + +export interface Period { + id: string; + label: string; + start: Date; + end: Date; + groups: PeriodGroup[]; +} + +export interface ReportData { + period: "weekly" | "monthly" | "yearly" | "fyly" | "full"; + rolling?: boolean; + report_date?: string; + group_by?: ("payee" | "tags")[]; + ignore_self?: boolean; + buckets: Period[]; +} diff --git a/src/features/report/report.utils.ts b/src/features/report/report.utils.ts new file mode 100644 index 0000000..387f3fe --- /dev/null +++ b/src/features/report/report.utils.ts @@ -0,0 +1,116 @@ +import { ReportData } from "./report.models"; + +/* ---------- ID BUILDING ---------- */ + +function formatDate(d: Date): string { + const y = d.getUTCFullYear(); + const m = String(d.getUTCMonth() + 1).padStart(2, "0"); + const day = String(d.getUTCDate()).padStart(2, "0"); + return `${y}-${m}-${day}`; +} + +function buildPeriodId( + type: ReportData["period"], + start: Date, + end: Date +): string { + const s = formatDate(start); + const e = formatDate(end); + + switch (type) { + case "weekly": + return `W:${s}_${e}`; + case "monthly": + return `M:${s}_${e}`; + case "yearly": + return `Y:${s}_${e}`; + case "fyly": + return `FY:${s}_${e}`; + case "full": + return `FULL:${s}_${e}`; + default: + return `${s}_${e}`; + } +} + +/* ---------- LABEL BUILDING ---------- */ + +const dayFmt = new Intl.DateTimeFormat("en-GB", { + day: "numeric", + month: "short", + timeZone: "UTC", +}); + +const monthDayFmt = new Intl.DateTimeFormat("en-GB", { + month: "short", + day: "numeric", + timeZone: "UTC", +}); + +const monthFmt = new Intl.DateTimeFormat("en-GB", { + month: "short", + timeZone: "UTC", +}); + +const yearFmt = new Intl.DateTimeFormat("en-GB", { + year: "numeric", + timeZone: "UTC", +}); + +function sameMonth(a: Date, b: Date) { + return ( + a.getUTCFullYear() === b.getUTCFullYear() && + a.getUTCMonth() === b.getUTCMonth() + ); +} + +function buildLabel( + type: ReportData["period"], + start: Date, + end: Date +): string { + switch (type) { + case "weekly": + if (sameMonth(start, end)) { + const sDay = start.getUTCDate(); + const eDay = end.getUTCDate(); + const m = monthFmt.format(start); + return `${sDay} ${m} - ${eDay} ${m}`; + } + return `${dayFmt.format(start)} - ${dayFmt.format(end)}`; + + case "monthly": + if (sameMonth(start, end)) { + return `${monthFmt.format(start)} ${yearFmt.format(start)}`; + } + return `${monthDayFmt.format(start)} - ${monthDayFmt.format(end)}`; + + case "yearly": + return yearFmt.format(start); + + case "fyly": { + const startY = start.getUTCFullYear(); + const endY = end.getUTCFullYear(); + return `FY ${startY}–${String(endY).slice(-2)}`; + } + + case "full": + return `${monthFmt.format(start)} ${yearFmt.format(start)} - ${monthFmt.format(end)} ${yearFmt.format(end)}`; + + default: + return `${monthDayFmt.format(start)} - ${monthDayFmt.format(end)}`; + } +} + +/* ---------- MAIN ---------- */ + +export function prepareReport(reportData: ReportData): ReportData { + return { + ...reportData, + buckets: reportData.buckets.map((p) => ({ + ...p, + id: buildPeriodId(reportData.period, p.start, p.end), + label: buildLabel(reportData.period, p.start, p.end), + })), + }; +}