Files
butcem/src/shared/reports.ts
T
suphicakir bc2a8a3bde feat(reports): rapor ve dashboard grafiği motorları
- 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>
2026-11-07 00:29:25 +03:00

121 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 };
}