feat: group transactions by date inside month accordions
Add two-layer grouping to the shared transaction list: each month accordion now renders one card per occurred_at date (most recent first) with a date label and txn count header, nesting the usual transaction rows inside. Add dateLabel + groupByDate helpers to src/common and use them in TransactionList. Month accordions, scroll pill/menu, and month totals are unchanged.
This commit is contained in:
@@ -14,7 +14,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import { formatCurrency } from "../../../react-openapi";
|
import { formatCurrency } from "../../../react-openapi";
|
||||||
import type { ExpenseItem, TxnFieldConfigs } from "../types";
|
import type { ExpenseItem, TxnFieldConfigs } from "../types";
|
||||||
import { groupByMonth } from "../utils/transactions";
|
import { groupByDate, groupByMonth } from "../utils/transactions";
|
||||||
import { monthLabel } from "../utils/dates";
|
import { monthLabel } from "../utils/dates";
|
||||||
import { TransactionRow } from "./TransactionRow";
|
import { TransactionRow } from "./TransactionRow";
|
||||||
|
|
||||||
@@ -160,13 +160,48 @@ export function TransactionList({ items, fields }: TransactionListProps) {
|
|||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails sx={{ p: 1.5, pt: 1 }}>
|
<AccordionDetails sx={{ p: 1.5, pt: 1 }}>
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
{group.items.map((item) => (
|
{groupByDate(group.items).map((dateGroup) => (
|
||||||
<TransactionRow
|
<Box
|
||||||
key={item.id}
|
key={dateGroup.date}
|
||||||
item={item}
|
sx={{
|
||||||
currency={group.currency}
|
border: "1px solid",
|
||||||
fields={fields}
|
borderColor: "divider",
|
||||||
/>
|
borderRadius: 2,
|
||||||
|
backgroundColor: "background.default",
|
||||||
|
p: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
px: 1,
|
||||||
|
pb: 1,
|
||||||
|
borderBottom: "1px solid",
|
||||||
|
borderColor: "divider",
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2" fontWeight={600} sx={{ letterSpacing: "-0.01em" }}>
|
||||||
|
{dateGroup.label}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ flex: 1 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{dateGroup.items.length} transaction{dateGroup.items.length === 1 ? "" : "s"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
{dateGroup.items.map((item) => (
|
||||||
|
<TransactionRow
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
currency={group.currency}
|
||||||
|
fields={fields}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
|
|||||||
@@ -63,4 +63,9 @@ export function monthLabel(key: string): string {
|
|||||||
if (!y || !m) return key;
|
if (!y || !m) return key;
|
||||||
const d = new Date(y, m - 1, 1);
|
const d = new Date(y, m - 1, 1);
|
||||||
return d.toLocaleDateString("en-IN", { month: "long", year: "numeric" });
|
return d.toLocaleDateString("en-IN", { month: "long", year: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateLabel(value: string): string {
|
||||||
|
const d = parseOccurredAt(value);
|
||||||
|
return d.toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" });
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,34 @@
|
|||||||
import type { ExpenseItem, GroupedMonth } from "../types";
|
import type { ExpenseItem, GroupedMonth } from "../types";
|
||||||
import { monthKey, parseOccurredAt } from "./dates";
|
import { dateLabel, monthKey, parseOccurredAt } from "./dates";
|
||||||
|
|
||||||
export function isExpense(item: ExpenseItem): boolean {
|
export function isExpense(item: ExpenseItem): boolean {
|
||||||
return (item.amount ?? 0) < 0;
|
return (item.amount ?? 0) < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DateGroup {
|
||||||
|
date: string;
|
||||||
|
label: string;
|
||||||
|
items: ExpenseItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupByDate(items: ExpenseItem[]): DateGroup[] {
|
||||||
|
const sorted = [...items].sort(
|
||||||
|
(a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(),
|
||||||
|
);
|
||||||
|
const map = new Map<string, ExpenseItem[]>();
|
||||||
|
for (const item of sorted) {
|
||||||
|
const key = item.occurred_at ?? "";
|
||||||
|
const list = map.get(key) ?? [];
|
||||||
|
list.push(item);
|
||||||
|
map.set(key, list);
|
||||||
|
}
|
||||||
|
return [...map.entries()].map(([date, list]) => ({
|
||||||
|
date,
|
||||||
|
label: dateLabel(date),
|
||||||
|
items: list,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
|
export function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
|
||||||
const map = new Map<string, ExpenseItem[]>();
|
const map = new Map<string, ExpenseItem[]>();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
|||||||
Reference in New Issue
Block a user