@@ -7,10 +7,9 @@ import { useGetUsersUsage, useGetUsage, Period, UserUsageStatsList, NodeUsageSta
77import { useMemo , useState , useEffect } from 'react'
88import { SearchXIcon , TrendingUp , TrendingDown } from 'lucide-react'
99import { Select , SelectTrigger , SelectValue , SelectContent , SelectItem } from '../ui/select'
10- import { dateUtils } from '@/utils/dateFormatter'
11- import dayjs from '@/lib/dayjs'
1210import { useAdmin } from '@/hooks/use-admin'
1311import useDirDetection from '@/hooks/use-dir-detection'
12+ import { formatPeriodLabelForPeriod , formatTooltipDate , getChartQueryRangeFromShortcut , getXAxisIntervalForShortcut } from '@/utils/chart-period-utils'
1413
1514type 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))' } />
0 commit comments