bc2a8a3bde
- periyot aralıkları (haftalık, aylık, altı aylık, yıllık) ve kümülatif dönüşüm - kategori payı, alt kategori kırılımı, önceki aya göre değişim, tasarruf oranı - kategori, trend, kişi/hesap ve bütçe raporu servisleri; gizli hesap kuralları; testler Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
/**
|
||
* Raporlar (Bölüm 6.14). Saf hesaplar: kategori dağılımı (üst kategori toplamları ve alt kategori
|
||
* kırılımı), toplam gidere oran, önceki aya göre değişim, tasarruf oranı. Tutarlar kuruş; oranlar
|
||
* bir ondalıklı yüzde (14,3).
|
||
*/
|
||
|
||
/** Payın toplamdaki yüzdesi, bir ondalık (toplam 0 ise 0). */
|
||
export function sharePct(part: number, total: number): number {
|
||
if (total <= 0) return 0;
|
||
return Math.round((part * 1000) / total) / 10;
|
||
}
|
||
|
||
/** Önceki döneme göre yüzde değişim; önceki 0 ise hesaplanamaz (null). */
|
||
export function changePct(current: number, previous: number): number | null {
|
||
if (previous === 0) return null;
|
||
return Math.round(((current - previous) * 1000) / previous) / 10;
|
||
}
|
||
|
||
/** Tasarruf oranı = (gelir − gider) / gelir; gelir yoksa null. */
|
||
export function savingsRate(incomeMinor: number, expenseMinor: number): number | null {
|
||
if (incomeMinor <= 0) return null;
|
||
return sharePct(incomeMinor - expenseMinor, incomeMinor);
|
||
}
|
||
|
||
export type ReportCategory = {
|
||
id: string;
|
||
parentId: string | null;
|
||
name: string;
|
||
order: number;
|
||
};
|
||
|
||
export type CategoryShare = {
|
||
id: string;
|
||
name: string;
|
||
depth: number;
|
||
amountMinor: number;
|
||
sharePct: number;
|
||
prevMinor: number;
|
||
changeMinor: number;
|
||
changePct: number | null;
|
||
};
|
||
|
||
export type TopCategoryShare = CategoryShare & {
|
||
/** Alt kategoriler (ağaç sırasıyla, derinlik 1+); tutarları kendi tutarlarıdır. */
|
||
children: CategoryShare[];
|
||
/** Üst kategoriye doğrudan (alt kategori seçilmeden) yazılan tutar. */
|
||
ownMinor: number;
|
||
};
|
||
|
||
export type CategoryBreakdown = {
|
||
totalMinor: number;
|
||
prevTotalMinor: number;
|
||
rows: TopCategoryShare[];
|
||
};
|
||
|
||
/**
|
||
* Üst kategori toplamları (alt ağaç dahil) ve alt kategori kırılımı. Tutarı ve önceki tutarı
|
||
* sıfır olan kategoriler gösterilmez. Satırlar tutara göre büyükten küçüğe.
|
||
*/
|
||
export function categoryBreakdown(
|
||
categories: readonly ReportCategory[],
|
||
current: ReadonlyMap<string, number>,
|
||
previous: ReadonlyMap<string, number>,
|
||
): CategoryBreakdown {
|
||
const byId = new Map(categories.map((c) => [c.id, c]));
|
||
const children = new Map<string, ReportCategory[]>();
|
||
const roots: ReportCategory[] = [];
|
||
for (const c of categories) {
|
||
if (c.parentId && byId.has(c.parentId))
|
||
children.set(c.parentId, [...(children.get(c.parentId) ?? []), c]);
|
||
else roots.push(c);
|
||
}
|
||
const sorted = (list: ReportCategory[]) =>
|
||
[...list].sort((a, b) => a.order - b.order || a.name.localeCompare(b.name, "tr"));
|
||
const totalMinor = [...current.values()].reduce((a, b) => a + b, 0);
|
||
const prevTotalMinor = [...previous.values()].reduce((a, b) => a + b, 0);
|
||
|
||
const share = (id: string, name: string, depth: number, cur: number, prev: number) => ({
|
||
id,
|
||
name,
|
||
depth,
|
||
amountMinor: cur,
|
||
sharePct: sharePct(cur, totalMinor),
|
||
prevMinor: prev,
|
||
changeMinor: cur - prev,
|
||
changePct: changePct(cur, prev),
|
||
});
|
||
|
||
const rows: TopCategoryShare[] = [];
|
||
for (const root of roots) {
|
||
const kids: CategoryShare[] = [];
|
||
let cur = current.get(root.id) ?? 0;
|
||
let prev = previous.get(root.id) ?? 0;
|
||
const walk = (list: ReportCategory[], depth: number, seen: Set<string>) => {
|
||
for (const c of sorted(list)) {
|
||
if (seen.has(c.id)) continue;
|
||
seen.add(c.id);
|
||
const a = current.get(c.id) ?? 0;
|
||
const p = previous.get(c.id) ?? 0;
|
||
cur += a;
|
||
prev += p;
|
||
const sub = children.get(c.id) ?? [];
|
||
const before = kids.length;
|
||
kids.push(share(c.id, c.name, depth, a, p));
|
||
walk(sub, depth + 1, seen);
|
||
// Kendisi ve altı boşsa satırı çıkar.
|
||
if (a === 0 && p === 0 && kids.length === before + 1) kids.pop();
|
||
}
|
||
};
|
||
walk(children.get(root.id) ?? [], 1, new Set([root.id]));
|
||
if (cur === 0 && prev === 0) continue;
|
||
rows.push({
|
||
...share(root.id, root.name, 0, cur, prev),
|
||
ownMinor: current.get(root.id) ?? 0,
|
||
children: kids,
|
||
});
|
||
}
|
||
rows.sort((a, b) => b.amountMinor - a.amountMinor || a.name.localeCompare(b.name, "tr"));
|
||
return { totalMinor, prevTotalMinor, rows };
|
||
}
|