2026-01-20 09:02:26 -06:00

418 lines
12 KiB
TypeScript

import FinancialCalendar, {
getDaysInBaseMonth,
} 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 { page } from "fresh";
import {
getDate,
getMonth,
getYear,
MakeNewDate,
} from "../../../../helpers.ts";
import { createPublicId } from "../../../../helpers.ts";
import { SessionCookie } from "../../../auth/callback.ts";
// Helper function to generate recurring income entries
function generateRecurringIncomes(
incomes: Income[],
targetYear: number,
targetMonth: number,
): Income[] {
const result: Income[] = [];
const baseMonth = targetMonth - 1;
for (const income of incomes) {
if (!income.frequency || !income.received_date) continue;
let currentDate = MakeNewDate(income.received_date);
let daysInMonth = getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
);
while (
(getYear(currentDate) === targetYear &&
getMonth(currentDate) <= baseMonth) ||
getYear(currentDate) < targetYear
) {
if (getDate(currentDate) <= daysInMonth) {
result.push({
...income,
received_date: currentDate.getTime(),
});
}
switch (income.frequency.toLowerCase()) {
case "daily":
currentDate = MakeNewDate(
currentDate.getTime() + 24 * 60 * 60 * 1000,
);
break;
case "biweekly":
currentDate = MakeNewDate(
currentDate.getTime() + 14 * 24 * 60 * 60 * 1000,
);
break;
case "monthly":
currentDate = MakeNewDate(
currentDate.getTime() +
getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
) * 24 * 60 * 60 * 1000,
);
break;
case "one-time":
currentDate = MakeNewDate((targetYear + 2) + "-01-01");
break;
case "weekly":
currentDate = MakeNewDate(
currentDate.getTime() + 7 * 24 * 60 * 60 * 1000,
);
break;
case "quarterly":
currentDate = MakeNewDate(
currentDate.getTime() +
Number(
getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
),
) * 3 * 24 * 60 * 60 * 1000,
);
break;
case "yearly":
case "annually": {
const monthNumber = getMonth(currentDate) + 1;
const monthStr = monthNumber > 9 ? monthNumber : "0" + monthNumber;
currentDate = MakeNewDate(
(getYear(currentDate) + 1) + "-" + monthStr + "-" +
getDate(currentDate),
);
break;
}
default:
// Unknown frequency, skip to prevent infinite loop
currentDate = MakeNewDate((targetYear + 2) + "-01-01");
break;
}
daysInMonth = getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
);
}
}
return result;
}
// Helper function to generate recurring obligation entries
function generateRecurringObligations(
obligations: Obligation[],
targetYear: number,
targetMonth: number,
): Obligation[] {
const result: Obligation[] = [];
const baseMonth = targetMonth - 1;
for (const obligation of obligations) {
if (!obligation.frequency || !obligation.due_date) continue;
let currentDate = MakeNewDate(obligation.due_date);
let daysInMonth = getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
);
while (
(getYear(currentDate) === targetYear &&
getMonth(currentDate) <= baseMonth) ||
getYear(currentDate) < targetYear
) {
if (getDate(currentDate) <= daysInMonth) {
result.push({
...obligation,
due_date: currentDate.getTime(),
});
}
switch (obligation.frequency.toLowerCase()) {
case "daily":
currentDate = MakeNewDate(
currentDate.getTime() + 24 * 60 * 60 * 1000,
);
break;
case "biweekly":
currentDate = MakeNewDate(
currentDate.getTime() + 14 * 24 * 60 * 60 * 1000,
);
break;
case "monthly":
currentDate = MakeNewDate(
currentDate.getTime() +
getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
) * 24 * 60 * 60 * 1000,
);
break;
case "one-time":
currentDate = MakeNewDate((targetYear + 2) + "-01-01");
break;
case "weekly":
currentDate = MakeNewDate(
currentDate.getTime() + 7 * 24 * 60 * 60 * 1000,
);
break;
case "quarterly":
currentDate = MakeNewDate(
currentDate.getTime() +
Number(
getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
),
) * 3 * 24 * 60 * 60 * 1000,
);
break;
case "yearly":
case "annually": {
const monthNumber = getMonth(currentDate) + 1;
const monthStr = monthNumber > 9 ? monthNumber : "0" + monthNumber;
currentDate = MakeNewDate(
(getYear(currentDate) + 1) + "-" + monthStr + "-" +
getDate(currentDate),
);
break;
}
default:
// Unknown frequency, skip to prevent infinite loop
currentDate = MakeNewDate((targetYear + 2) + "-01-01");
break;
}
daysInMonth = getDaysInBaseMonth(
getYear(currentDate),
getMonth(currentDate),
);
}
}
return result;
}
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)) {
return new Response(null, {
status: 302,
headers: { Location: "/auth/login" },
});
}
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;
let date: number | 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
);
obligations = generateRecurringObligations(
filteredObligations,
yearNum,
monthNum,
);
incomes = generateRecurringIncomes(filteredIncomes, yearNum, monthNum);
const financialBalances = await getFinancialBalancesByUser(
stateData.user_id,
);
if (financialBalances) {
const latestBalance = financialBalances;
balance = latestBalance.balance;
date = latestBalance.balance_date;
}
}
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,
});
},
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)) {
return new Response(null, {
status: 302,
headers: { Location: "/auth/login" },
});
}
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;
const stateId = state.stateId;
let obligations: Obligation[] = [];
let incomes: Income[] = [];
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
);
obligations = generateRecurringObligations(
filteredObligations,
yearNum,
monthNum,
);
incomes = generateRecurringIncomes(filteredIncomes, yearNum, monthNum);
console.log("obligations", obligations.length);
console.log("incomes", incomes.length);
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 {
const updatedBalance = await upsertFinancialBalance(
stateData.user_id,
balance,
date,
);
}
}
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,
});
},
});
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;
};
},
) => {
return (
<main class="min-h-screen flex flex-col items-center justify-start pt-0">
<div class="w-full max-w-4xl px-8 sm:px-12 lg:px-24 mt-8">
<FinancialCalendar
obligations={data.obligations}
incomes={data.incomes}
yearNum={data.yearNum}
monthNum={data.monthNum}
oldBalance={data.balance}
oldDate={data.date}
userId={data.publicId}
/>
</div>
</main>
);
},
);