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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user