423 lines
12 KiB
TypeScript
423 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)) {
|
|
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;
|
|
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)) {
|
|
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[] = [];
|
|
|
|
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>
|
|
);
|
|
},
|
|
);
|