Reports frontend — spec-driven generate form, sliceable viewer, dimension bars #16

Merged
aetos merged 15 commits from cached-reporting into main 2026-08-21 14:16:36 +00:00
3 changed files with 73 additions and 9 deletions
Showing only changes of commit a107029b90 - Show all commits

View File

@@ -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>

View File

@@ -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" });
}

View File

@@ -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) {