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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import "server-only";
|
||||
import { budgetFor, settingsOf } from "@/features/budgets/service";
|
||||
import { incomeByDay, incomePlan, monthDailyPlan } from "@/features/budgets/plan";
|
||||
import { loadContext } from "@/features/transactions/service";
|
||||
import { budgetPeriod, periodOfDate } from "@/shared/budget";
|
||||
import { todayIstanbul } from "@/shared/dates";
|
||||
import {
|
||||
overviewRange,
|
||||
type OverviewPeriod,
|
||||
type OverviewPoint,
|
||||
type OverviewRange,
|
||||
} from "@/shared/overview";
|
||||
|
||||
/**
|
||||
* Dashboard ana grafiği (Bölüm 6.13). Bütçe ve gelir planı motorlarıyla aynı kurallar: bütçe ayı,
|
||||
* taksit modu ve gizli hesap ayarı.
|
||||
*
|
||||
* Aylık noktalar: beklenen gelir (gerçekleşen + planlı + elle), bütçe toplamı (bütçe yoksa boş),
|
||||
* gerçekleşen gider. Günlük noktalar: o günün gelirleri (elle eklenen beklenen gelir dönemin ilk
|
||||
* gününe), günlük plan payı, o günün gerçekleşen gideri; gelecek günlerde gerçekleşen boş.
|
||||
*/
|
||||
export type OverviewData = {
|
||||
period: OverviewPeriod;
|
||||
range: OverviewRange;
|
||||
points: OverviewPoint[];
|
||||
today: string;
|
||||
};
|
||||
|
||||
export async function getOverview(
|
||||
userId: string,
|
||||
householdId: string,
|
||||
period: OverviewPeriod,
|
||||
): Promise<OverviewData | null> {
|
||||
const ctx = await loadContext(userId, householdId, "viewer");
|
||||
if (!ctx) return null;
|
||||
const s = await settingsOf(ctx.m.householdId);
|
||||
const today = todayIstanbul();
|
||||
const range = overviewRange(period, today, s.startDay);
|
||||
|
||||
if (range.granularity === "month") {
|
||||
const points = await Promise.all(
|
||||
range.buckets.map(async (month) => {
|
||||
const [budget, income] = await Promise.all([
|
||||
budgetFor(ctx, s, month),
|
||||
incomePlan(ctx, s, month),
|
||||
]);
|
||||
return {
|
||||
key: month,
|
||||
plannedIncome: income.expectedMinor,
|
||||
budget: budget.hasBudget ? budget.totals.limitMinor : null,
|
||||
actual: budget.totals.actualMinor,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return { period, range, points, today };
|
||||
}
|
||||
|
||||
// Günlük: aralığın dokunduğu bütçe ayları (hafta iki aya yayılabilir).
|
||||
const months = [...new Set([range.from, range.to].map((d) => periodOfDate(d, s.startDay)))];
|
||||
const planned = new Map<string, number | null>();
|
||||
const actual = new Map<string, number>();
|
||||
const income = new Map<string, number>();
|
||||
for (const month of months) {
|
||||
const [budget, plan] = await Promise.all([budgetFor(ctx, s, month), incomePlan(ctx, s, month)]);
|
||||
const dp = await monthDailyPlan(ctx, s, month, budget.totals.limitMinor, today);
|
||||
for (const d of dp.days) {
|
||||
planned.set(d.date, budget.hasBudget ? d.plannedMinor : null);
|
||||
actual.set(d.date, d.actualMinor);
|
||||
}
|
||||
const first = budgetPeriod(month, s.startDay).from;
|
||||
if (plan.manualMinor) income.set(first, (income.get(first) ?? 0) + plan.manualMinor);
|
||||
}
|
||||
for (const [d, v] of await incomeByDay(ctx, s, range.from, range.to))
|
||||
income.set(d, (income.get(d) ?? 0) + v);
|
||||
|
||||
const points = range.buckets.map((d) => ({
|
||||
key: d,
|
||||
plannedIncome: income.get(d) ?? 0,
|
||||
budget: planned.get(d) ?? null,
|
||||
actual: d <= today ? (actual.get(d) ?? 0) : null,
|
||||
}));
|
||||
return { period, range, points, today };
|
||||
}
|
||||
Reference in New Issue
Block a user