diff --git a/src/Expense/Expense.tsx b/src/Expense/Expense.tsx index b0b2df0..c290302 100644 --- a/src/Expense/Expense.tsx +++ b/src/Expense/Expense.tsx @@ -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 }; diff --git a/src/Expense/ExpenseList.tsx b/src/Expense/ExpenseList.tsx index 0e07670..ee48be3 100644 --- a/src/Expense/ExpenseList.tsx +++ b/src/Expense/ExpenseList.tsx @@ -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); diff --git a/src/Expense/types.ts b/src/Expense/types.ts index 746eaaf..1bd3ff9 100644 --- a/src/Expense/types.ts +++ b/src/Expense/types.ts @@ -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;