refactor(budgets): bütçe, gelir planı ve günlük planı bağlam alan yardımcılara ayır
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 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ export type IncomePlan = {
|
||||
manual: { id: string; name: string; amountMinor: number }[];
|
||||
};
|
||||
|
||||
async function incomePlan(ctx: Ctx, s: Settings, month: string): Promise<IncomePlan> {
|
||||
export async function incomePlan(ctx: Ctx, s: Settings, month: string): Promise<IncomePlan> {
|
||||
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<DailyPlan> {
|
||||
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,
|
||||
|
||||
@@ -191,7 +191,11 @@ export async function getBudget(
|
||||
): Promise<BudgetView | null> {
|
||||
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<BudgetView> {
|
||||
const today = todayIstanbul();
|
||||
const currentMonth = periodOfDate(today, s.startDay);
|
||||
const m = month ?? currentMonth;
|
||||
|
||||
Reference in New Issue
Block a user