Skip to content

Commit ae3d411

Browse files
committed
fix: Replace dateUtils with dateTimeParsing utility for date formatting across components
1 parent 88e50cc commit ae3d411

File tree

12 files changed

+173
-236
lines changed

12 files changed

+173
-236
lines changed

dashboard/src/components/charts/area-costume-chart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Skeleton } from '@/components/ui/skeleton'
99
import { EmptyState } from './empty-state'
1010
import { Button } from '@/components/ui/button'
1111
import { Clock, History, Cpu, MemoryStick } from 'lucide-react'
12-
import { dateUtils } from '@/utils/dateFormatter'
12+
import { formatOffsetDateTime } from '@/utils/dateTimeParsing'
1313
import { useTheme } from 'next-themes'
1414
import {
1515
buildPeriodOptions,
@@ -164,7 +164,7 @@ export function AreaCostumeChart({ nodeId, currentStats, realtimeStats }: AreaCo
164164
time: timeStr,
165165
cpu: cpuUsage,
166166
ram: ramUsage,
167-
_period_start: dateUtils.toSystemTimezoneISO(now),
167+
_period_start: formatOffsetDateTime(now),
168168
},
169169
]
170170

dashboard/src/components/common/date-picker.tsx

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Input } from '@/components/ui/input'
1212
import { useTranslation } from 'react-i18next'
1313
import { Calendar as PersianCalendar } from '@/components/ui/persian-calendar'
1414
import { formatDateByLocale, formatDateShort, isDateDisabled } from '@/utils/datePickerUtils'
15+
import { formatOffsetDateTime, toUnixSeconds } from '@/utils/dateTimeParsing'
1516
import { useIsMobile } from '@/hooks/use-mobile'
1617
import { useTheme } from '@/components/common/theme-provider'
1718
import { DATE_PICKER_PREFERENCE_KEY, getDatePickerPreference, type DatePickerPreference } from '@/utils/userPreferenceStorage'
@@ -110,29 +111,6 @@ export interface DatePickerProps {
110111
side?: DatePickerSide
111112
}
112113

