diff --git a/src/Expense/Expense.tsx b/src/Expense.tsx
similarity index 64%
rename from src/Expense/Expense.tsx
rename to src/Expense.tsx
index 076f4e3..8953dc7 100644
--- a/src/Expense/Expense.tsx
+++ b/src/Expense.tsx
@@ -10,32 +10,15 @@ import {
} from "@mui/material";
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import { useNavigate } from "react-router-dom";
-import { useResource, useAppContext, formatCurrency } from "../../react-openapi";
-import { PageHeader } from "../ui/PageHeader";
-import { EmptyState } from "../ui/EmptyState";
-import { ExpenseList } from "./ExpenseList";
-import { ExpenseItem, ExpenseFieldConfigs, isExpense, currentMonthKey, monthKey, monthLabel, parseOccurredAt } from "./types";
-
-function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) {
- return (
-
-
- {label}
-
-
- {value}
-
- {hint && (
-
- {hint}
-
- )}
-
- );
-}
+import { useResource, useAppContext, formatCurrency } from "../react-openapi";
+import { PageHeader } from "./ui/PageHeader";
+import { EmptyState } from "./ui/EmptyState";
+import { StatCard } from "./common/components/StatCard";
+import { TransactionList } from "./common/components/TransactionList";
+import { buildTxnFieldConfigs } from "./common/utils/fieldConfigs";
+import { isExpense } from "./common/utils/transactions";
+import { currentMonthKey, monthKey, monthLabel, parseOccurredAt } from "./common/utils/dates";
+import type { ExpenseItem } from "./common/types";
export default function Expense() {
const navigate = useNavigate();
@@ -43,33 +26,7 @@ export default function Expense() {
const { resources } = useAppContext();
const [items, setItems] = useState(null);
- const fieldConfigs = useMemo(() => {
- if (!resource) return null;
- const find = (name: string) => resource.fields.find((f) => f.name === name);
- const entity = find("entity");
- const amount = find("amount");
- const account = find("account");
- const tags = find("tags");
- const occurredAt = find("occurred_at");
- const entitiesRes = resources.find((r) => r.name === "entities");
- const accountsRes = resources.find((r) => r.name === "accounts");
- const tagsRes = resources.find((r) => r.name === "tags");
- const logo = entitiesRes?.fields.find((f) => f.name === "logo");
- if (!entity || !amount || !account || !tags || !occurredAt || !logo) return null;
- return {
- entity,
- amount,
- account,
- tags,
- occurredAt,
- logo,
- formats: {
- entity: entitiesRes?.displayFormat ?? "{name}",
- account: accountsRes?.displayFormat ?? "{name}",
- tags: tagsRes?.displayFormat ?? "{name}",
- },
- };
- }, [resource, resources]);
+ const fieldConfigs = useMemo(() => buildTxnFieldConfigs(resources), [resources]);
useEffect(() => {
let mounted = true;
@@ -80,7 +37,7 @@ export default function Expense() {
return () => {
mounted = false;
};
- }, []);
+ }, [list]);
const sorted = useMemo(
() =>
@@ -161,7 +118,7 @@ export default function Expense() {
- {fieldConfigs && }
+ {fieldConfigs && }
>
)}
diff --git a/src/Reports/GenerateReportPanel.tsx b/src/Reports/GenerateReportPanel.tsx
index 9845649..c634d05 100644
--- a/src/Reports/GenerateReportPanel.tsx
+++ b/src/Reports/GenerateReportPanel.tsx
@@ -17,7 +17,8 @@ import AddIcon from "@mui/icons-material/Add";
import RemoveCircleOutlineIcon from "@mui/icons-material/RemoveCircleOutline";
import { useResource, useAppContext, applyDisplayFormat } from "../../react-openapi";
import { useToast } from "../ui/Toast";
-import { apiErrorMessage, groupTypeEnum, isDdmmyyyy, periodHints } from "./types";
+import { isDdmmyyyy } from "../common/utils/dates";
+import { apiErrorMessage, groupTypeEnum, periodHints } from "./types";
interface GroupRow {
id: number;
diff --git a/src/Reports/Reports.tsx b/src/Reports/Report.tsx
similarity index 86%
rename from src/Reports/Reports.tsx
rename to src/Reports/Report.tsx
index 61b3e0e..4b044f9 100644
--- a/src/Reports/Reports.tsx
+++ b/src/Reports/Report.tsx
@@ -5,30 +5,14 @@ import { useResource, useAppContext, formatCurrency } from "../../react-openapi"
import { useToast } from "../ui/Toast";
import { PageHeader } from "../ui/PageHeader";
import { EmptyState } from "../ui/EmptyState";
+import { StatCard } from "../common/components/StatCard";
+import { buildTxnFieldConfigs } from "../common/utils/fieldConfigs";
import { GenerateReportPanel } from "./GenerateReportPanel";
import { ReportList } from "./ReportList";
import { ReportViewer } from "./ReportViewer";
-import { apiErrorMessage, buildReportFieldConfigs, buildTxnFieldConfigs } from "./types";
+import { apiErrorMessage, buildReportFieldConfigs } from "./types";
-function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) {
- return (
-
-
- {label}
-
-
- {value}
-
- {hint && (
-
- {hint}
-
- )}
-
- );
-}
-
-export default function Reports() {
+export default function Report() {
const { resources } = useAppContext();
const { showToast } = useToast();
const { list, loading, error } = useResource("reports");
diff --git a/src/Reports/ReportViewer.tsx b/src/Reports/ReportViewer.tsx
index db7c5a0..2ac5066 100644
--- a/src/Reports/ReportViewer.tsx
+++ b/src/Reports/ReportViewer.tsx
@@ -9,9 +9,6 @@ import {
Skeleton,
TextField,
Autocomplete,
- Accordion,
- AccordionSummary,
- AccordionDetails,
Table,
TableBody,
TableCell,
@@ -19,14 +16,12 @@ import {
TableHead,
TableRow,
} from "@mui/material";
-import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import CloseIcon from "@mui/icons-material/Close";
import CachedIcon from "@mui/icons-material/Cached";
-import { useAppContext, useResource, ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
-import { groupByMonth } from "../Expense/ExpenseList";
-import type { ExpenseItem, ExpenseFieldConfigs } from "../Expense/types";
-import { monthLabel } from "../Expense/types";
-import type { TxnFieldConfigs } from "./types";
+import { useAppContext, useResource, formatCurrency } from "../../react-openapi";
+import { StatCard } from "../common/components/StatCard";
+import { TransactionList } from "../common/components/TransactionList";
+import type { TxnFieldConfigs } from "../common/types";
import { aggregateSlice, buildPivot, metricLabels } from "./types";
interface ReportViewerProps {
@@ -37,83 +32,6 @@ interface ReportViewerProps {
onRegenerated: (report: any) => void;
}
-function StatCard({ label, value, color }: { label: string; value: string; color?: string }) {
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-}
-
-const ReportTxnRow = React.memo(function ReportTxnRow({
- item,
- currency,
- fields,
-}: {
- item: ExpenseItem;
- currency: string;
- fields: ExpenseFieldConfigs;
-}) {
- const itemCurrency = item.account?.currency ?? currency;
-
- return (
-
-
- {item.entity?.logo ? (
-
- ) : (
-
- )}
-
-
-
- {item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"}
-
-
-
-
-
- {item.account?.name && (
-
- )}
-
-
-
-
-
- );
-});
-
export function ReportViewer({ id, version, fields, onClose, onRegenerated }: ReportViewerProps) {
const { schemas } = useAppContext();
const { get } = useResource("reports");
@@ -124,7 +42,6 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
const [reload, setReload] = useState(0);
const [selectedPeriod, setSelectedPeriod] = useState("*");
const [selectedPayee, setSelectedPayee] = useState("*");
- const [openMonth, setOpenMonth] = useState(null);
useEffect(() => {
let mounted = true;
@@ -165,8 +82,6 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
[data, selectedPeriod, selectedPayee],
);
- const months = useMemo(() => groupByMonth(slice.txns), [slice.txns]);
-
const pivot = useMemo(
() => buildPivot(data ?? [], groupOptions.period, groupOptions.payee),
[data, groupOptions],
@@ -297,11 +212,7 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
const value = metricValue(m.key);
if (value == null) return null;
return (
-
+
{m.label}
@@ -363,74 +274,15 @@ export function ReportViewer({ id, version, fields, onClose, onRegenerated }: Re
)}
- {months.length === 0 ? (
+ {slice.txns.length === 0 ? (
No transactions in this slice.
- ) : (
-
- {months.map((group) => (
- setOpenMonth(isExpanded ? group.key : null)}
- TransitionProps={{ unmountOnExit: true }}
- sx={{
- border: "1px solid",
- borderColor: openMonth === group.key ? "primary.main" : "divider",
- borderRadius: 2,
- overflow: "hidden",
- boxShadow: "none",
- backgroundColor: "background.paper",
- "&:before": { display: "none" },
- "&:hover": { borderColor: "primary.light" },
- }}
- >
- }
- sx={{
- px: 2,
- py: 1,
- "& .MuiAccordionSummary-content": { alignItems: "center", gap: 1.5, minWidth: 0 },
- }}
- >
-
- {monthLabel(group.key)}
-
-
- {group.items.length} transaction{group.items.length === 1 ? "" : "s"}
-
-
-
- {formatCurrency(group.spent, group.currency)}
-
-
- /
-
-
- {formatCurrency(group.income, group.currency)}
-
-
-
-
- {fields &&
- group.items.map((item) => (
-
- ))}
-
-
-
- ))}
-
- )}
+ ) : fields ? (
+
+ ) : null}
)}
diff --git a/src/Reports/types.ts b/src/Reports/types.ts
index 1c5760c..ebbee8b 100644
--- a/src/Reports/types.ts
+++ b/src/Reports/types.ts
@@ -1,6 +1,5 @@
import type { FieldConfig, ResourceConfig } from "../../react-openapi";
-
-const DDMMYYYY = /^(\d{2})-(\d{2})-(\d{4})$/;
+import { parseDdmmyyyy } from "../common/utils/dates";
export interface ParsedGroupKey {
period?: { granularity: string; label: string };
@@ -53,15 +52,6 @@ export interface PivotTable {
totals: PivotCell[];
}
-export interface TxnFieldConfigs {
- entity: FieldConfig;
- amount: FieldConfig;
- account: FieldConfig;
- occurredAt: FieldConfig;
- logo: FieldConfig;
- formats: { entity: string; account: string };
-}
-
export interface ReportFieldConfigs {
groupLabel: FieldConfig;
granularity: FieldConfig;
@@ -94,36 +84,6 @@ export function parseCacheKey(key: string): ParsedGroupKey {
return out;
}
-export function isDdmmyyyy(value?: string): boolean {
- if (!value) return true;
- const m = value.match(DDMMYYYY);
- if (!m) return false;
- const [, dd, mm, yyyy] = m;
- const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd));
- return !(
- Number.isNaN(d.getTime()) ||
- d.getDate() !== Number(dd) ||
- d.getMonth() !== Number(mm) - 1 ||
- d.getFullYear() !== Number(yyyy)
- );
-}
-
-export function parseDdmmyyyy(value: string): Date {
- const m = value.match(DDMMYYYY);
- if (!m) throw new Error(`Date is not DD-MM-YYYY: ${value}`);
- const [, dd, mm, yyyy] = m;
- const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd));
- if (
- Number.isNaN(d.getTime()) ||
- d.getDate() !== Number(dd) ||
- d.getMonth() !== Number(mm) - 1 ||
- d.getFullYear() !== Number(yyyy)
- ) {
- throw new Error(`Invalid date: ${value}`);
- }
- return d;
-}
-
export function apiErrorMessage(e: any): string {
if (e?.response?.data) {
const d = e.response.data;
@@ -263,30 +223,6 @@ export function buildPivot(groups: ReportGroupLike[], periodOrder: string[], pay
return { rows, payees, totals };
}
-export function buildTxnFieldConfigs(resources: ResourceConfig[]): TxnFieldConfigs | null {
- const expensesRes = resources.find((r) => r.name === "expenses");
- const entitiesRes = resources.find((r) => r.name === "entities");
- const accountsRes = resources.find((r) => r.name === "accounts");
- const find = (res: ResourceConfig | undefined, name: string) => res?.fields.find((f) => f.name === name);
- const entity = find(expensesRes, "entity");
- const amount = find(expensesRes, "amount");
- const account = find(expensesRes, "account");
- const occurredAt = find(expensesRes, "occurred_at");
- const logo = find(entitiesRes, "logo");
- if (!entity || !amount || !account || !occurredAt || !logo) return null;
- return {
- entity,
- amount,
- account,
- occurredAt,
- logo,
- formats: {
- entity: entitiesRes?.displayFormat ?? "{name}",
- account: accountsRes?.displayFormat ?? "{name}",
- },
- };
-}
-
export function buildReportFieldConfigs(resources: ResourceConfig[]): ReportFieldConfigs | null {
const reportsRes = resources.find((r) => r.name === "reports");
if (!reportsRes) return null;
diff --git a/src/common/components/StatCard.tsx b/src/common/components/StatCard.tsx
new file mode 100644
index 0000000..3e4cd77
--- /dev/null
+++ b/src/common/components/StatCard.tsx
@@ -0,0 +1,26 @@
+import { Paper, Typography } from "@mui/material";
+
+interface StatCardProps {
+ label: string;
+ value: string;
+ color?: string;
+ hint?: string;
+}
+
+export function StatCard({ label, value, color, hint }: StatCardProps) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+ {hint && (
+
+ {hint}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/Expense/ExpenseList.tsx b/src/common/components/TransactionList.tsx
similarity index 69%
rename from src/Expense/ExpenseList.tsx
rename to src/common/components/TransactionList.tsx
index 5b10561..d62bf0d 100644
--- a/src/Expense/ExpenseList.tsx
+++ b/src/common/components/TransactionList.tsx
@@ -12,116 +12,18 @@ import {
import { alpha } from "@mui/material/styles";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
-import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
-import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
-import { isExpense, monthKey, monthLabel, parseOccurredAt } from "./types";
+import { formatCurrency } from "../../../react-openapi";
+import type { ExpenseItem, TxnFieldConfigs } from "../types";
+import { groupByMonth } from "../utils/transactions";
+import { monthLabel } from "../utils/dates";
+import { TransactionRow } from "./TransactionRow";
-interface GroupedMonth {
- key: string;
+interface TransactionListProps {
items: ExpenseItem[];
- spent: number;
- income: number;
- currency: string;
+ fields: TxnFieldConfigs;
}
-function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
- const map = new Map();
- for (const item of items) {
- const key = monthKey(item.occurred_at);
- const list = map.get(key) ?? [];
- list.push(item);
- map.set(key, list);
- }
- return [...map.entries()]
- .map(([key, list]) => {
- const sorted = [...list].sort(
- (a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(),
- );
- const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR";
- const spent = sorted.filter(isExpense).reduce((sum, it) => sum + Math.abs(it.amount ?? 0), 0);
- const income = sorted.filter((it) => !isExpense(it)).reduce((sum, it) => sum + (it.amount ?? 0), 0);
- return { key, items: sorted, spent, income, currency };
- })
- .sort((a, b) => b.key.localeCompare(a.key));
-}
-
-interface ExpenseRowProps {
- item: ExpenseItem;
- currency: string;
- fields: ExpenseFieldConfigs;
-}
-
-const ExpenseRow = React.memo(function ExpenseRow({ item, currency, fields }: ExpenseRowProps) {
- const itemCurrency = item.account?.currency ?? currency;
-
- return (
-
-
- {item.entity?.logo ? (
-
- ) : (
-
- )}
-
-
-
- {item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"}
-
-
-
-
-
- {item.account?.name && (
-
- )}
-
-
-
-
-
- );
-});
-
-interface ExpenseListProps {
- items: ExpenseItem[];
- fields: ExpenseFieldConfigs;
-}
-
-export function ExpenseList({ items, fields }: ExpenseListProps) {
+export function TransactionList({ items, fields }: TransactionListProps) {
const [activeMonth, setActiveMonth] = useState(null);
const [openMonth, setOpenMonth] = useState(null);
const [menuAnchor, setMenuAnchor] = useState(null);
@@ -259,7 +161,7 @@ export function ExpenseList({ items, fields }: ExpenseListProps) {
{group.items.map((item) => (
-
>
);
-}
-
-export { groupByMonth };
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/common/components/TransactionRow.tsx b/src/common/components/TransactionRow.tsx
new file mode 100644
index 0000000..cddb2c3
--- /dev/null
+++ b/src/common/components/TransactionRow.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import { Box, Typography } from "@mui/material";
+import { ListCellRenderer, CurrencyField, applyDisplayFormat } from "../../../react-openapi";
+import type { ExpenseItem, TxnFieldConfigs } from "../types";
+
+interface TransactionRowProps {
+ item: ExpenseItem;
+ currency: string;
+ fields: TxnFieldConfigs;
+}
+
+export const TransactionRow = React.memo(function TransactionRow({ item, currency, fields }: TransactionRowProps) {
+ const itemCurrency = item.account?.currency ?? currency;
+
+ return (
+
+
+ {item.entity?.logo ? (
+
+ ) : (
+
+ )}
+
+
+
+ {item.entity ? applyDisplayFormat(item.entity, fields.formats.entity) : "Unknown"}
+
+
+
+
+
+ {item.account?.name && (
+
+ )}
+
+
+
+
+
+ );
+});
\ No newline at end of file
diff --git a/src/common/types.ts b/src/common/types.ts
new file mode 100644
index 0000000..a0e9b81
--- /dev/null
+++ b/src/common/types.ts
@@ -0,0 +1,29 @@
+import type { FieldConfig } from "../../react-openapi";
+
+export interface ExpenseItem {
+ id: string;
+ entity?: { name?: string; type?: string; logo?: string } | null;
+ amount: number;
+ account?: { name?: string; number?: string; type?: string; currency?: string } | null;
+ tags?: { icon?: string; name?: string }[];
+ occurred_at?: string;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface GroupedMonth {
+ key: string;
+ items: ExpenseItem[];
+ spent: number;
+ income: number;
+ currency: string;
+}
+
+export interface TxnFieldConfigs {
+ entity: FieldConfig;
+ amount: FieldConfig;
+ account: FieldConfig;
+ occurredAt: FieldConfig;
+ logo: FieldConfig;
+ formats: { entity: string; account: string };
+}
\ No newline at end of file
diff --git a/src/Expense/types.ts b/src/common/utils/dates.ts
similarity index 52%
rename from src/Expense/types.ts
rename to src/common/utils/dates.ts
index efe769e..d6b5e83 100644
--- a/src/Expense/types.ts
+++ b/src/common/utils/dates.ts
@@ -1,36 +1,7 @@
-import type { FieldConfig } from "../../react-openapi";
-
-export interface ExpenseItem {
- id: string;
- entity?: { name?: string; type?: string; logo?: string } | null;
- amount: number;
- account?: { name?: string; number?: string; type?: string; currency?: string } | null;
- tags?: { icon?: string; name?: string }[];
- occurred_at?: string;
- created_at?: string;
- updated_at?: string;
-}
-
-export interface ExpenseFieldConfigs {
- entity: FieldConfig;
- amount: FieldConfig;
- account: FieldConfig;
- tags: FieldConfig;
- occurredAt: FieldConfig;
- logo: FieldConfig;
- formats: {
- entity: string;
- account: string;
- tags: string;
- };
-}
-
-export function isExpense(item: ExpenseItem): boolean {
- return (item.amount ?? 0) < 0;
-}
+const DDMMYYYY = /^(\d{2})-(\d{2})-(\d{4})$/;
export function parseOccurredAt(value?: string): Date {
- const m = value?.match(/^(\d{2})-(\d{2})-(\d{4})$/);
+ const m = value?.match(DDMMYYYY);
if (!m) {
throw new Error(`Expense occurred_at is not DD-MM-YYYY: ${value}`);
}
@@ -47,6 +18,36 @@ export function parseOccurredAt(value?: string): Date {
return d;
}
+export function isDdmmyyyy(value?: string): boolean {
+ if (!value) return true;
+ const m = value.match(DDMMYYYY);
+ if (!m) return false;
+ const [, dd, mm, yyyy] = m;
+ const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd));
+ return !(
+ Number.isNaN(d.getTime()) ||
+ d.getDate() !== Number(dd) ||
+ d.getMonth() !== Number(mm) - 1 ||
+ d.getFullYear() !== Number(yyyy)
+ );
+}
+
+export function parseDdmmyyyy(value: string): Date {
+ const m = value.match(DDMMYYYY);
+ if (!m) throw new Error(`Date is not DD-MM-YYYY: ${value}`);
+ const [, dd, mm, yyyy] = m;
+ const d = new Date(Number(yyyy), Number(mm) - 1, Number(dd));
+ if (
+ Number.isNaN(d.getTime()) ||
+ d.getDate() !== Number(dd) ||
+ d.getMonth() !== Number(mm) - 1 ||
+ d.getFullYear() !== Number(yyyy)
+ ) {
+ throw new Error(`Invalid date: ${value}`);
+ }
+ return d;
+}
+
export function monthKey(value?: string): string {
const d = parseOccurredAt(value);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
diff --git a/src/common/utils/fieldConfigs.ts b/src/common/utils/fieldConfigs.ts
new file mode 100644
index 0000000..0522faf
--- /dev/null
+++ b/src/common/utils/fieldConfigs.ts
@@ -0,0 +1,26 @@
+import type { ResourceConfig } from "../../../react-openapi";
+import type { TxnFieldConfigs } from "../types";
+
+export function buildTxnFieldConfigs(resources: ResourceConfig[]): TxnFieldConfigs | null {
+ const expensesRes = resources.find((r) => r.name === "expenses");
+ const entitiesRes = resources.find((r) => r.name === "entities");
+ const accountsRes = resources.find((r) => r.name === "accounts");
+ const find = (res: ResourceConfig | undefined, name: string) => res?.fields.find((f) => f.name === name);
+ const entity = find(expensesRes, "entity");
+ const amount = find(expensesRes, "amount");
+ const account = find(expensesRes, "account");
+ const occurredAt = find(expensesRes, "occurred_at");
+ const logo = find(entitiesRes, "logo");
+ if (!entity || !amount || !account || !occurredAt || !logo) return null;
+ return {
+ entity,
+ amount,
+ account,
+ occurredAt,
+ logo,
+ formats: {
+ entity: entitiesRes?.displayFormat ?? "{name}",
+ account: accountsRes?.displayFormat ?? "{name}",
+ },
+ };
+}
\ No newline at end of file
diff --git a/src/common/utils/transactions.ts b/src/common/utils/transactions.ts
new file mode 100644
index 0000000..9538088
--- /dev/null
+++ b/src/common/utils/transactions.ts
@@ -0,0 +1,27 @@
+import type { ExpenseItem, GroupedMonth } from "../types";
+import { monthKey, parseOccurredAt } from "./dates";
+
+export function isExpense(item: ExpenseItem): boolean {
+ return (item.amount ?? 0) < 0;
+}
+
+export function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
+ const map = new Map();
+ for (const item of items) {
+ const key = monthKey(item.occurred_at);
+ const list = map.get(key) ?? [];
+ list.push(item);
+ map.set(key, list);
+ }
+ return [...map.entries()]
+ .map(([key, list]) => {
+ const sorted = [...list].sort(
+ (a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(),
+ );
+ const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR";
+ const spent = sorted.filter(isExpense).reduce((sum, it) => sum + Math.abs(it.amount ?? 0), 0);
+ const income = sorted.filter((it) => !isExpense(it)).reduce((sum, it) => sum + (it.amount ?? 0), 0);
+ return { key, items: sorted, spent, income, currency };
+ })
+ .sort((a, b) => b.key.localeCompare(a.key));
+}
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
index 7c4d8ea..b5fe503 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -17,8 +17,8 @@ import {
import Home from './Home';
import FetchRequests from './FetchRequest/FetchRequestCreate';
import FetchRequestDetail from './FetchRequest/FetchRequestDetail';
-import Expense from './Expense/Expense';
-import Reports from './Reports/Reports';
+import Expense from './Expense';
+import Reports from './Reports/Report';
import { AppProvider, useAppContext, Admin, ProfileRoutes } from '../react-openapi';
import { AuthProvider, AuthPage, useAuth, ProfileCreate, ProfileEdit, ProfileView } from "../react-auth";
import Header from './Header';