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 }[];
|
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 { from, to } = budgetPeriod(month, s.startDay);
|
||||||
const [docs, budget] = await Promise.all([
|
const [docs, budget] = await Promise.all([
|
||||||
TransactionModel.find(baseMatch(ctx, s, "income", from, to))
|
TransactionModel.find(baseMatch(ctx, s, "income", from, to))
|
||||||
@@ -145,7 +145,7 @@ export async function saveIncomeItems(
|
|||||||
// --- Günlük plan ---
|
// --- Günlük plan ---
|
||||||
|
|
||||||
/** Güne göre gider: sabit (düzenli/taksit; gerçekleşmiş + planlı) ve gerçekleşen (tümü). */
|
/** 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[] = [
|
const pipeline: PipelineStage[] = [
|
||||||
{ $match: baseMatch(ctx, s, "expense", from, to) },
|
{ $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 };
|
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 = {
|
export type DailyPlanView = {
|
||||||
month: string;
|
month: string;
|
||||||
period: { from: string; to: string };
|
period: { from: string; to: string };
|
||||||
@@ -199,6 +213,40 @@ export type DailyPlanView = {
|
|||||||
canWrite: boolean;
|
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ı. */
|
/** Dashboard günlük planı: içinde bulunulan bütçe ayı. */
|
||||||
export async function getDailyPlan(
|
export async function getDailyPlan(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -212,34 +260,14 @@ export async function getDailyPlan(
|
|||||||
const period = budgetPeriod(month, s.startDay);
|
const period = budgetPeriod(month, s.startDay);
|
||||||
const nextMonth = shiftMonth(month, 1);
|
const nextMonth = shiftMonth(month, 1);
|
||||||
const mode = s.dailyMode;
|
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),
|
getBudget(userId, householdId, month),
|
||||||
expensesByDay(ctx, s, period.from, period.to),
|
|
||||||
incomePlan(ctx, s, month),
|
incomePlan(ctx, s, month),
|
||||||
incomePlan(ctx, s, nextMonth),
|
incomePlan(ctx, s, nextMonth),
|
||||||
getBudget(userId, householdId, nextMonth),
|
getBudget(userId, householdId, nextMonth),
|
||||||
]);
|
]);
|
||||||
if (!budget) return null;
|
if (!budget) return null;
|
||||||
|
const plan = await monthDailyPlan(ctx, s, month, budget.totals.limitMinor, today);
|
||||||
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 forecastExpense = budget.totals.forecastMinor;
|
const forecastExpense = budget.totals.forecastMinor;
|
||||||
return {
|
return {
|
||||||
month,
|
month,
|
||||||
|
|||||||
@@ -191,7 +191,11 @@ export async function getBudget(
|
|||||||
): Promise<BudgetView | null> {
|
): Promise<BudgetView | null> {
|
||||||
const ctx = await loadContext(userId, householdId, "viewer");
|
const ctx = await loadContext(userId, householdId, "viewer");
|
||||||
if (!ctx) return null;
|
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 today = todayIstanbul();
|
||||||
const currentMonth = periodOfDate(today, s.startDay);
|
const currentMonth = periodOfDate(today, s.startDay);
|
||||||
const m = month ?? currentMonth;
|
const m = month ?? currentMonth;
|
||||||
|
|||||||
Reference in New Issue
Block a user