import { api } from "#lib/api/client.js"; import { all } from "#lib/api/lists.js"; import type { Account, BasicSummaryEntry } from "#lib/api/types.js"; import { addDays, today } from "#lib/dates.js"; import { compactMoney, formatMoney, toNumber } from "#lib/format.js"; import { period } from "#lib/period.svelte.js"; import { session } from "#lib/session.svelte.js"; export interface Cur { code: string; decimals: number; symbol: string; } export interface SummaryEntry extends Cur { amount: number; noBudgets: boolean; sub: string; title: string; } export interface ChartSet { currency_code: string; currency_decimal_places: number; currency_symbol?: string; entries: Record; label: string; pc_entries?: Record | unknown[]; primary_currency_code?: string; primary_currency_decimal_places?: number; primary_currency_symbol?: string; } export interface InsightEntry { currency_code: string; difference: string; difference_float?: number; id?: string; name?: string; } const SUMMARY_KEY = /^(.+)-in-([A-Z0-9]+)$/; export function currency(code: string): Cur { const found = session.currencies.find((c) => c.attributes.code === code); return { code, decimals: found?.attributes.decimal_places ?? 2, symbol: found?.attributes.symbol ?? code, }; } export const setCurrency = (set: ChartSet): Cur => ({ ...currency(set.currency_code), ...(set.currency_symbol ? { symbol: set.currency_symbol } : {}), }); export const money = (cur: Cur) => (value: number) => formatMoney({ amount: value, code: cur.code, decimals: cur.decimals, symbol: cur.symbol, }); export const axis = (cur: Cur) => (value: number) => compactMoney(value, cur.symbol); export const primaryCode = () => session.primary?.code ?? ""; export function byCurrency( items: T[], code: (item: T) => string ): [string, T[]][] { const groups = new Map(); for (const item of items) { const key = code(item); const list = groups.get(key); if (list) { list.push(item); } else { groups.set(key, [item]); } } const primary = primaryCode(); return [...groups.entries()].sort( ([a, la], [b, lb]) => Number(b === primary) - Number(a === primary) || lb.length - la.length ); } export function parseSummary( raw: Record | undefined ): Record { const result: Record = {}; for (const [key, entry] of Object.entries(raw ?? {})) { const match = key.match(SUMMARY_KEY); if (!match) { continue; } const extra = entry as BasicSummaryEntry & { no_available_budgets?: boolean; }; const list = result[match[1]] ?? []; list.push({ amount: toNumber(entry.monetary_value), code: entry.currency_code ?? match[2], decimals: entry.currency_decimal_places ?? 2, noBudgets: extra.no_available_budgets === true, sub: entry.sub_title ?? "", symbol: entry.currency_symbol ?? match[2], title: entry.title ?? "", }); result[match[1]] = list; } const primary = primaryCode(); for (const list of Object.values(result)) { list.sort( (a, b) => Number(b.code === primary) - Number(a.code === primary) || Math.abs(b.amount) - Math.abs(a.amount) ); } return result; } export function hasPrimary(sets: ChartSet[]): boolean { return ( session.prefs.convert_to_primary === true && sets.length > 0 && sets.every((s) => s.pc_entries && !Array.isArray(s.pc_entries)) ); } export function upTo(end: string): string { const now = today(); return end < now ? end : now; } export function periodQuery(extra: Record = {}) { return { end: period.end, start: period.start, ...extra }; } export function hrefWith( url: { href: string }, changes: Record ): string { const next = new URL(url.href); for (const [name, value] of Object.entries(changes)) { if (value === null) { next.searchParams.delete(name); } else { next.searchParams.set(name, value); } } next.searchParams.set("start", period.start); next.searchParams.set("end", period.end); return next.pathname + next.search; } export const summaryQuery = (start: string, end: string) => ({ queryFn: () => api>("/v1/summary/basic", { query: { end, start }, }), queryKey: ["summary", start, end], }); export const accountsChartQuery = ( start: string, end: string, everything = false ) => ({ queryFn: () => api("/v1/chart/account/overview", { query: { ...span(start, end), period: "1D", preselected: everything ? "all" : undefined, }, }), queryKey: ["chart-accounts", start, end, everything], }); export function seriesPoints(set: ChartSet, converted: boolean) { const entries = (converted ? set.pc_entries : set.entries) as | Record | undefined; return Object.entries(entries ?? {}).map(([date, value]) => ({ x: date.slice(0, 10), y: toNumber(value), })); } export function setPrimary(set: ChartSet): Cur { return currency(set.primary_currency_code ?? set.currency_code); } export function balanceDate(start: string, end: string): string { const until = upTo(end); return until < start ? end : until; } export function balanceRange(start: string, end: string) { const until = balanceDate(start, end); return until > start ? { date: until, end: until, start } : { date: until }; } export function span(start: string, end: string) { return { end: end > start ? end : addDays(start, 1), start }; } export const accountsQuery = (type: string, start: string, end: string) => ({ queryFn: () => all("/v1/accounts", { ...balanceRange(start, end), type }), queryKey: ["accounts", type, start, end], }); export function proportional(counts: number[]): string { return counts.map((n) => `minmax(0, ${Math.max(2, n)}fr)`).join(" "); }