290 lines
8.6 KiB
TypeScript
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>
|
|
);
|
|
},
|
|
);
|