expense list: replace accordion with flat Stripe-style rows

- drop the per-row accordion and delete ExpenseDetail (was only used
  for the expanded view); rows are now static bordered cards with a
  hover highlight
- row layout: logo | name/date + account chip | amount
- tag chips were added to the row during iteration but removed for
  now until a better placement is settled
This commit is contained in:
2026-08-18 13:44:06 +05:30
parent 537aef94ff
commit 8e9a13dd19
2 changed files with 51 additions and 101 deletions

View File

@@ -1,24 +0,0 @@
import React from "react";
import { Box, Divider } from "@mui/material";
import { DetailFieldRenderer } from "../../react-openapi";
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
interface ExpenseDetailProps {
item: ExpenseItem;
fields: ExpenseFieldConfigs;
}
export function ExpenseDetail({ item, fields }: ExpenseDetailProps) {
return (
<Box sx={{ pt: 1, pb: 0.5 }}>
<Divider sx={{ mb: 1.5 }} />
<Box sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
<DetailFieldRenderer field={fields.entity} value={item.entity} displayFormat={fields.formats.entity} />
<DetailFieldRenderer field={fields.amount} value={item.amount} />
<DetailFieldRenderer field={fields.account} value={item.account} displayFormat={fields.formats.account} />
<DetailFieldRenderer field={fields.tags} value={item.tags} displayFormat={fields.formats.tags} />
<DetailFieldRenderer field={fields.occurredAt} value={item.occurred_at} />
</Box>
</Box>
);
}

View File

@@ -1,18 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Box,
Typography,
Accordion,
AccordionSummary,
AccordionDetails,
} from "@mui/material";
import { Box, Typography } from "@mui/material";
import { alpha } from "@mui/material/styles";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
import { isExpense, monthKey, monthLabel, parseOccurredAt } from "./types";
import { ExpenseDetail } from "./ExpenseDetail";
interface GroupedMonth {
key: string;
@@ -43,86 +35,74 @@ function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
.sort((a, b) => b.key.localeCompare(a.key));
}
interface ExpenseCardProps {
interface ExpenseRowProps {
item: ExpenseItem;
expanded: boolean;
currency: string;
fields: ExpenseFieldConfigs;
onToggle: (id: string) => void;
}
const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency, fields, onToggle }: ExpenseCardProps) {
const ExpenseRow = React.memo(function ExpenseRow({ item, currency, fields }: ExpenseRowProps) {
const itemCurrency = item.account?.currency ?? currency;
return (
<Accordion
disableGutters
expanded={expanded}
onChange={(_, isExpanded) => onToggle(isExpanded ? item.id : "")}
TransitionProps={{ unmountOnExit: true }}
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 2,
px: 2,
py: 1.25,
border: "1px solid",
borderColor: expanded ? "primary.main" : "divider",
borderColor: "divider",
borderRadius: 2,
overflow: "hidden",
boxShadow: "none",
"&:before": { display: "none" },
transition: "border-color 160ms ease, background-color 160ms ease",
"&:hover": { borderColor: "primary.light" },
backgroundColor: "background.paper",
transition: "background-color 160ms ease, border-color 160ms ease",
"&:hover": {
backgroundColor: "action.hover",
borderColor: "primary.light",
},
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
<Box
sx={{
"& .MuiAccordionSummary-content": {
alignItems: "center",
gap: 2,
minWidth: 0,
py: 0.5,
},
width: 40,
flexShrink: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Box
sx={{
width: 40,
flexShrink: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
{item.entity?.logo ? (
<ListCellRenderer field={fields.logo} value={item.entity.logo} />
) : (
<Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: "action.hover" }} />
)}
</Box>
<Box sx={{ flex: "0 1 auto", minWidth: 0 }}>
<Typography
variant="body1"
fontWeight={600}
noWrap
sx={{ fontSize: "0.9375rem", lineHeight: 1.3 }}
>
{item.entity?.logo ? (
<ListCellRenderer field={fields.logo} value={item.entity.logo} />
) : (
<Box sx={{ width: 40, height: 40, borderRadius: 2, bgcolor: "action.hover" }} />
{item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"}
</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
<Box sx={{ color: "text.secondary" }}>
<ListCellRenderer field={fields.occurredAt} value={item.occurred_at} />
</Box>
{item.account?.name && (
<ListCellRenderer
field={fields.account}
value={item.account}
displayFormat={fields.formats.account}
/>
)}
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
variant="body1"
fontWeight={600}
noWrap
sx={{ fontSize: "0.9375rem", lineHeight: 1.3 }}
>
{item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"}
</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mt: 0.25, flexWrap: "wrap" }}>
<ListCellRenderer field={fields.occurredAt} value={item.occurred_at} />
{item.account?.name && (
<ListCellRenderer
field={fields.account}
value={item.account}
displayFormat={fields.formats.account}
/>
)}
</Box>
</Box>
<CurrencyField value={item.amount} currency={itemCurrency} large />
</AccordionSummary>
<AccordionDetails sx={{ pt: 0 }}>
<ExpenseDetail item={item} fields={fields} />
</AccordionDetails>
</Accordion>
</Box>
<Box sx={{ flex: 1 }} />
<CurrencyField value={item.amount} currency={itemCurrency} large />
</Box>
);
});
@@ -132,13 +112,9 @@ interface ExpenseListProps {
}
export function ExpenseList({ items, fields }: ExpenseListProps) {
const [expandedId, setExpandedId] = useState<string | null>(null);
const [activeMonth, setActiveMonth] = useState<string | null>(null);
const groups = useMemo(() => groupByMonth(items), [items]);
const listRef = useRef<HTMLDivElement>(null);
const handleToggle = useCallback((id: string) => {
setExpandedId((prev) => (prev === id ? null : id));
}, []);
const jumpToActiveMonth = useCallback(() => {
if (!activeMonth) return;
@@ -239,13 +215,11 @@ export function ExpenseList({ items, fields }: ExpenseListProps) {
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{group.items.map((item) => (
<ExpenseCard
<ExpenseRow
key={item.id}
item={item}
expanded={expandedId === item.id}
currency={group.currency}
fields={fields}
onToggle={handleToggle}
/>
))}
</Box>