2026-06-04 13:05:50 +00:00

290 lines
8.6 KiB
TypeScript

import FinancialCalendar from "../../../../islands/FinancialCalendar.tsx";
import { define } from "../../../../utils.ts";
import {
getIncomesByUser,
getObligationsByUser,
Income,
Obligation,
} from "../../../../services/financhalServices.ts";
import {
getFinancialBalancesByUser,
upsertFinancialBalance,
} from "../../../../services/userService.ts";
import {
computeMonthlyBalances,
generateRecurringIncomesForMonth,
generateRecurringObligationsForMonth,
invalidateMonthCache,
type MonthlyBalanceCache,
} from "../../../../services/dailyBalanceCache.ts";
import { page } from "fresh";
import { getMonth, getYear } from "../../../../helpers.ts";
import { createPublicId } from "../../../../helpers.ts";
import { SessionCookie } from "../../../auth/callback.ts";
export const handler = define.handlers({
GET: async (ctx) => {
const params = ctx.params;
let yearNum = parseInt(params.year, 10);
let monthNum = parseInt(params.month, 10);
if (isNaN(yearNum) || isNaN(monthNum)) {
const fallback = `/calendar/financial-calendar/${getYear(new Date())}/${
getMonth(new Date()) + 1
}`;
return new Response(null, {
status: 302,
headers: { Location: fallback },
});
}
if (yearNum < 1900 || yearNum > 2100 || monthNum < 1 || monthNum > 12) {
yearNum = getYear(new Date());
monthNum = getMonth(new Date()) + 1;
}
const state = ctx.state;
const stateData: SessionCookie = state.data;
const publicId = stateData?.user_id
? await createPublicId(stateData.user_id)
: "";
const isEncrypted = stateData?.user_encrypted;
let obligations: Obligation[] = [];
let incomes: Income[] = [];
let balance: number | null = null;
let date: number | null = null;
let monthlyCache: MonthlyBalanceCache | null = null;
if (stateData?.user_id) {
const rawObligations = await getObligationsByUser(stateData.user_id) ??
[];
const filteredObligations = rawObligations.filter((ob) =>
ob.id && ob.user_id &&
ob.frequency && ob.due_date && ob.created_on &&
ob.updated_on
);
const rawIncomes = await getIncomesByUser(stateData.user_id) ?? [];
const filteredIncomes = rawIncomes.filter((inc) =>
inc.id && inc.user_id &&
inc.frequency && inc.received_date && inc.created_on &&
inc.updated_on
);
// Get base balance
const financialBalances = await getFinancialBalancesByUser(
stateData.user_id,
);
let baseBalance: { date: string; balance: number } | null = null;
if (financialBalances) {
balance = financialBalances.balance ?? null;
date = financialBalances.balance_date ?? null;
if (balance !== null && date !== null) {
baseBalance = {
date: new Date(date).toISOString().slice(0, 10),
balance: balance,
};
}
}
// Compute or retrieve cached monthly balances
monthlyCache = await computeMonthlyBalances(
stateData.user_id,
yearNum,
monthNum,
filteredIncomes,
filteredObligations,
baseBalance,
);
// Generate recurring entries for the current month for display
// (used for day markers and list views - retains encrypted_data)
obligations = generateRecurringObligationsForMonth(
filteredObligations,
yearNum,
monthNum,
);
incomes = generateRecurringIncomesForMonth(
filteredIncomes,
yearNum,
monthNum,
);
}
return page({
stateData,
obligations,
incomes,
params,
yearNum,
monthNum,
balance: balance!,
date: date ? new Date(date).toISOString().slice(0, 10) : null,
publicId,
isEncrypted,
urlRoute: `/calendar/financial-calendar/${yearNum}/${monthNum}`,
stateId: state.stateId,
monthlyCache,
});
},
POST: async (ctx) => {
const params = ctx.params;
const formData = await ctx.req.formData();
let balance: number | null = formData.get("balance")
? Number(formData.get("balance"))
: null;
let date: number | null = formData.get("date")
? Date.parse(formData.get("date") as string)
: null;
let yearNum = parseInt(params.year, 10);
let monthNum = parseInt(params.month, 10);
if (isNaN(yearNum) || isNaN(monthNum)) {
const fallback = `/calendar/financial-calendar/${getYear(new Date())}/${
getMonth(new Date()) + 1
}`;
return new Response(null, {
status: 302,
headers: { Location: fallback },
});
}
if (yearNum < 1900 || yearNum > 2100 || monthNum < 1 || monthNum > 12) {
yearNum = getYear(new Date());
monthNum = getMonth(new Date()) + 1;
}
console.log("POST url", ctx.req.url);
const state = ctx.state;
const stateData: SessionCookie = state.data;
const publicId = stateData?.user_id
? await createPublicId(stateData.user_id)
: "";
const isEncrypted = stateData?.user_encrypted;
let obligations: Obligation[] = [];
let incomes: Income[] = [];
let monthlyCache: MonthlyBalanceCache | null = null;
if (stateData?.user_id) {
const rawObligations = await getObligationsByUser(stateData.user_id) ??
[];
const filteredObligations = rawObligations.filter((ob) =>
ob.id && ob.user_id &&
ob.frequency && ob.due_date && ob.created_on &&
ob.updated_on
);
const rawIncomes = await getIncomesByUser(stateData.user_id) ?? [];
const filteredIncomes = rawIncomes.filter((inc) =>
inc.id && inc.user_id &&
inc.frequency && inc.received_date && inc.created_on &&
inc.updated_on
);
if (!balance || !date) {
const financialBalances = await getFinancialBalancesByUser(
stateData.user_id,
);
if (financialBalances) {
const latestBalance = financialBalances;
if (!balance) {
balance = latestBalance.balance ? latestBalance.balance : null;
}
if (!date) {
date = latestBalance.balance_date
? latestBalance.balance_date
: null;
}
}
} else {
// Update balance and invalidate cache for this month
await upsertFinancialBalance(
stateData.user_id,
balance,
date,
);
// Invalidate cache so next request recomputes with new base balance
await invalidateMonthCache(stateData.user_id, yearNum, monthNum);
}
// Compute or retrieve cached monthly balances
let baseBalance: { date: string; balance: number } | null = null;
if (balance !== null && date !== null) {
baseBalance = {
date: new Date(date).toISOString().slice(0, 10),
balance: balance,
};
}
monthlyCache = await computeMonthlyBalances(
stateData.user_id,
yearNum,
monthNum,
filteredIncomes,
filteredObligations,
baseBalance,
);
// Generate recurring entries for the current month for display
// (used for day markers and list views - retains encrypted_data)
obligations = generateRecurringObligationsForMonth(
filteredObligations,
yearNum,
monthNum,
);
incomes = generateRecurringIncomesForMonth(
filteredIncomes,
yearNum,
monthNum,
);
}
return page({
stateData,
obligations,
incomes,
params,
yearNum,
monthNum,
balance,
date: date ? new Date(date).toISOString().slice(0, 10) : null,
publicId,
isEncrypted,
urlRoute: `/calendar/financial-calendar/${yearNum}/${monthNum}`,
stateId: state.stateId,
monthlyCache,
});
},
});
export default define.page(
(
{ data }: {
data: {
stateData: SessionCookie;
obligations: Obligation[];
incomes: Income[];
yearNum: number;
monthNum: number;
balance?: string;
date?: string;
publicId: string;
isEncrypted: boolean;
urlRoute: string;
stateId: string;
monthlyCache?: MonthlyBalanceCache | null;
};
},
) => {
return (
<main class="calendar-route-shell">
<div class="calendar-route-content">
<FinancialCalendar
obligations={data.obligations}
incomes={data.incomes}
yearNum={data.yearNum}
monthNum={data.monthNum}
oldBalance={data.balance}
oldDate={data.date}
userId={data.publicId}
monthlyCache={data.monthlyCache}
/>
</div>
</main>
);
},
);