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>
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
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 };
|
||
}
|