@@ -36,7 +36,8 @@ import {
3636 type UserResponse ,
3737} from '@/service/api'
3838import { dateUtils , useRelativeExpiryDate } from '@/utils/dateFormatter'
39- import { formatOffsetDateTime , parseDateInput , toDisplayDate , toUnixSeconds } from '@/utils/dateTimeParsing'
39+ import { normalizeDatePickerValueForSubmit , serializeDatePickerValue , toDatePickerDisplayDate } from '@/utils/datePickerUtils'
40+ import { parseDateInput } from '@/utils/dateTimeParsing'
4041import { bytesToFormGigabytes , formatBytes , gbToBytes } from '@/utils/formatByte'
4142import { invalidateUserMetricsQueries , upsertUserInUsersCache } from '@/utils/usersCache'
4243import { generateWireGuardKeyPair , getWireGuardPublicKey } from '@/utils/wireguard'
@@ -59,8 +60,6 @@ interface UserModalProps {
5960 onSuccessCallback ?: ( user : UserResponse ) => void
6061}
6162
62- const isDate = ( v : unknown ) : v is Date => typeof v === 'object' && v !== null && v instanceof Date
63-
6463// Add template validation schema
6564const templateUserSchema = z . object ( {
6665 username : z . string ( ) . min ( 3 , 'validation.minLength' ) . max ( 128 , 'validation.maxLength' ) ,
@@ -104,8 +103,7 @@ const ExpiryDateField = ({
104103 const handleDateChange = React . useCallback (
105104 ( date : Date | undefined ) => {
106105 if ( date ) {
107- // Use the same logic as centralized DatePicker
108- const value = useUtcTimestamp ? toUnixSeconds ( date ) : formatOffsetDateTime ( date )
106+ const value = serializeDatePickerValue ( date , { useUtcTimestamp } )
109107 field . onChange ( value )
110108 handleFieldChange ( fieldName , value )
111109 } else {
@@ -395,6 +393,7 @@ function UserModal({ isDialogOpen, onOpenChange, form, editingUser, editingUserI
395393 const onHoldExpireDurationInputRef = React . useRef < string > ( '' )
396394 const nextPlanExpireInputRef = React . useRef < string > ( '' )
397395 const nextPlanDataLimitInputRef = React . useRef < string > ( '' )
396+ const previousStatusRef = React . useRef ( status )
398397
399398 const handleModalOpenChange = React . useCallback (
400399 ( open : boolean ) => {
@@ -459,42 +458,8 @@ function UserModal({ isDialogOpen, onOpenChange, form, editingUser, editingUserI
459458 const onHoldValue = form . watch ( 'on_hold_timeout' )
460459 const dataLimitValue = form . watch ( 'data_limit' )
461460
462- let displayDate : Date | null = null
463- let onHoldDisplayDate : Date | null = null
464-
465- // Handle various formats of expire value using the same logic as OnlineBadge/OnlineStatus
466- const parseDateValue = ( value : unknown ) : Date | null => {
467- if ( isDate ( value ) ) {
468- return value
469- } else if ( typeof value === 'string' ) {
470- if ( value === '' ) {
471- return null
472- } else {
473- try {
474- const trimmedValue = value . trim ( )
475- const dayjsDate = parseDateInput ( trimmedValue )
476- if ( dayjsDate . isValid ( ) ) {
477- return toDisplayDate ( trimmedValue )
478- }
479- } catch ( error ) {
480- // Ignore invalid values and return null.
481- }
482- }
483- } else if ( typeof value === 'number' ) {
484- try {
485- const dayjsDate = parseDateInput ( value )
486- if ( dayjsDate . isValid ( ) ) {
487- return toDisplayDate ( value )
488- }
489- } catch ( error ) {
490- // Ignore invalid values and return null.
491- }
492- }
493- return null
494- }
495-
496- displayDate = parseDateValue ( expireValue )
497- onHoldDisplayDate = parseDateValue ( onHoldValue )
461+ const displayDate = toDatePickerDisplayDate ( expireValue )
462+ const onHoldDisplayDate = toDatePickerDisplayDate ( onHoldValue )
498463
499464 // Query client for data refetching
500465 const queryClient = useQueryClient ( )
@@ -633,16 +598,25 @@ function UserModal({ isDialogOpen, onOpenChange, form, editingUser, editingUserI
633598 } , [ selectedTemplateId , form , t ] )
634599
635600 useEffect ( ( ) => {
601+ const previousStatus = previousStatusRef . current
602+
636603 if ( status === 'on_hold' ) {
637604 form . setValue ( 'expire' , undefined )
638605 form . clearErrors ( 'expire' )
639606 } else {
607+ if ( previousStatus === 'on_hold' ) {
608+ form . setValue ( 'expire' , undefined )
609+ form . clearErrors ( 'expire' )
610+ setExpireCalendarOpen ( false )
611+ }
640612 onHoldExpireDurationInputRef . current = ''
641613 form . setValue ( 'on_hold_expire_duration' , undefined )
642614 form . clearErrors ( 'on_hold_expire_duration' )
643615 form . setValue ( 'on_hold_timeout' , undefined )
644616 form . clearErrors ( 'on_hold_timeout' )
645617 }
618+
619+ previousStatusRef . current = status
646620 } , [ status , form ] )
647621
648622 useEffect ( ( ) => {
@@ -708,29 +682,6 @@ function UserModal({ isDialogOpen, onOpenChange, form, editingUser, editingUserI
708682 }
709683 } , [ isDialogOpen , editingUser , hasNextPlanData , nextPlanManuallyDisabled , form , nextPlanUserTemplateId , nextPlanExpire , nextPlanDataLimit , nextPlanAddRemainingTraffic , editingUserData ] )
710684
711- // Helper to convert expire field to needed schema using the same logic as other components
712- function normalizeExpire ( expire : Date | string | number | null | undefined , useUtcTimestamp : boolean = false ) : string | number | undefined {
713- if ( expire === '' ) return 0
714- if ( expire === undefined || expire === null ) return undefined
715-
716- // For Date objects, convert to appropriate format
717- if ( expire instanceof Date ) {
718- return useUtcTimestamp ? toUnixSeconds ( expire ) : formatOffsetDateTime ( expire )
719- }
720-
721- // For strings and numbers, normalize via centralized parser.
722- try {
723- const dayjsDate = parseDateInput ( expire )
724- if ( dayjsDate . isValid ( ) ) {
725- return useUtcTimestamp ? toUnixSeconds ( expire ) : formatOffsetDateTime ( expire )
726- }
727- } catch ( error ) {
728- // If dayjs parsing fails, return undefined
729- }
730-
731- return undefined
732- }
733-
734685 // Helper to clear group selection
735686 const clearGroups = ( ) => form . setValue ( 'group_ids' , [ ] )
736687 // Helper to clear template selection
@@ -1017,13 +968,14 @@ function UserModal({ isDialogOpen, onOpenChange, form, editingUser, editingUserI
1017968 const preparedValues = {
1018969 ...valuesWithoutNextPlan ,
1019970 data_limit : typeof values . data_limit === 'string' ? parseFloat ( values . data_limit ) : values . data_limit ,
1020- on_hold_expire_duration : values . on_hold_expire_duration
1021- ? typeof values . on_hold_expire_duration === 'string'
1022- ? parseFloat ( values . on_hold_expire_duration )
1023- : values . on_hold_expire_duration
1024- : undefined ,
1025- expire : status === 'on_hold' ? undefined : normalizeExpire ( values . expire ) ,
1026- on_hold_timeout : status === 'on_hold' ? normalizeExpire ( values . on_hold_timeout ) : undefined ,
971+ on_hold_expire_duration :
972+ status === 'on_hold' && values . on_hold_expire_duration
973+ ? typeof values . on_hold_expire_duration === 'string'
974+ ? parseFloat ( values . on_hold_expire_duration )
975+ : values . on_hold_expire_duration
976+ : undefined ,
977+ expire : status === 'on_hold' ? undefined : normalizeDatePickerValueForSubmit ( values . expire ) ,
978+ on_hold_timeout : status === 'on_hold' ? normalizeDatePickerValueForSubmit ( values . on_hold_timeout ) : undefined ,
1027979 group_ids : Array . isArray ( values . group_ids ) ? values . group_ids : [ ] ,
1028980 status : values . status ,
1029981 }
0 commit comments