integratedhub/islands/FinancialCalendar.tsx
2026-06-04 13:05:50 +00:00

1057 lines
36 KiB
TypeScript

// import { h } from "preact";
import {
decryptDataWithStoredPassword,
getDate,
getEncryptionPassword,
getMonth,
getYear,
MakeNewDate,
setEncryptionPassword,
} from "../helpers.ts";
import { Income } from "../services/financhalServices.ts";
import type { MonthlyBalanceCache } from "../services/dailyBalanceCache.ts";
interface Obligation {
id: string;
user_id: string | null;
description: string | null;
amount: number | null;
frequency: string | null;
due_date: number | null;
encrypted_data: string | null;
created_on: number | null;
updated_on: number | null;
}
interface FinancialCalendarProps {
obligations: Obligation[];
incomes: Income[];
yearNum: number;
monthNum: number;
oldBalance?: string;
oldDate?: string;
userId?: string;
monthlyCache?: MonthlyBalanceCache | null;
}
export function getDaysInBaseMonth(year: number, month: number) {
return new Date(year, month + 1, 0).getDate();
}
import { useEffect, useReducer } from "preact/hooks";
// Consolidated state interface for better data synchronization
interface FinancialCalendarState {
processedObligations: Obligation[];
processedIncomes: Income[];
formDate: string;
formBalance: number;
formYear: number;
formMonth: number;
showListView: boolean;
listViewType: "income" | "obligation";
selectedDay: number;
selectedDayData: {
incomes: Income[];
obligations: Obligation[];
};
}
type FinancialCalendarAction =
| { type: "SET_PROCESSED_OBLIGATIONS"; payload: Obligation[] }
| { type: "SET_PROCESSED_INCOMES"; payload: Income[] }
| { type: "SET_FORM_DATE"; payload: string }
| { type: "SET_FORM_BALANCE"; payload: number }
| { type: "SET_FORM_YEAR"; payload: number }
| { type: "SET_FORM_MONTH"; payload: number }
| {
type: "SET_FORM_DATE_BALANCE";
payload: { date: string; balance: number };
}
| { type: "SET_FORM_YEAR_MONTH"; payload: { year: number; month: number } }
| {
type: "SHOW_LIST_VIEW";
payload: {
day: number;
data: { incomes: Income[]; obligations: Obligation[] };
type: "income" | "obligation";
};
}
| { type: "HIDE_LIST_VIEW" }
| {
type: "RESET_FORM_TO_PROPS";
payload: { year: number; month: number; date?: string; balance?: string };
};
// Initial state function
function getInitialState(
props: FinancialCalendarProps,
): FinancialCalendarState {
const thisDay = new Date();
const todayStr = thisDay.toISOString().slice(0, 10);
return {
processedObligations: props.obligations,
processedIncomes: props.incomes,
formDate: props.oldDate || todayStr,
formBalance: props.oldBalance ? parseFloat(props.oldBalance) : 0,
formYear: props.yearNum,
formMonth: props.monthNum,
showListView: false,
listViewType: "income",
selectedDay: 0,
selectedDayData: { incomes: [], obligations: [] },
};
}
// Reducer function for state management
function financialCalendarReducer(
state: FinancialCalendarState,
action: FinancialCalendarAction,
): FinancialCalendarState {
switch (action.type) {
case "SET_PROCESSED_OBLIGATIONS":
return { ...state, processedObligations: action.payload };
case "SET_PROCESSED_INCOMES":
return { ...state, processedIncomes: action.payload };
case "SET_FORM_DATE":
return { ...state, formDate: action.payload };
case "SET_FORM_BALANCE":
return { ...state, formBalance: action.payload };
case "SET_FORM_YEAR":
return { ...state, formYear: action.payload };
case "SET_FORM_MONTH":
return { ...state, formMonth: action.payload };
case "SET_FORM_DATE_BALANCE":
return {
...state,
formDate: action.payload.date,
formBalance: action.payload.balance,
};
case "SET_FORM_YEAR_MONTH":
return {
...state,
formYear: action.payload.year,
formMonth: action.payload.month,
};
case "SHOW_LIST_VIEW":
return {
...state,
showListView: true,
selectedDay: action.payload.day,
selectedDayData: action.payload.data,
listViewType: action.payload.type,
};
case "HIDE_LIST_VIEW":
return {
...state,
showListView: false,
};
case "RESET_FORM_TO_PROPS": {
const thisDay = new Date();
const todayStr = thisDay.toISOString().slice(0, 10);
return {
...state,
formYear: action.payload.year,
formMonth: action.payload.month,
formDate: action.payload.date || todayStr,
formBalance: action.payload.balance
? parseFloat(action.payload.balance)
: 0,
};
}
default:
return state;
}
}
export default function FinancialCalendar(
{
obligations,
incomes,
yearNum,
monthNum,
oldBalance,
oldDate,
userId,
monthlyCache,
}: FinancialCalendarProps,
) {
// Theme classes are defined in styles.css and respond to theme mode + media.
// Consolidated state management with useReducer
const [state, dispatch] = useReducer(
financialCalendarReducer,
{ obligations, incomes, yearNum, monthNum, oldBalance, oldDate, userId },
getInitialState,
);
// Process obligations for decryption on component mount and when data changes
useEffect(() => {
if (!userId) {
dispatch({ type: "SET_PROCESSED_OBLIGATIONS", payload: obligations });
return;
}
const processObligations = async () => {
let encryptionPassword = getEncryptionPassword(userId);
const hasEncrypted = obligations.some((o) => o.encrypted_data) ||
incomes.some((i) => i.encrypted_data);
// If no local PIN but we have encrypted data, try to fetch PIN from server (e.g. new device or calendar opened first)
if (!encryptionPassword && hasEncrypted) {
try {
const res = await fetch("/api/sync-pin/get", {
credentials: "include",
cache: "no-store",
});
if (res.ok) {
const data = await res.json();
if (data.pin && setEncryptionPassword(userId, data.pin)) {
encryptionPassword = data.pin;
}
}
} catch (_e) {
// sync-pin/get fetch failed
}
}
if (!encryptionPassword) {
dispatch({ type: "SET_PROCESSED_OBLIGATIONS", payload: obligations });
return;
}
const updatedObligations: Obligation[] = [];
let decryptedCount = 0;
for (const obligation of obligations) {
const processedObligation = { ...obligation };
if (obligation.encrypted_data) {
try {
const decryptedData = await decryptDataWithStoredPassword(
userId,
obligation.encrypted_data,
);
if (decryptedData) {
const parsed = JSON.parse(decryptedData);
if (parsed.amount !== undefined) {
processedObligation.amount = parsed.amount;
}
if (parsed.description !== undefined) {
processedObligation.description = parsed.description;
}
decryptedCount++;
}
} catch {
// decrypt failed for this obligation
}
}
updatedObligations.push(processedObligation);
}
dispatch({
type: "SET_PROCESSED_OBLIGATIONS",
payload: updatedObligations,
});
};
processObligations();
}, [obligations, incomes, userId]);
// Process incomes for decryption on component mount and when data changes
useEffect(() => {
if (!userId) {
dispatch({ type: "SET_PROCESSED_INCOMES", payload: incomes });
return;
}
const processIncomes = async () => {
let encryptionPassword = getEncryptionPassword(userId);
const hasEncrypted = obligations.some((o) => o.encrypted_data) ||
incomes.some((i) => i.encrypted_data);
if (!encryptionPassword && hasEncrypted) {
try {
const res = await fetch("/api/sync-pin/get", {
credentials: "include",
cache: "no-store",
});
if (res.ok) {
const data = await res.json();
if (data.pin && setEncryptionPassword(userId, data.pin)) {
encryptionPassword = data.pin;
}
}
} catch (_e) {
// sync-pin/get fetch failed
}
}
if (!encryptionPassword) {
dispatch({ type: "SET_PROCESSED_INCOMES", payload: incomes });
return;
}
const updatedIncomes: Income[] = [];
let decryptedCount = 0;
for (const income of incomes) {
const processedIncome = { ...income };
if (income.encrypted_data) {
try {
const decryptedData = await decryptDataWithStoredPassword(
userId,
income.encrypted_data,
);
if (decryptedData) {
const parsed = JSON.parse(decryptedData);
if (parsed.amount !== undefined) {
processedIncome.amount = parsed.amount;
}
if (parsed.source !== undefined) {
processedIncome.source = parsed.source;
}
decryptedCount++;
}
} catch {
// decrypt failed for this income
}
}
updatedIncomes.push(processedIncome);
}
dispatch({ type: "SET_PROCESSED_INCOMES", payload: updatedIncomes });
};
processIncomes();
}, [incomes, obligations, userId]);
// Remove old form state variables - now managed in consolidated state
const baseMonth = monthNum - 1;
const daysInThisMonth = getDaysInBaseMonth(yearNum, baseMonth);
// Map days to markers
const dayPositiveMarkers: Record<number, number> = {};
const dayNegativeMarkers: Record<number, number> = {};
for (let day = 1; day <= daysInThisMonth; day++) {
dayPositiveMarkers[day] = 0;
dayNegativeMarkers[day] = 0;
}
state.processedObligations.forEach((ob) => {
if (!ob.due_date) return;
const d = MakeNewDate(ob.due_date);
if (getYear(d) === yearNum && getMonth(d) === baseMonth) {
const day = getDate(d);
dayNegativeMarkers[day]++;
}
});
state.processedIncomes.forEach((inc) => {
if (!inc.received_date) return;
const d = MakeNewDate(inc.received_date);
if (getYear(d) === yearNum && getMonth(d) === baseMonth) {
const day = getDate(d);
dayPositiveMarkers[day]++;
}
});
// Helper: Get data for a specific day
function getDayData(
day: number,
): { incomes: Income[]; obligations: Obligation[] } {
const dayIncomes: Income[] = [];
const dayObligations: Obligation[] = [];
state.processedIncomes.forEach((inc) => {
if (!inc.received_date) return;
const d = MakeNewDate(inc.received_date);
if (
getYear(d) === yearNum && getMonth(d) === baseMonth &&
getDate(d) === day
) {
dayIncomes.push(inc);
}
});
state.processedObligations.forEach((ob) => {
if (!ob.due_date) return;
const d = MakeNewDate(ob.due_date);
if (
getYear(d) === yearNum && getMonth(d) === baseMonth &&
getDate(d) === day
) {
dayObligations.push(ob);
}
});
return { incomes: dayIncomes, obligations: dayObligations };
}
// Helper: Get daily balances - use cache if available, otherwise compute as fallback
function getDailyBalances(): {
dailyBalances: Record<number, number>;
incomesByDay: Record<number, number[]>;
obligationsByDay: Record<number, number[]>;
} {
const dailyBalances: Record<number, number> = {};
const incomesByDay: Record<number, number[]> = {};
const obligationsByDay: Record<number, number[]> = {};
// Always calculate daily changes from client-side decrypted data
for (let d = 1; d <= daysInThisMonth; d++) {
incomesByDay[d] = [];
obligationsByDay[d] = [];
}
state.processedIncomes.forEach((inc) => {
if (!inc.received_date) return;
const d = MakeNewDate(inc.received_date);
if (getYear(d) === yearNum && getMonth(d) === baseMonth) {
const day = getDate(d);
incomesByDay[day].push(inc.amount || 0);
}
});
state.processedObligations.forEach((ob) => {
if (!ob.due_date) return;
const d = MakeNewDate(ob.due_date);
if (getYear(d) === yearNum && getMonth(d) === baseMonth) {
const day = getDate(d);
obligationsByDay[day].push(ob.amount || 0);
}
});
// If we have cached data, use it for running balances
if (monthlyCache && monthlyCache.entries) {
for (const entry of monthlyCache.entries) {
const date = new Date(entry.date);
const day = date.getDate();
dailyBalances[day] = entry.balance;
}
} else {
// Fallback: Compute running balances from scratch
const start = MakeNewDate(state.formDate);
const startYear = getYear(start);
const startMonth = getMonth(start);
const startDay = getDate(start);
const startMonthYear = startYear * 12 + startMonth;
const endMonthYear = yearNum * 12 + baseMonth;
let runningBalance = 0;
if (startMonthYear < endMonthYear) {
state.processedIncomes.forEach((inc) => {
if (!inc.received_date) return;
const d = MakeNewDate(inc.received_date);
const thisYear = getYear(d);
const thisMonth = getMonth(d);
const thisDay = getDate(d);
const thisMonthYear = thisYear * 12 + thisMonth;
if (thisMonthYear < endMonthYear) {
if (
thisYear > startYear ||
(thisYear === startYear && thisMonth > startMonth) ||
(thisYear === startYear && thisMonth === startMonth &&
thisDay >= startDay)
) {
runningBalance += inc.amount || 0;
}
}
});
state.processedObligations.forEach((ob) => {
if (!ob.due_date) return;
const d = MakeNewDate(ob.due_date);
const thisYear = getYear(d);
const thisMonth = getMonth(d);
const thisDay = getDate(d);
const thisMonthYear = thisYear * 12 + thisMonth;
if (thisMonthYear < endMonthYear) {
if (
thisYear > startYear ||
(thisYear === startYear && thisMonth > startMonth) ||
(thisYear === startYear && thisMonth === startMonth &&
thisDay >= startDay)
) {
runningBalance -= ob.amount || 0;
}
}
});
}
if (startMonthYear <= endMonthYear) {
runningBalance += state.formBalance || 0;
const sDay = (startMonthYear < endMonthYear) ? 1 : startDay;
for (let d = sDay; d <= daysInThisMonth; d++) {
if (incomesByDay[d]) {
for (const amt of incomesByDay[d]) runningBalance += amt;
}
if (obligationsByDay[d]) {
for (const amt of obligationsByDay[d]) runningBalance -= amt;
}
dailyBalances[d] = runningBalance;
}
}
}
return { dailyBalances, incomesByDay, obligationsByDay };
}
// Render calendar grid
// Explicitly create date in local time to avoid timezone issues
const first = new Date(yearNum, monthNum - 1, 1);
const firstDay = first.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
let weeks: (string | number)[][] = [];
let week: (string | number)[] = Array(firstDay).fill("");
for (let day = 1; day <= daysInThisMonth; day++) {
week.push(day);
if (week.length === 7) {
weeks.push(week);
week = [];
}
}
if (week.length) {
while (week.length < 7) week.push("");
weeks.push(week);
}
if (weeks.length < 5) {
weeks = [Array(7).fill(""), ...weeks];
}
if (weeks.length < 6) {
weeks.push(Array(7).fill(""));
}
// Month names for display
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
return (
<>
{/* Financial Balance Form */}
<div class="flex justify-center mt-14 mb-4">
<div class="w-[560px] p-4 border rounded-lg theme-calendar-form">
<div class="flex items-center gap-2">
<label
htmlFor="balance"
class="font-bold whitespace-nowrap theme-calendar-label"
>
Balance
</label>
<input
id="balance"
type="number"
step="0.01"
required
value={state.formBalance}
onInput={(e) =>
dispatch({
type: "SET_FORM_BALANCE",
payload: (e.target as HTMLInputElement).valueAsNumber,
})}
class="px-2 py-1 text-center border rounded theme-calendar-input"
/>
<label
htmlFor="date"
class="font-bold whitespace-nowrap theme-calendar-label"
>
Date
</label>
<input
id="date"
type="date"
required
value={state.formDate}
onInput={(e) =>
dispatch({
type: "SET_FORM_DATE",
payload: (e.target as HTMLInputElement).value,
})}
class="px-2 py-1 border rounded theme-calendar-input"
/>
<button
type="button"
onClick={async () => {
console.log("Set button clicked!");
console.log("Form data:", {
balance: state.formBalance,
date: state.formDate,
year: state.formYear,
month: state.formMonth,
});
try {
console.log("Making fetch request...");
const response = await fetch(
"/api/update/financial-balance",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
balance: state.formBalance,
date: state.formDate,
year: state.formYear,
month: state.formMonth,
}),
credentials: "include",
},
);
console.log("Response status:", response.status);
console.log("Response ok:", response.ok);
const responseData = await response.json();
console.log("Response data:", responseData);
if (!response.ok) {
console.error("Failed to update balance:", responseData);
alert(
`Error: ${
responseData.error || "Failed to update balance"
}`,
);
return;
}
console.log("Balance updated successfully!");
alert("Balance updated successfully!");
globalThis.location.reload();
} catch (error) {
console.error("Error updating balance:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error";
alert(`Error: ${errorMessage}`);
}
}}
class="calendar-set-button"
>
Set
</button>
</div>
</div>
</div>
<form
method="GET"
action={`/calendar/financial-calendar/${state.formYear}/${state.formMonth}?`}
>
<button
type="submit"
aria-label="Previous Month"
onClick={(_e) => {
//e.preventDefault(); // Prevent form submission
let newMonth = monthNum;
let newYear = yearNum;
if (newMonth === 1) {
if (newYear !== 1900) {
newYear = newYear - 1;
newMonth = 12;
}
} else {
newMonth = newMonth - 1;
}
console.log("newMonth", newMonth);
console.log("newYear", newYear);
dispatch({
type: "SET_FORM_YEAR_MONTH",
payload: { year: newYear, month: newMonth },
});
// // Navigate after state update
// setTimeout(() => {
// //globalThis.location.href = `/financial-calendar/${newYear}/${newMonth}`;
// }, 100);
}}
class="theme-calendar-nav-button calendar-nav-button calendar-nav-button--prev"
>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<button
type="submit"
aria-label="Next Month"
onClick={(_e) => {
//e.preventDefault(); // Prevent form submission
let newMonth = monthNum;
let newYear = yearNum;
if (newMonth === 12) {
if (newYear !== 2100) {
newYear = newYear + 1;
newMonth = 1;
}
} else {
newMonth = newMonth + 1;
}
console.log("newMonth", newMonth);
console.log("newYear", newYear);
dispatch({
type: "SET_FORM_YEAR_MONTH",
payload: { year: newYear, month: newMonth },
});
// Navigate after state update
// setTimeout(() => {
// //globalThis.location.href = `/financial-calendar/${newYear}/${newMonth}`;
// }, 100);
}}
class="theme-calendar-nav-button calendar-nav-button calendar-nav-button--next"
>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 6 15 12 9 18" />
</svg>
</button>
</form>
<div class="w-full text-center text-3xl font-bold mb-0 theme-calendar-title">
{monthNames[baseMonth]} {yearNum}
</div>
<div id="calendar" class="flex justify-center">
<table class="max-w-[500px] border-collapse text-center theme-calendar-table">
<thead>
<tr>
<th class="p-1">Sun</th>
<th class="p-1">Mon</th>
<th class="p-1">Tue</th>
<th class="p-1">Wed</th>
<th class="p-1">Thu</th>
<th class="p-1">Fri</th>
<th class="p-1">Sat</th>
</tr>
</thead>
<tbody>
{weeks.map((week, i) => (
<tr key={i}>
{(() => {
// Compute daily balances for this render
const { dailyBalances, incomesByDay, obligationsByDay } =
getDailyBalances();
// Calculate total change for each day
const getDayChange = (day: number) => {
const dayIncomes = incomesByDay[day] || [];
const dayObligations = obligationsByDay[day] || [];
const totalIncome = dayIncomes.reduce(
(sum, amount) => sum + amount,
0,
);
const totalObligations = dayObligations.reduce(
(sum, amount) => sum + amount,
0,
);
return totalIncome - totalObligations;
};
// Get today's date for highlighting current day
const today = new Date();
const todayDay = today.getDate();
const todayMonth = today.getMonth();
const todayYear = today.getFullYear();
const isCurrentMonth = yearNum === todayYear &&
baseMonth === todayMonth;
return week.map((d, j) => (
<td
key={j}
class={`p-1 h-[85px] align-top theme-calendar-cell ${
isCurrentMonth && d === todayDay
? "theme-calendar-today rounded"
: "border"
}`}
>
{d
? (
<div>
<div
id="day-markers"
class="relative w-[72px] h-8 flex items-center justify-center"
>
{dayNegativeMarkers[d as number] > 0 && (
<button
id="button-negative"
type="button"
class="absolute left-0 w-6 h-6 rounded-full bg-red-500 text-white text-xs font-bold flex items-center justify-center"
onClick={() => {
const dayData = getDayData(d as number);
dispatch({
type: "SHOW_LIST_VIEW",
payload: {
day: d as number,
data: dayData,
type: "obligation",
},
});
// Scroll to list view after a brief delay to ensure it's rendered
setTimeout(() => {
const listView = document.getElementById(
"list-view",
);
if (listView) {
listView.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}, 100);
}}
>
-{dayNegativeMarkers[d as number]}
</button>
)}
<div
id="day-number"
class="w-full font-bold theme-calendar-day"
>
{d}
</div>
{dayPositiveMarkers[d as number] > 0 && (
<button
id="button-positive"
type="button"
class="absolute right-0 w-6 h-6 rounded-full bg-green-500 text-white text-xs font-bold flex items-center justify-center"
onClick={() => {
const dayData = getDayData(d as number);
dispatch({
type: "SHOW_LIST_VIEW",
payload: {
day: d as number,
data: dayData,
type: "income",
},
});
// Scroll to list view after a brief delay to ensure it's rendered
setTimeout(() => {
const listView = document.getElementById(
"list-view",
);
if (listView) {
listView.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}, 100);
}}
>
+{dayPositiveMarkers[d as number]}
</button>
)}
</div>
{(() => {
const dayChange = getDayChange(d as number);
const hasValues = (incomesByDay[d as number] &&
incomesByDay[d as number].length > 0) ||
(obligationsByDay[d as number] &&
obligationsByDay[d as number].length > 0);
return hasValues && dayChange !== 0
? (
<div
id="day-change"
class={`w-full h-[16px] text-xs font-mono ${
dayChange >= 0
? "calendar-change-positive"
: "calendar-change-negative"
}`}
>
{dayChange >= 0 ? "+" : ""}${dayChange
.toFixed(2)}
</div>
)
: (
<div
id="day-change"
class={`w-full h-[16px] text-xs font-mono`}
>
</div>
);
})()}
{dailyBalances[d as number] !== undefined && (
<div
id="daily-balance"
class={"w-full text-sm " +
(dailyBalances[d as number] >= 0
? "calendar-balance-positive"
: "calendar-balance-negative") +
" font-mono mt-1"}
>
${dailyBalances[d as number].toFixed(2)}
</div>
)}
</div>
)
: null}
</td>
));
})()}
</tr>
))}
</tbody>
</table>
</div>
<div id="list-view" class="w-[500px] mx-auto">
{/* List View */}
{state.showListView
? (
<div class="mt-4 mx-auto">
<div class="flex justify-between w-[450px] mx-auto items-center mb-2">
<h3 class="text-xl font-bold theme-calendar-title">
{state.listViewType === "income" ? "Incomes" : "Obligations"}
{" "}
for Day {state.selectedDay}
</h3>
<button
type="button"
onClick={() => dispatch({ type: "HIDE_LIST_VIEW" })}
class="px-3 py-1 rounded theme-calendar-close-button"
>
Close
</button>
</div>
<div class="border rounded-lg theme-calendar-list-container">
{state.listViewType === "income"
? (
<div>
{state.selectedDayData.incomes.length === 0
? (
<div class="p-4 text-center theme-calendar-list-empty">
No incomes for this day
</div>
)
: (
<div class="divide-y divide-inherit">
{state.selectedDayData.incomes.map((
income,
index,
) => (
<div
key={income.id || index}
class="p-4 flex justify-between items-center"
>
<div>
<div class="font-medium calendar-income-text">
{income.source || "Income"}
</div>
<div class="text-sm theme-calendar-list-frequency">
Frequency: {income.frequency || "One-time"}
</div>
</div>
<div class="text-lg font-bold calendar-income-text">
+${income.amount?.toFixed(2) || "0.00"}
</div>
</div>
))}
</div>
)}
</div>
)
: (
<div>
{state.selectedDayData.obligations.length === 0
? (
<div class="p-4 text-center theme-calendar-list-empty">
No obligations for this day
</div>
)
: (
<div class="divide-y divide-inherit">
{state.selectedDayData.obligations.map((
obligation,
index,
) => (
<div
key={obligation.id || index}
class="p-4 flex justify-between items-center"
>
<div>
<div class="font-medium calendar-obligation-text">
{obligation.description || "Obligation"}
</div>
<div class="text-sm theme-calendar-list-frequency">
Frequency:{" "}
{obligation.frequency || "One-time"}
</div>
</div>
<div class="text-lg font-bold calendar-obligation-text">
-${obligation.amount?.toFixed(2) || "0.00"}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
)
: (
// Placeholder div to prevent layout shift
<div class="mt-4 mx-auto">
<div class="w-[450px] mx-auto mb-2 calendar-placeholder-title">
</div>
<div class="bg-transparent border-transparent rounded-lg calendar-placeholder-body">
</div>
</div>
)}
</div>
<div aria-hidden="true" class="h-28"></div>
<div class="fixed bottom-4 inset-x-0 flex justify-center z-40 pointer-events-none">
<div class="flex gap-4 w-11/12 max-w-2xl pointer-events-auto">
<a
href="/calendar/budget-obligations?modal=off"
class="flex-1 calendar-action-link calendar-action-link--obligation"
>
Manage Obligations
</a>
<a
href="/calendar/every-income?modal=off"
class="flex-1 calendar-action-link calendar-action-link--income"
>
View Incomes
</a>
</div>
</div>
</>
);
}