113-
/**
114-
* Helper function to get local ISO time string with timezone offset
115-
*/
116-
const getLocalISOTime = (date: Date): string => {
117-
// Create a properly formatted ISO string with timezone offset
118-
const tzOffset = -date.getTimezoneOffset()
119-
const offsetSign = tzOffset >= 0 ? '+' : '-'
120-
const pad = (num: number) => Math.abs(num).toString().padStart(2, '0')
121-
122-
const offsetHours = pad(Math.floor(Math.abs(tzOffset) / 60))
123-
const offsetMinutes = pad(Math.abs(tzOffset) % 60)
124-
125-
// Get the local date/time components without timezone conversion
126-
const year = date.getFullYear()
127-
const month = pad(date.getMonth() + 1)
128-
const day = pad(date.getDate())
129-
const hours = pad(date.getHours())
130-
const minutes = pad(date.getMinutes())
131-
const seconds = pad(date.getSeconds())
132-
133-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`
134-
}
135-
136114
/**
137115
* Centralized Date Picker Component
138116
* Supports both single date and date range selection modes
@@ -243,7 +221,7 @@ export function DatePicker({
243221
}
244222

245223
setInternalDate(selectedDate)
246-
const value = useUtcTimestamp ? Math.floor(selectedDate.getTime() / 1000) : getLocalISOTime(selectedDate)
224+
const value = useUtcTimestamp ? toUnixSeconds(selectedDate) : formatOffsetDateTime(selectedDate)
247225
onDateChange(selectedDate)
248226
onFieldChange?.(fieldName, value)
249227
setTimeout(() => {
@@ -274,7 +252,7 @@ export function DatePicker({
274252
newDate.setTime(now.getTime())
275253
}
276254

277-
const value = useUtcTimestamp ? Math.floor(newDate.getTime() / 1000) : getLocalISOTime(newDate)
255+
const value = useUtcTimestamp ? toUnixSeconds(newDate) : formatOffsetDateTime(newDate)
278256
setInternalDate(newDate)
279257
onDateChange(newDate)
280258
onFieldChange?.(fieldName, value)

dashboard/src/components/dashboard/data-usage-chart.tsx

Lines changed: 17 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import { useGetUsersUsage, useGetUsage, Period, UserUsageStatsList, NodeUsageSta
77
import { useMemo, useState, useEffect } from 'react'
88
import { SearchXIcon, TrendingUp, TrendingDown } from 'lucide-react'
99
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../ui/select'
10-
import { dateUtils } from '@/utils/dateFormatter'
11-
import dayjs from '@/lib/dayjs'
1210
import { useAdmin } from '@/hooks/use-admin'
1311
import useDirDetection from '@/hooks/use-dir-detection'
12+
import { formatPeriodLabelForPeriod, formatTooltipDate, getChartQueryRangeFromShortcut, getXAxisIntervalForShortcut } from '@/utils/chart-period-utils'
1413

1514
type PeriodOption = {
1615
label: string
@@ -40,48 +39,13 @@ const PERIOD_KEYS = [
4039
{ key: 'all', period: 'day' as Period, allTime: true },
4140
]
4241

43-
const toChartPeriodStart = (periodStart: string | Date) => dateUtils.toSystemTimezoneDayjs(periodStart)
44-
45-
const transformUsageData = (apiData: { stats: (UserUsageStat | NodeUsageStat)[] }, periodOption: PeriodOption, isNodeUsage: boolean = false, locale: string = 'en') => {
42+
const transformUsageData = (apiData: { stats: (UserUsageStat | NodeUsageStat)[] }, period: Period, isNodeUsage: boolean = false, locale: string = 'en') => {
4643
if (!apiData?.stats || !Array.isArray(apiData.stats)) {
4744
return []
4845
}
4946

5047
return apiData.stats.map((stat: UserUsageStat | NodeUsageStat) => {
51-
const d = toChartPeriodStart(stat.period_start)
52-
53-
let displayLabel = ''
54-
if (periodOption.hours) {
55-
displayLabel = d.format('HH:mm')
56-
} else if (periodOption.period === 'day') {
57-
// Always format day labels from period_start to keep bucket labels stable.
58-
if (locale === 'fa') {
59-
const localDate = new Date(d.year(), d.month(), d.date(), 0, 0, 0)
60-
displayLabel = localDate.toLocaleString('fa-IR', {
61-
month: '2-digit',
62-
day: '2-digit',
63-
})
64-
} else {
65-
const localDate = new Date(d.year(), d.month(), d.date(), 0, 0, 0)
66-
displayLabel = localDate.toLocaleString('en-US', {
67-
month: '2-digit',
68-
day: '2-digit',
69-
})
70-
}
71-
} else {
72-
// For other periods (month, etc.), show date format
73-
if (locale === 'fa') {
74-
displayLabel = d.toDate().toLocaleString('fa-IR', {
75-
month: '2-digit',
76-
day: '2-digit',
77-
})
78-
} else {
79-
displayLabel = d.toDate().toLocaleString('en-US', {
80-
month: '2-digit',
81-
day: '2-digit',
82-
})
83-
}
84-
}
48+
const displayLabel = formatPeriodLabelForPeriod(stat.period_start, period, locale)
8549

8650
const traffic = isNodeUsage ? ((stat as NodeUsageStat).uplink || 0) + ((stat as NodeUsageStat).downlink || 0) : (stat as UserUsageStat).total_traffic || 0
8751

@@ -104,57 +68,7 @@ function CustomBarTooltip({ active, payload, period }: TooltipProps<number, stri
10468
const { t, i18n } = useTranslation()
10569
if (!active || !payload || !payload.length) return null
10670
const data = payload[0].payload
107-
// Use period_start if available (from transformUsageData), otherwise parse the display label
108-
const d = data.period_start ? toChartPeriodStart(data.period_start) : dateUtils.toDayjs(data.date)
109-
110-
let formattedDate
111-
if (i18n.language === 'fa') {
112-
try {
113-
if (period === 'day') {
114-
const localDate = new Date(d.year(), d.month(), d.date(), 0, 0, 0)
115-
formattedDate = localDate.toLocaleDateString('fa-IR', {
116-
year: 'numeric',
117-
month: '2-digit',
118-
day: '2-digit',
119-
})
120-
} else {
121-
formattedDate = d
122-
.toDate()
123-
.toLocaleString('fa-IR', {
124-
year: 'numeric',
125-
month: '2-digit',
126-
day: '2-digit',
127-
hour: '2-digit',
128-
minute: '2-digit',
129-
hour12: false,
130-
})
131-
.replace(',', '')
132-
}
133-
} catch {
134-
formattedDate = period === 'day' ? d.format('YYYY/MM/DD') : d.format('YYYY/MM/DD HH:mm')
135-
}
136-
} else {
137-
if (period === 'day') {
138-
const localDate = new Date(d.year(), d.month(), d.date(), 0, 0, 0)
139-
formattedDate = localDate.toLocaleDateString('en-US', {
140-
year: 'numeric',
141-
month: '2-digit',
142-
day: '2-digit',
143-
})
144-
} else {
145-
formattedDate = d
146-
.toDate()
147-
.toLocaleString('en-US', {
148-
year: 'numeric',
149-
month: '2-digit',
150-
day: '2-digit',
151-
hour: '2-digit',
152-
minute: '2-digit',
153-
hour12: false,
154-
})
155-
.replace(',', '')
156-
}
157-
}
71+
const formattedDate = data.period_start ? formatTooltipDate(data.period_start, period ?? Period.hour, i18n.language) : String(data.date ?? '')
15872

15973
const isRTL = i18n.language === 'fa'
16074

@@ -208,43 +122,28 @@ const DataUsageChart = ({ admin_username }: { admin_username?: string }) => {
208122
})
209123
}, [PERIOD_OPTIONS])
210124

211-
const { startDate, endDate } = useMemo(() => {
212-
const now = dayjs()
213-
let start: dayjs.Dayjs
214-
if (periodOption.allTime) {
215-
start = dayjs('2000-01-01T00:00:00Z')
216-
} else if (periodOption.hours) {
217-
start = now.subtract(periodOption.hours, 'hour')
218-
} else if (periodOption.days) {
219-
const daysToSubtract = periodOption.days === 7 ? 6 : periodOption.days === 3 ? 2 : periodOption.days === 1 ? 0 : periodOption.days
220-
start = now.subtract(daysToSubtract, 'day').startOf('day')
221-
} else if (periodOption.months) {
222-
start = now.subtract(periodOption.months, 'month').startOf('day')
223-
} else {
224-
start = now
225-
}
226-
return { startDate: dateUtils.toSystemTimezoneISO(start.toDate()), endDate: dateUtils.toSystemTimezoneISO(now.toDate()) }
227-
}, [periodOption])
125+
const queryRange = useMemo(() => getChartQueryRangeFromShortcut(periodOption.value, new Date(), { minuteForOneHour: true }), [periodOption.value])
126+
const activePeriod = queryRange.period
228127

229128
const shouldUseNodeUsage = is_sudo && !admin_username
230129

231130
const nodeUsageParams = useMemo(
232131
() => ({
233-
period: periodOption.period,
234-
start: startDate,
235-
end: dateUtils.toSystemTimezoneISO(dateUtils.toSystemTimezoneDayjs(endDate).endOf('day').toDate()),
132+
period: activePeriod,
133+
start: queryRange.startDate,
134+
end: queryRange.endDate,
236135
}),
237-
[periodOption.period, startDate, endDate],
136+
[activePeriod, queryRange.startDate, queryRange.endDate],
238137
)
239138

240139
const userUsageParams = useMemo(
241140
() => ({
242141
...(admin_username ? { admin: [admin_username] } : {}),
243-
period: periodOption.period,
244-
start: startDate,
245-
end: dateUtils.toSystemTimezoneISO(dateUtils.toSystemTimezoneDayjs(endDate).endOf('day').toDate()),
142+
period: activePeriod,
143+
start: queryRange.startDate,
144+
end: queryRange.endDate,
246145
}),
247-
[admin_username, periodOption.period, startDate, endDate],
146+
[admin_username, activePeriod, queryRange.startDate, queryRange.endDate],
248147
)
249148

250149
const { data: nodeData, isLoading: isLoadingNodes } = useGetUsage(nodeUsageParams, {
@@ -274,7 +173,7 @@ const DataUsageChart = ({ admin_username }: { admin_username?: string }) => {
274173
}
275174
}
276175

277-
const chartData = useMemo(() => transformUsageData({ stats: statsArr }, periodOption, shouldUseNodeUsage, i18n.language), [statsArr, periodOption, shouldUseNodeUsage, i18n.language])
176+
const chartData = useMemo(() => transformUsageData({ stats: statsArr }, activePeriod, shouldUseNodeUsage, i18n.language), [statsArr, activePeriod, shouldUseNodeUsage, i18n.language])
278177

279178
const trend = useMemo(() => {
280179
if (!chartData || chartData.length < 2) return null
@@ -295,25 +194,7 @@ const DataUsageChart = ({ admin_username }: { admin_username?: string }) => {
295194
return formatBytes(totalBytes, 2)
296195
}, [chartData])
297196

298-
const xAxisInterval = useMemo(() => {
299-
// For hours (24h), show approximately 8 labels
300-
if (periodOption.hours) {
301-
const targetLabels = 8
302-
return Math.max(1, Math.floor(chartData.length / targetLabels))
303-
}
304-
305-
if (periodOption.months || periodOption.allTime) {
306-
const targetLabels = 5
307-
return Math.max(1, Math.floor(chartData.length / targetLabels))
308-
}
309-
310-
if (periodOption.days && periodOption.days > 7) {
311-
const targetLabels = periodOption.days === 30 ? 10 : 8
312-
return Math.max(1, Math.floor(chartData.length / targetLabels))
313-
}
314-
315-
return 0
316-
}, [periodOption.hours, periodOption.months, periodOption.allTime, periodOption.days, chartData.length])
197+
const xAxisInterval = useMemo(() => getXAxisIntervalForShortcut(periodOption.value, chartData.length, { minuteForOneHour: true }), [periodOption.value, chartData.length])
317198

318199
return (
319200
<Card className="flex h-full flex-col justify-between overflow-hidden">
@@ -399,7 +280,7 @@ const DataUsageChart = ({ admin_username }: { admin_username?: string }) => {
399280
tickFormatter={(value: string): string => value || ''}
400281
/>
401282
<YAxis dataKey={'traffic'} tickLine={false} tickMargin={4} axisLine={false} width={40} tickFormatter={val => formatBytes(val, 0, true).toString()} tick={{ fontSize: 10 }} />
402-
<ChartTooltip cursor={false} content={<CustomBarTooltip period={periodOption.period} />} />
283+
<ChartTooltip cursor={false} content={<CustomBarTooltip period={activePeriod} />} />
403284
<Bar dataKey="traffic" radius={6} maxBarSize={48}>
404285
{chartData.map((_, index: number) => (
405286
<Cell key={`cell-${index}`} fill={index === activeIndex ? 'hsl(var(--muted-foreground))' : 'hsl(var(--primary))'} />

dashboard/src/components/dialogs/user-modal.tsx

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
type UserResponse,
2727
type UsersResponse,
2828
} from '@/service/api'
29+
import { formatOffsetDateTime, parseDateInput, toDisplayDate, toUnixSeconds } from '@/utils/dateTimeParsing'
2930
import { dateUtils, useRelativeExpiryDate } from '@/utils/dateFormatter'
3031
import { formatBytes, gbToBytes } from '@/utils/formatByte'
3132
import { useQuery, useQueryClient } from '@tanstack/react-query'
@@ -61,29 +62,6 @@ const templateModifySchema = z.object({
6162
user_template_id: z.number(),
6263
})
6364

64-
65-
// Helper function to get local ISO time string with timezone offset
66-
// This is kept for backward compatibility with normalizeExpire function
67-
function getLocalISOTime(date: Date): string {
68-
// Create a properly formatted ISO string with timezone offset
69-
const tzOffset = -date.getTimezoneOffset()
70-
const offsetSign = tzOffset >= 0 ? '+' : '-'
71-
const pad = (num: number) => Math.abs(num).toString().padStart(2, '0')
72-
73-
const offsetHours = pad(Math.floor(Math.abs(tzOffset) / 60))
74-
const offsetMinutes = pad(Math.abs(tzOffset) % 60)
75-
76-
// Get the local date/time components without timezone conversion
77-
const year = date.getFullYear()
78-
const month = pad(date.getMonth() + 1)
79-
const day = pad(date.getDate())
80-
const hours = pad(date.getHours())
81-
const minutes = pad(date.getMinutes())
82-
const seconds = pad(date.getSeconds())
83-
84-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`
85-
}
86-
8765
// Add this new component before the UserModal component
8866
const ExpiryDateField = ({
8967
field,
@@ -112,7 +90,7 @@ const ExpiryDateField = ({
11290
(date: Date | undefined) => {
11391
if (date) {
11492
// Use the same logic as centralized DatePicker
115-
const value = useUtcTimestamp ? Math.floor(date.getTime() / 1000) : getLocalISOTime(date)
93+
const value = useUtcTimestamp ? toUnixSeconds(date) : formatOffsetDateTime(date)
11694
field.onChange(value)
11795
handleFieldChange(fieldName, value)
11896
} else {
@@ -462,24 +440,24 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
462440
if (value === '') {
463441
return null
464442
} else {
465-
// Use the same dateUtils.toDayjs logic as other components
466443
try {
467-
const dayjsDate = dateUtils.toDayjs(value.trim())
444+
const trimmedValue = value.trim()
445+
const dayjsDate = parseDateInput(trimmedValue)
468446
if (dayjsDate.isValid()) {
469-
return dayjsDate.toDate()
447+
return toDisplayDate(trimmedValue)
470448
}
471449
} catch (error) {
472-
// If dayjs parsing fails, return null
450+
// Ignore invalid values and return null.
473451
}
474452
}
475453
} else if (typeof value === 'number') {
476454
try {
477-
const dayjsDate = dateUtils.toDayjs(value)
455+
const dayjsDate = parseDateInput(value)
478456
if (dayjsDate.isValid()) {
479-
return dayjsDate.toDate()
457+
return toDisplayDate(value)
480458
}
481459
} catch (error) {
482-
// If dayjs parsing fails, return null
460+
// Ignore invalid values and return null.
483461
}
484462
}
485463
return null
@@ -730,14 +708,14 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
730708

731709
// For Date objects, convert to appropriate format
732710
if (expire instanceof Date) {
733-
return useUtcTimestamp ? Math.floor(expire.getTime() / 1000) : getLocalISOTime(expire)
711+
return useUtcTimestamp ? toUnixSeconds(expire) : formatOffsetDateTime(expire)
734712
}
735713

736-
// For strings and numbers, use the same dateUtils logic as other components
714+
// For strings and numbers, normalize via centralized parser.
737715
try {
738-
const dayjsDate = dateUtils.toDayjs(expire)
716+
const dayjsDate = parseDateInput(expire)
739717
if (dayjsDate.isValid()) {
740-
return useUtcTimestamp ? Math.floor(dayjsDate.toDate().getTime() / 1000) : getLocalISOTime(dayjsDate.toDate())
718+
return useUtcTimestamp ? toUnixSeconds(expire) : formatOffsetDateTime(expire)
741719
}
742720
} catch (error) {
743721
// If dayjs parsing fails, return undefined

0 commit comments

Comments
 (0)