From d77a2efcb876eab2d747891ac61180458c1143c6 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Sat, 7 Nov 2026 00:29:25 +0300 Subject: [PATCH] =?UTF-8?q?refactor(budgets):=20b=C3=BCt=C3=A7e,=20gelir?= =?UTF-8?q?=20plan=C4=B1=20ve=20g=C3=BCnl=C3=BCk=20plan=C4=B1=20ba=C4=9Fla?= =?UTF-8?q?m=20alan=20yard=C4=B1mc=C4=B1lara=20ay=C4=B1r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit budgetFor, incomePlan, expensesByDay, incomeByDay ve monthDailyPlan raporlarda ve dashboard grafiğinde birden çok ay için bağlamı yeniden yüklemeden kullanılır. Co-Authored-By: Claude Opus 5.5 --- src/features/budgets/plan.ts | 76 ++++++++++++++++++++++----------- src/features/budgets/service.ts | 6 ++- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/features/budgets/plan.ts b/src/features/budgets/plan.ts index c17e4b6..85da0fb 100644 --- a/src/features/budgets/plan.ts +++ b/src/features/budgets/plan.ts @@ -60,7 +60,7 @@ export type IncomePlan = { manual: { id: string; name: string; amountMinor: number }[]; }; -async function incomePlan(ctx: Ctx, s: Settings, month: string): Promise { +export async function incomePlan(ctx: Ctx, s: Settings, month: string): Promise { const { from, to } = budgetPeriod(month, s.startDay); const [docs, budget] = await Promise.all([ TransactionModel.find(baseMatch(ctx, s, "income", from, to)) @@ -145,7 +145,7 @@ export async function saveIncomeItems( // --- Günlük plan --- /** Güne göre gider: sabit (düzenli/taksit; gerçekleşmiş + planlı) ve gerçekleşen (tümü). */ -async function expensesByDay(ctx: Ctx, s: Settings, from: string, to: string) { +export async function expensesByDay(ctx: Ctx, s: Settings, from: string, to: string) { const pipeline: PipelineStage[] = [ { $match: baseMatch(ctx, s, "expense", from, to) }, { @@ -182,6 +182,20 @@ async function expensesByDay(ctx: Ctx, s: Settings, from: string, to: string) { return { fixed, actual, variable }; } +/** Güne göre beklenen gelir (gerçekleşmiş + planlı gelir işlemleri). */ +export async function incomeByDay(ctx: Ctx, s: Settings, from: string, to: string) { + const rows = await TransactionModel.aggregate<{ _id: string; sum: number }>([ + { $match: baseMatch(ctx, s, "income", from, to) }, + { + $group: { + _id: { $dateToString: { date: "$date", format: "%Y-%m-%d", timezone: "Europe/Istanbul" } }, + sum: { $sum: "$amountMinor" }, + }, + }, + ]); + return new Map(rows.map((r) => [r._id, r.sum])); +} + export type DailyPlanView = { month: string; period: { from: string; to: string }; @@ -199,6 +213,40 @@ export type DailyPlanView = { canWrite: boolean; }; +/** + * Bir bütçe ayının günlük planı: sabit giderler kendi günlerine, değişken bütçe hane ayarına göre + * (eşit ya da önceki ayın örüntüsü) dağıtılır. + */ +export async function monthDailyPlan( + ctx: Ctx, + s: Settings, + month: string, + budgetMinor: number, + today = todayIstanbul(), +): Promise { + const period = budgetPeriod(month, s.startDay); + const byDay = await expensesByDay(ctx, s, period.from, period.to); + let weights: number[] | undefined; + if (s.dailyMode === "previous_month_pattern") { + const prev = budgetPeriod(shiftMonth(month, -1), s.startDay); + const prevDays = await expensesByDay(ctx, s, prev.from, prev.to); + const len = daysBetween(prev.from, prev.to); + weights = patternWeights( + Array.from({ length: len }, (_, i) => prevDays.variable.get(addDays(prev.from, i)) ?? 0), + daysBetween(period.from, period.to), + ); + } + return dailyPlan({ + from: period.from, + to: period.to, + today, + budgetMinor, + fixedByDay: byDay.fixed, + actualByDay: byDay.actual, + weights, + }); +} + /** Dashboard günlük planı: içinde bulunulan bütçe ayı. */ export async function getDailyPlan( userId: string, @@ -212,34 +260,14 @@ export async function getDailyPlan( const period = budgetPeriod(month, s.startDay); const nextMonth = shiftMonth(month, 1); const mode = s.dailyMode; - const [budget, byDay, income, nextIncome, nextBudget] = await Promise.all([ + const [budget, income, nextIncome, nextBudget] = await Promise.all([ getBudget(userId, householdId, month), - expensesByDay(ctx, s, period.from, period.to), incomePlan(ctx, s, month), incomePlan(ctx, s, nextMonth), getBudget(userId, householdId, nextMonth), ]); if (!budget) return null; - - let weights: number[] | undefined; - if (mode === "previous_month_pattern") { - const prev = budgetPeriod(shiftMonth(month, -1), s.startDay); - const prevDays = await expensesByDay(ctx, s, prev.from, prev.to); - const len = daysBetween(prev.from, prev.to); - weights = patternWeights( - Array.from({ length: len }, (_, i) => prevDays.variable.get(addDays(prev.from, i)) ?? 0), - daysBetween(period.from, period.to), - ); - } - const plan = dailyPlan({ - from: period.from, - to: period.to, - today, - budgetMinor: budget.totals.limitMinor, - fixedByDay: byDay.fixed, - actualByDay: byDay.actual, - weights, - }); + const plan = await monthDailyPlan(ctx, s, month, budget.totals.limitMinor, today); const forecastExpense = budget.totals.forecastMinor; return { month, diff --git a/src/features/budgets/service.ts b/src/features/budgets/service.ts index b22336a..fa5a63e 100644 --- a/src/features/budgets/service.ts +++ b/src/features/budgets/service.ts @@ -191,7 +191,11 @@ export async function getBudget( ): Promise { const ctx = await loadContext(userId, householdId, "viewer"); if (!ctx) return null; - const s = await settingsOf(ctx.m.householdId); + return budgetFor(ctx, await settingsOf(ctx.m.householdId), month); +} + +/** Bağlam ve ayarlar hazırken bütçe (raporlarda birden çok ay için yeniden yüklemeden). */ +export async function budgetFor(ctx: Ctx, s: Settings, month?: string): Promise { const today = todayIstanbul(); const currentMonth = periodOfDate(today, s.startDay); const m = month ?? currentMonth;