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 { formatCurrency } from "../../../react-openapi";
|
||||
import type { ExpenseItem, TxnFieldConfigs } from "../types";
|
||||
import { groupByMonth } from "../utils/transactions";
|
||||
import { groupByDate, groupByMonth } from "../utils/transactions";
|
||||
import { monthLabel } from "../utils/dates";
|
||||
import { TransactionRow } from "./TransactionRow";
|
||||
|
||||
@@ -160,13 +160,48 @@ export function TransactionList({ items, fields }: TransactionListProps) {
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ p: 1.5, pt: 1 }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{group.items.map((item) => (
|
||||
<TransactionRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
currency={group.currency}
|
||||
fields={fields}
|
||||
/>
|
||||
{groupByDate(group.items).map((dateGroup) => (
|
||||
<Box
|
||||
key={dateGroup.date}
|
||||
sx={{
|
||||
border: "1px solid",
|
||||
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>
|
||||
</AccordionDetails>
|
||||
|
||||
@@ -63,4 +63,9 @@ export function monthLabel(key: string): string {
|
||||
if (!y || !m) return key;
|
||||
const d = new Date(y, m - 1, 1);
|
||||
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 { monthKey, parseOccurredAt } from "./dates";
|
||||
import { dateLabel, monthKey, parseOccurredAt } from "./dates";
|
||||
|
||||
export function isExpense(item: ExpenseItem): boolean {
|
||||
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[] {
|
||||
const map = new Map<string, ExpenseItem[]>();
|
||||
for (const item of items) {
|
||||
|
||||
Reference in New Issue
Block a user