shared currency field + expenses rendered from react-openapi fields
- add CurrencyField renderer (inherits NumberField for editing) with cached Intl formatter; route via uiType "currency" in list/detail/form - add resourceConfig.fieldTypes override applied in AppProvider, so amount becomes currency for both Admin and the Expenses page - render Expenses page through react-openapi fields (ListCellRenderer, DetailFieldRenderer, applyDisplayFormat) instead of custom fields - resolve relative /uploads entity logos against the API base URL on load so entity logos render on the expenses feed - drop custom avatar, currency/date formatters from the Expense module
This commit is contained in:
@@ -5,13 +5,11 @@ import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Avatar,
|
||||
Chip,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import type { ExpenseItem } from "./types";
|
||||
import { formatCurrency, formatDate, isExpense, monthKey, monthLabel } from "./types";
|
||||
import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
|
||||
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
|
||||
import { monthKey, monthLabel } from "./types";
|
||||
import { ExpenseDetail } from "./ExpenseDetail";
|
||||
|
||||
interface GroupedMonth {
|
||||
@@ -41,40 +39,15 @@ function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
|
||||
.sort((a, b) => b.key.localeCompare(a.key));
|
||||
}
|
||||
|
||||
function EntityAvatar({ entity }: { entity: ExpenseItem["entity"] }) {
|
||||
const theme = useTheme();
|
||||
const name = entity?.name ?? "?";
|
||||
const letter = name.trim().charAt(0).toUpperCase() || "?";
|
||||
const logo = entity?.logo;
|
||||
const isImage = typeof logo === "string" && (logo.startsWith("http") || logo.startsWith("data:"));
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
src={isImage ? logo : undefined}
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
fontSize: 15,
|
||||
fontWeight: 700,
|
||||
bgcolor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
{letter}
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExpenseCardProps {
|
||||
item: ExpenseItem;
|
||||
expanded: boolean;
|
||||
currency: string;
|
||||
fields: ExpenseFieldConfigs;
|
||||
onToggle: (id: string) => void;
|
||||
}
|
||||
|
||||
const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency, onToggle }: ExpenseCardProps) {
|
||||
const negative = isExpense(item);
|
||||
const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency, fields, onToggle }: ExpenseCardProps) {
|
||||
const itemCurrency = item.account?.currency ?? currency;
|
||||
|
||||
return (
|
||||
@@ -105,7 +78,21 @@ const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<EntityAvatar entity={item.entity} />
|
||||
<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: 1, minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
@@ -113,43 +100,34 @@ const ExpenseCard = React.memo(function ExpenseCard({ item, expanded, currency,
|
||||
noWrap
|
||||
sx={{ fontSize: "0.9375rem", lineHeight: 1.3 }}
|
||||
>
|
||||
{item.entity?.name ?? "Unknown"}
|
||||
{item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mt: 0.25 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatDate(item.occurred_at)}
|
||||
</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 && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={item.account.name}
|
||||
variant="outlined"
|
||||
sx={{ height: 20, fontSize: 11, "& .MuiChip-label": { px: 1 } }}
|
||||
<ListCellRenderer
|
||||
field={fields.account}
|
||||
value={item.account}
|
||||
displayFormat={fields.formats.account}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight={700}
|
||||
sx={{
|
||||
fontSize: "0.9375rem",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
color: negative ? "error.main" : "success.main",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{formatCurrency(item.amount, itemCurrency)}
|
||||
</Typography>
|
||||
<CurrencyField value={item.amount} currency={itemCurrency} large />
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ pt: 0 }}>
|
||||
<ExpenseDetail item={item} />
|
||||
<ExpenseDetail item={item} fields={fields} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
});
|
||||
|
||||
export function ExpenseList({ items }: { items: ExpenseItem[] }) {
|
||||
interface ExpenseListProps {
|
||||
items: ExpenseItem[];
|
||||
fields: ExpenseFieldConfigs;
|
||||
}
|
||||
|
||||
export function ExpenseList({ items, fields }: ExpenseListProps) {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const groups = useMemo(() => groupByMonth(items), [items]);
|
||||
const handleToggle = useCallback((id: string) => {
|
||||
@@ -180,6 +158,7 @@ export function ExpenseList({ items }: { items: ExpenseItem[] }) {
|
||||
item={item}
|
||||
expanded={expandedId === item.id}
|
||||
currency={group.currency}
|
||||
fields={fields}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
))}
|
||||
@@ -190,4 +169,4 @@ export function ExpenseList({ items }: { items: ExpenseItem[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
export { groupByMonth };
|
||||
export { groupByMonth };
|
||||
Reference in New Issue
Block a user