diff --git a/src/components/charts/SpendingTrendChart.tsx b/src/components/charts/SpendingTrendChart.tsx index 68e3412..54e1db3 100644 --- a/src/components/charts/SpendingTrendChart.tsx +++ b/src/components/charts/SpendingTrendChart.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import ReactECharts from 'echarts-for-react'; import type { Transaction } from '../../types'; import { formatCurrency } from '../../utils/format'; +import { toLocalDateString } from '../../utils/dateUtils'; interface SpendingTrendChartProps { transactions: Transaction[]; @@ -20,11 +21,11 @@ export const SpendingTrendChart: React.FC = ({ for (let i = days - 1; i >= 0; i--) { const d = new Date(today); d.setDate(d.getDate() - i); - const dateStr = d.toISOString().split('T')[0]; + const dateStr = toLocalDateString(d); const displayDate = `${d.getMonth() + 1}/${d.getDate()}`; const dailyTotal = transactions - .filter(t => t.type === 'expense' && t.transactionDate.startsWith(dateStr)) + .filter(t => t.type === 'expense' && toLocalDateString(t.transactionDate) === dateStr) .reduce((sum, t) => sum + Math.abs(t.amount), 0); data.push({ diff --git a/src/components/transaction/TransactionCalendar/TransactionCalendar.tsx b/src/components/transaction/TransactionCalendar/TransactionCalendar.tsx index ce0c8f4..6ae61c1 100644 --- a/src/components/transaction/TransactionCalendar/TransactionCalendar.tsx +++ b/src/components/transaction/TransactionCalendar/TransactionCalendar.tsx @@ -7,6 +7,7 @@ import React, { useMemo } from 'react'; import { Icon } from '@iconify/react'; import type { Transaction } from '../../../types'; import { formatCurrency } from '../../../utils/format'; +import { toLocalDateString } from '../../../utils/dateUtils'; import './TransactionCalendar.css'; interface TransactionCalendarProps { @@ -83,7 +84,7 @@ export const TransactionCalendar: React.FC = ({ transactions.forEach(t => { // Handle timezone issues by taking date part only - const dateStr = t.transactionDate.split('T')[0]; // Assuming ISO string + const dateStr = toLocalDateString(t.transactionDate); if (!stats.has(dateStr)) { stats.set(dateStr, { income: 0, expense: 0, transactions: [] }); } diff --git a/src/components/transaction/TransactionFilter/TransactionFilter.tsx b/src/components/transaction/TransactionFilter/TransactionFilter.tsx index baac53e..47c2291 100644 --- a/src/components/transaction/TransactionFilter/TransactionFilter.tsx +++ b/src/components/transaction/TransactionFilter/TransactionFilter.tsx @@ -10,6 +10,7 @@ import type { Category, Account, TransactionType } from '../../../types'; import { getCategories } from '../../../services/categoryService'; import { getAccounts } from '../../../services/accountService'; import { getDisplayIcon } from '../../../utils/iconUtils'; +import { toLocalDateString } from '../../../utils/dateUtils'; import './TransactionFilter.css'; export interface FilterValues { @@ -58,35 +59,37 @@ const DATE_PRESETS = [ * Get date range based on preset */ function getDateRange(preset: 'today' | 'week' | 'month' | 'year'): { + startDate: string; endDate: string; } { const now = new Date(); - const today = now.toISOString().split('T')[0]; + const today = toLocalDateString(now); switch (preset) { case 'today': return { startDate: today, endDate: today }; case 'week': { - const dayOfWeek = now.getDay(); + const dayOfWeek = now.getDay() || 7; // Monday = 1, Sunday = 7 const startOfWeek = new Date(now); - startOfWeek.setDate(now.getDate() - dayOfWeek); + // Adjust to Monday of current week + startOfWeek.setDate(now.getDate() - dayOfWeek + 1); return { - startDate: startOfWeek.toISOString().split('T')[0], + startDate: toLocalDateString(startOfWeek), endDate: today, }; } case 'month': { const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); return { - startDate: startOfMonth.toISOString().split('T')[0], + startDate: toLocalDateString(startOfMonth), endDate: today, }; } case 'year': { const startOfYear = new Date(now.getFullYear(), 0, 1); return { - startDate: startOfYear.toISOString().split('T')[0], + startDate: toLocalDateString(startOfYear), endDate: today, }; } diff --git a/src/components/transaction/TransactionList/TransactionList.tsx b/src/components/transaction/TransactionList/TransactionList.tsx index 75f9c16..a711cdf 100644 --- a/src/components/transaction/TransactionList/TransactionList.tsx +++ b/src/components/transaction/TransactionList/TransactionList.tsx @@ -43,22 +43,30 @@ interface TransactionListProps { * Format date header for grouping */ function formatDateHeader(dateString: string): string { + // dateString is YYYY-MM-DD (local) from grouping logic const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + const todayStr = `${year}-${month}-${day}`; + const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); + const yYear = yesterday.getFullYear(); + const yMonth = String(yesterday.getMonth() + 1).padStart(2, '0'); + const yDay = String(yesterday.getDate()).padStart(2, '0'); + const yesterdayStr = `${yYear}-${yMonth}-${yDay}`; - const dateOnly = dateString.split('T')[0]; - const todayOnly = today.toISOString().split('T')[0]; - const yesterdayOnly = yesterday.toISOString().split('T')[0]; - - if (dateOnly === todayOnly) { + if (dateString === todayStr) { return '今天'; } - if (dateOnly === yesterdayOnly) { + if (dateString === yesterdayStr) { return '昨天'; } - return formatDate(dateString, { + // Construct a date object that represents this local date specifically for formatting + // Using T00:00:00 to ensure it's treated as local time, not UTC + return formatDate(dateString + 'T00:00:00', { year: 'numeric', month: 'long', day: 'numeric', diff --git a/src/pages/Budget/Budget.tsx b/src/pages/Budget/Budget.tsx index 56fde8a..bbbd86b 100644 --- a/src/pages/Budget/Budget.tsx +++ b/src/pages/Budget/Budget.tsx @@ -33,6 +33,7 @@ import { createTransaction } from '../../services/transactionService'; import { TransactionForm } from '../../components/transaction'; import { getCategories } from '../../services/categoryService'; import { getAccounts } from '../../services/accountService'; +import { toLocalDateString } from '../../utils/dateUtils'; import { Icon } from '@iconify/react'; import './Budget.css'; @@ -305,7 +306,7 @@ function Budget() { type: 'expense', categoryId: budget.categoryId || 0, accountId: budget.accountId || 0, - transactionDate: new Date().toISOString().split('T')[0], + transactionDate: toLocalDateString(new Date()), amount: 0 }); setShowTransactionForm(true); diff --git a/src/pages/ExchangeRates/ExchangeRates.tsx b/src/pages/ExchangeRates/ExchangeRates.tsx index d401650..13e7207 100644 --- a/src/pages/ExchangeRates/ExchangeRates.tsx +++ b/src/pages/ExchangeRates/ExchangeRates.tsx @@ -17,6 +17,7 @@ import { } from '../../services/exchangeRateService'; import { getAccounts, calculateTotalBalance } from '../../services/accountService'; import { getTransactions } from '../../services/transactionService'; +import { toLocalDateString } from '../../utils/dateUtils'; import { ExchangeRateCard } from '../../components/exchangeRate/ExchangeRateCard'; import NetWorthCard from '../../components/exchangeRate/NetWorthCard/NetWorthCard'; import { SyncStatusBar } from '../../components/exchangeRate/SyncStatusBar'; @@ -107,8 +108,8 @@ const ExchangeRates: React.FC = () => { startDate.setDate(startDate.getDate() - 30); const { items: transactions } = await getTransactions({ - startDate: startDate.toISOString().split('T')[0], - endDate: endDate.toISOString().split('T')[0], + startDate: toLocalDateString(startDate), + endDate: toLocalDateString(endDate), pageSize: 1000 // Ensure we get all relevant transactions }); @@ -122,7 +123,7 @@ const ExchangeRates: React.FC = () => { for (let i = 0; i < 30; i++) { const date = new Date(); date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD + const dateStr = toLocalDateString(date); // YYYY-MM-DD // Push current day's End-of-Day balance // Note: Array is being built backwards (Today ... 30 days ago) diff --git a/src/services/transactionService.ts b/src/services/transactionService.ts index bcd8d0a..30250fa 100644 --- a/src/services/transactionService.ts +++ b/src/services/transactionService.ts @@ -178,7 +178,7 @@ export async function updateTransaction( if (data.transactionDate !== undefined) payload.transaction_date = data.transactionDate; if (data.note !== undefined) payload.note = data.note; if (data.tagIds !== undefined) payload.tag_ids = data.tagIds; - + const response = await api.put>(`/transactions/${id}`, payload); if (!response.data) { throw new Error(response.error || 'Failed to update transaction'); @@ -209,7 +209,14 @@ export function groupTransactionsByDate(transactions: Transaction[] | undefined) if (!dateValue) { continue; // Skip transactions without a date } - const date = dateValue.split('T')[0]; + + // Fix: Use local time for grouping instead of UTC string split + const d = new Date(dateValue); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const date = `${year}-${month}-${day}`; + const existing = groups.get(date) || []; existing.push(transaction); groups.set(date, existing); diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts new file mode 100644 index 0000000..c2e5602 --- /dev/null +++ b/src/utils/dateUtils.ts @@ -0,0 +1,80 @@ +/** + * Date Utilities + * Handles local date formatting and manipulation to avoid UTC timezone issues. + */ + +/** + * Formats a date object to 'YYYY-MM-DD' string using local time. + * Replaces: date.toISOString().split('T')[0] + */ +export function toLocalDateString(date: Date | string | number): string { + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +/** + * Checks if two dates refer to the same day in local time. + */ +export function isSameDay(date1: Date | string, date2: Date | string): boolean { + const d1 = new Date(date1); + const d2 = new Date(date2); + return ( + d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate() + ); +} + +/** + * Returns a new Date object set to the start of the day (00:00:00.000) in local time. + */ +export function getStartOfDay(date: Date = new Date()): Date { + const d = new Date(date); + d.setHours(0, 0, 0, 0); + return d; +} + +/** + * Returns a new Date object set to the end of the day (23:59:59.999) in local time. + */ +export function getEndOfDay(date: Date = new Date()): Date { + const d = new Date(date); + d.setHours(23, 59, 59, 999); + return d; +} + +/** + * Calculates date range helpers + */ +export const DateRanges = { + today: () => { + const start = getStartOfDay(); + const end = getEndOfDay(); + return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) }; + }, + yesterday: () => { + const start = getStartOfDay(); + start.setDate(start.getDate() - 1); + const end = getEndOfDay(start); + return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) }; + }, + thisWeek: () => { + const now = new Date(); + const day = now.getDay() || 7; // Sunday is 0, make it 7 for ISO week (Mon-Sun)? Or standard week. Let's assume Mon start. + const start = getStartOfDay(); + if (day !== 1) { + start.setDate(now.getDate() - day + 1 + (day === 0 ? -7 : 0)); // Move to Monday + } + const end = getEndOfDay(); + return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) }; + }, + thisMonth: () => { + const now = new Date(); + const start = getStartOfDay(new Date(now.getFullYear(), now.getMonth(), 1)); + const end = getEndOfDay(new Date(now.getFullYear(), now.getMonth() + 1, 0)); + return { start, end, startStr: toLocalDateString(start), endStr: toLocalDateString(end) }; + } +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index cef78a1..b2dbabb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,4 +3,4 @@ */ export { formatCurrency, formatDate, formatRelativeTime, formatPercentage } from './format'; - +export * from './dateUtils';