diff --git a/src/Expense/ExpenseList.tsx b/src/Expense/ExpenseList.tsx index ee48be3..93b00c2 100644 --- a/src/Expense/ExpenseList.tsx +++ b/src/Expense/ExpenseList.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, Typography, @@ -6,16 +6,18 @@ import { AccordionSummary, AccordionDetails, } from "@mui/material"; +import { alpha } from "@mui/material/styles"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi"; import type { ExpenseItem, ExpenseFieldConfigs } from "./types"; -import { monthKey, monthLabel, parseOccurredAt } from "./types"; +import { isExpense, monthKey, monthLabel, parseOccurredAt } from "./types"; import { ExpenseDetail } from "./ExpenseDetail"; interface GroupedMonth { key: string; items: ExpenseItem[]; - total: number; + spent: number; + income: number; currency: string; } @@ -33,8 +35,9 @@ function groupByMonth(items: ExpenseItem[]): GroupedMonth[] { (a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(), ); const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR"; - const total = sorted.reduce((sum, it) => sum + (it.amount ?? 0), 0); - return { key, items: sorted, total, currency }; + const spent = sorted.filter(isExpense).reduce((sum, it) => sum + Math.abs(it.amount ?? 0), 0); + const income = sorted.filter((it) => !isExpense(it)).reduce((sum, it) => sum + (it.amount ?? 0), 0); + return { key, items: sorted, spent, income, currency }; }) .sort((a, b) => b.key.localeCompare(a.key)); } @@ -129,16 +132,70 @@ interface ExpenseListProps { export function ExpenseList({ items, fields }: ExpenseListProps) { const [expandedId, setExpandedId] = useState(null); + const [activeMonth, setActiveMonth] = useState(null); const groups = useMemo(() => groupByMonth(items), [items]); + const listRef = useRef(null); const handleToggle = useCallback((id: string) => { setExpandedId((prev) => (prev === id ? null : id)); }, []); + useEffect(() => { + const root = listRef.current; + if (!root || groups.length === 0) return; + let ticking = false; + + const update = () => { + ticking = false; + const headers = root.querySelectorAll("[data-month-header]"); + let current: string | null = null; + for (const header of headers) { + if (header.getBoundingClientRect().top <= 72) { + current = header.dataset.monthHeader ?? null; + } else { + break; + } + } + setActiveMonth(current); + }; + + const onScroll = () => { + if (!ticking) { + ticking = true; + requestAnimationFrame(update); + } + }; + + update(); + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("resize", onScroll); + return () => { + window.removeEventListener("scroll", onScroll); + window.removeEventListener("resize", onScroll); + }; + }, [groups]); + return ( - - {groups.map((group) => ( - - + <> + + {groups.map((group) => ( + + alpha(theme.palette.background.default, 0.85), + backdropFilter: "blur(8px)", + borderBottom: "1px solid", + borderColor: "divider", + }} + > {monthLabel(group.key)} @@ -146,8 +203,14 @@ export function ExpenseList({ items, fields }: ExpenseListProps) { {group.items.length} transaction{group.items.length === 1 ? "" : "s"} - - {formatCurrency(group.total, group.currency)} + + {formatCurrency(group.spent, group.currency)} + + + / + + + {formatCurrency(group.income, group.currency)} @@ -165,7 +228,40 @@ export function ExpenseList({ items, fields }: ExpenseListProps) { ))} - + + + + alpha(theme.palette.background.default, 0.85), + backdropFilter: "blur(8px)", + border: "1px solid", + borderColor: "divider", + boxShadow: 1, + }} + > + + {activeMonth ? monthLabel(activeMonth) : ""} + + + + ); }