fix expenses grouping to parse occurred_at as DD-MM-YYYY strictly
occurred_at comes from the backend as a DD-MM-YYYY string, which new Date() cannot parse (NaN), so most transactions fell into an "unknown" bucket and month sorting was broken. - add parseOccurredAt() that only accepts DD-MM-YYYY, day-first, and throws on any other format or invalid date - monthKey() now throws instead of returning "unknown" - add currentMonthKey() for the "this month" stat - sort groupByMonth/summary by parsed timestamp
This commit is contained in:
@@ -14,7 +14,7 @@ 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, monthKey, monthLabel, resolveLogoUrl } from "./types";
|
||||
import { ExpenseItem, ExpenseFieldConfigs, isExpense, currentMonthKey, monthKey, monthLabel, parseOccurredAt, resolveLogoUrl } from "./types";
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function Expense() {
|
||||
const sorted = useMemo(
|
||||
() =>
|
||||
[...(items ?? [])].sort(
|
||||
(a, b) => new Date(b.occurred_at ?? 0).getTime() - new Date(a.occurred_at ?? 0).getTime(),
|
||||
(a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(),
|
||||
),
|
||||
[items],
|
||||
);
|
||||
@@ -103,8 +103,7 @@ export default function Expense() {
|
||||
const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR";
|
||||
const totalSpent = sorted.filter(isExpense).reduce((s, it) => s + (it.amount ?? 0), 0);
|
||||
const totalIncome = sorted.filter((it) => !isExpense(it)).reduce((s, it) => s + (it.amount ?? 0), 0);
|
||||
const now = new Date();
|
||||
const thisMonth = monthKey(now.toISOString());
|
||||
const thisMonth = currentMonthKey();
|
||||
const monthItems = sorted.filter((it) => monthKey(it.occurred_at) === thisMonth);
|
||||
const monthTotal = monthItems.filter(isExpense).reduce((s, it) => s + (it.amount ?? 0), 0);
|
||||
return { currency, totalSpent, totalIncome, thisMonth: monthLabel(thisMonth), monthItems, monthTotal };
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
|
||||
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
|
||||
import { monthKey, monthLabel } from "./types";
|
||||
import { monthKey, monthLabel, parseOccurredAt } from "./types";
|
||||
import { ExpenseDetail } from "./ExpenseDetail";
|
||||
|
||||
interface GroupedMonth {
|
||||
@@ -30,7 +30,7 @@ function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
|
||||
return [...map.entries()]
|
||||
.map(([key, list]) => {
|
||||
const sorted = [...list].sort(
|
||||
(a, b) => new Date(b.occurred_at ?? 0).getTime() - new Date(a.occurred_at ?? 0).getTime(),
|
||||
(a, b) => parseOccurredAt(b.occurred_at).getTime() - parseOccurredAt(a.occurred_at).getTime(),
|
||||
);
|
||||
const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR";
|
||||
const total = sorted.reduce((sum, it) => sum + (it.amount ?? 0), 0);
|
||||
|
||||
@@ -36,13 +36,34 @@ export function resolveLogoUrl(logo?: string, base?: string): string | undefined
|
||||
return logo;
|
||||
}
|
||||
|
||||
export function monthKey(iso?: string): string {
|
||||
if (!iso) return "unknown";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "unknown";
|
||||
export function parseOccurredAt(value?: string): Date {
|
||||
const m = value?.match(/^(\d{2})-(\d{2})-(\d{4})$/);
|
||||
if (!m) {
|
||||
throw new Error(`Expense occurred_at 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 expense occurred_at date: ${value}`);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
export function monthKey(value?: string): string {
|
||||
const d = parseOccurredAt(value);
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function currentMonthKey(): string {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export function monthLabel(key: string): string {
|
||||
const [y, m] = key.split("-").map(Number);
|
||||
if (!y || !m) return key;
|
||||
|
||||
Reference in New Issue
Block a user