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 { PageHeader } from "../ui/PageHeader";
|
||||||
import { EmptyState } from "../ui/EmptyState";
|
import { EmptyState } from "../ui/EmptyState";
|
||||||
import { ExpenseList } from "./ExpenseList";
|
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;
|
const API_BASE = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ export default function Expense() {
|
|||||||
const sorted = useMemo(
|
const sorted = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[...(items ?? [])].sort(
|
[...(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],
|
[items],
|
||||||
);
|
);
|
||||||
@@ -103,8 +103,7 @@ export default function Expense() {
|
|||||||
const currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR";
|
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 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 totalIncome = sorted.filter((it) => !isExpense(it)).reduce((s, it) => s + (it.amount ?? 0), 0);
|
||||||
const now = new Date();
|
const thisMonth = currentMonthKey();
|
||||||
const thisMonth = monthKey(now.toISOString());
|
|
||||||
const monthItems = sorted.filter((it) => monthKey(it.occurred_at) === thisMonth);
|
const monthItems = sorted.filter((it) => monthKey(it.occurred_at) === thisMonth);
|
||||||
const monthTotal = monthItems.filter(isExpense).reduce((s, it) => s + (it.amount ?? 0), 0);
|
const monthTotal = monthItems.filter(isExpense).reduce((s, it) => s + (it.amount ?? 0), 0);
|
||||||
return { currency, totalSpent, totalIncome, thisMonth: monthLabel(thisMonth), monthItems, monthTotal };
|
return { currency, totalSpent, totalIncome, thisMonth: monthLabel(thisMonth), monthItems, monthTotal };
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
|
import { ListCellRenderer, CurrencyField, applyDisplayFormat, formatCurrency } from "../../react-openapi";
|
||||||
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
|
import type { ExpenseItem, ExpenseFieldConfigs } from "./types";
|
||||||
import { monthKey, monthLabel } from "./types";
|
import { monthKey, monthLabel, parseOccurredAt } from "./types";
|
||||||
import { ExpenseDetail } from "./ExpenseDetail";
|
import { ExpenseDetail } from "./ExpenseDetail";
|
||||||
|
|
||||||
interface GroupedMonth {
|
interface GroupedMonth {
|
||||||
@@ -30,7 +30,7 @@ function groupByMonth(items: ExpenseItem[]): GroupedMonth[] {
|
|||||||
return [...map.entries()]
|
return [...map.entries()]
|
||||||
.map(([key, list]) => {
|
.map(([key, list]) => {
|
||||||
const sorted = [...list].sort(
|
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 currency = sorted.find((it) => it.account?.currency)?.account?.currency ?? "INR";
|
||||||
const total = sorted.reduce((sum, it) => sum + (it.amount ?? 0), 0);
|
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;
|
return logo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function monthKey(iso?: string): string {
|
export function parseOccurredAt(value?: string): Date {
|
||||||
if (!iso) return "unknown";
|
const m = value?.match(/^(\d{2})-(\d{2})-(\d{4})$/);
|
||||||
const d = new Date(iso);
|
if (!m) {
|
||||||
if (Number.isNaN(d.getTime())) return "unknown";
|
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")}`;
|
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 {
|
export function monthLabel(key: string): string {
|
||||||
const [y, m] = key.split("-").map(Number);
|
const [y, m] = key.split("-").map(Number);
|
||||||
if (!y || !m) return key;
|
if (!y || !m) return key;
|
||||||
|
|||||||
Reference in New Issue
Block a user