@@ -216,62 +216,65 @@ const ExpiryDateField = ({
216216 [ now ] ,
217217 )
218218
219+ const dir = useDirDetection ( )
220+
219221 return (
220222 < FormItem className = "flex flex-1 flex-col" >
221223 < FormLabel > { label } </ FormLabel >
222- < Popover open = { calendarOpen } onOpenChange = { setCalendarOpen } >
223- < PopoverTrigger asChild >
224- < FormControl >
225- < div className = "relative w-full" >
226- < Button
227- dir = { 'ltr' }
228- variant = { 'outline' }
229- className = { cn ( '!mt-3.5 h-fit w-full text-left font-normal' , ! field . value && 'text-muted-foreground' ) }
230- type = "button"
231- onClick = { e => {
232- e . preventDefault ( )
233- e . stopPropagation ( )
234- setCalendarOpen ( true )
235- } }
236- >
237- { displayDate ? (
238- usePersianCalendar ? (
239- // Persian format - display in local time
240- displayDate . toLocaleDateString ( 'fa-IR' , {
241- year : 'numeric' ,
242- month : '2-digit' ,
243- day : '2-digit' ,
244- } ) +
245- ' ' +
246- displayDate . toLocaleTimeString ( 'fa-IR' , {
247- hour : '2-digit' ,
248- minute : '2-digit' ,
249- hour12 : false ,
250- } )
224+ < div className = "relative" >
225+ < Popover open = { calendarOpen } onOpenChange = { setCalendarOpen } >
226+ < PopoverTrigger asChild >
227+ < FormControl >
228+ < div className = "relative w-full" >
229+ < Button
230+ dir = { 'ltr' }
231+ variant = { 'outline' }
232+ className = { cn ( 'mt-1 h-fit w-full text-left font-normal' , ! field . value && 'text-muted-foreground' ) }
233+ type = "button"
234+ onClick = { e => {
235+ e . preventDefault ( )
236+ e . stopPropagation ( )
237+ setCalendarOpen ( true )
238+ } }
239+ >
240+ { displayDate ? (
241+ usePersianCalendar ? (
242+ // Persian format - display in local time
243+ displayDate . toLocaleDateString ( 'fa-IR' , {
244+ year : 'numeric' ,
245+ month : '2-digit' ,
246+ day : '2-digit' ,
247+ } ) +
248+ ' ' +
249+ displayDate . toLocaleTimeString ( 'fa-IR' , {
250+ hour : '2-digit' ,
251+ minute : '2-digit' ,
252+ hour12 : false ,
253+ } )
254+ ) : (
255+ // Gregorian format - display in local time
256+ displayDate . toLocaleDateString ( 'sv-SE' , {
257+ year : 'numeric' ,
258+ month : '2-digit' ,
259+ day : '2-digit' ,
260+ } ) +
261+ ' ' +
262+ displayDate . toLocaleTimeString ( 'sv-SE' , {
263+ hour : '2-digit' ,
264+ minute : '2-digit' ,
265+ hour12 : false ,
266+ } )
267+ )
268+ ) : field . value && ! isNaN ( Number ( field . value ) ) ? (
269+ String ( field . value )
251270 ) : (
252- // Gregorian format - display in local time
253- displayDate . toLocaleDateString ( 'sv-SE' , {
254- year : 'numeric' ,
255- month : '2-digit' ,
256- day : '2-digit' ,
257- } ) +
258- ' ' +
259- displayDate . toLocaleTimeString ( 'sv-SE' , {
260- hour : '2-digit' ,
261- minute : '2-digit' ,
262- hour12 : false ,
263- } )
264- )
265- ) : field . value && ! isNaN ( Number ( field . value ) ) ? (
266- String ( field . value )
267- ) : (
268- < span > { t ( 'userDialog.expireDate' , { defaultValue : 'Expire date' } ) } </ span >
269- ) }
270- < CalendarIcon className = "ml-auto h-4 w-4 opacity-50" />
271- </ Button >
272- </ div >
273- </ FormControl >
274- </ PopoverTrigger >
271+ < span > { t ( 'userDialog.expireDate' , { defaultValue : 'Expire date' } ) } </ span >
272+ ) }
273+ < CalendarIcon className = "ml-auto h-4 w-4 opacity-50" />
274+ </ Button >
275+ </ div >
276+ </ FormControl >
277+ </ PopoverTrigger >
275278 < PopoverContent
276279 className = "w-auto p-0"
277280 align = "start"
@@ -358,14 +361,15 @@ const ExpiryDateField = ({
358361 ) }
359362 </ div >
360363 </ PopoverContent >
361- </ Popover >
362- { expireInfo && (
363- < p className = { cn ( ! expireInfo . time && 'hidden' , 'text-xs text-muted-foreground' ) } >
364- { expireInfo . time !== '0' && expireInfo . time !== '0s'
365- ? t ( 'expires' , { time : expireInfo . time , defaultValue : 'Expires in {{time}}' } )
366- : t ( 'expired' , { time : expireInfo . time , defaultValue : 'Expired in {{time}}' } ) }
367- </ p >
368- ) }
364+ </ Popover >
365+ { expireInfo && (
366+ < p className = { cn ( "absolute top-full mt-1 text-xs text-muted-foreground whitespace-nowrap" , ! expireInfo . time && 'hidden' , dir === "rtl" ? 'right-0' : 'left-0' ) } >
367+ { expireInfo . time !== '0' && expireInfo . time !== '0s'
368+ ? t ( 'expires' , { time : expireInfo . time , defaultValue : 'Expires in {{time}}' } )
369+ : t ( 'expired' , { time : expireInfo . time , defaultValue : 'Expired in {{time}}' } ) }
370+ </ p >
371+ ) }
372+ </ div >
369373 < FormMessage />
370374 </ FormItem >
371375 )
@@ -476,7 +480,10 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
476480 { id : 'groups' , label : 'groups' , icon : Users } ,
477481 { id : 'templates' , label : 'templates.title' , icon : Layers } ,
478482 ]
479- const [ nextPlanEnabled , setNextPlanEnabled ] = useState ( ! ! form . watch ( 'next_plan' ) )
483+ const [ nextPlanEnabled , setNextPlanEnabled ] = useState ( ( ) => {
484+ const nextPlan = form . watch ( 'next_plan' )
485+ return nextPlan !== undefined && nextPlan !== null && Object . keys ( nextPlan ) . length > 0
486+ } )
480487 const [ selectedTemplateId , setSelectedTemplateId ] = useState < number | null > ( null )
481488 const [ expireCalendarOpen , setExpireCalendarOpen ] = useState ( false )
482489 const [ onHoldCalendarOpen , setOnHoldCalendarOpen ] = useState ( false )
@@ -712,12 +719,23 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
712719 useEffect ( ( ) => {
713720 if ( ! nextPlanEnabled ) {
714721 form . setValue ( 'next_plan' , undefined )
715- } else if ( ! form . watch ( 'next_plan' ) ) {
722+ handleFieldChange ( 'next_plan' , undefined )
723+ } else if ( ! form . watch ( 'next_plan' ) || form . watch ( 'next_plan' ) === null ) {
716724 form . setValue ( 'next_plan' , { } )
725+ handleFieldChange ( 'next_plan' , { } )
717726 }
718727 // eslint-disable-next-line
719728 } , [ nextPlanEnabled ] )
720729
730+ // Sync switch state when next_plan value changes (e.g., when editing a user)
731+ useEffect ( ( ) => {
732+ const nextPlan = form . watch ( 'next_plan' )
733+ const shouldBeEnabled = nextPlan !== undefined && nextPlan !== null && Object . keys ( nextPlan ) . length > 0
734+ if ( nextPlanEnabled !== shouldBeEnabled ) {
735+ setNextPlanEnabled ( shouldBeEnabled )
736+ }
737+ } , [ form . watch ( 'next_plan' ) , nextPlanEnabled ] )
738+
721739 // Helper to convert GB to bytes
722740 function gbToBytes ( gb : string | number | undefined ) : number | undefined {
723741 if ( gb === undefined || gb === null || gb === '' ) return undefined
@@ -772,6 +790,18 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
772790 return true
773791 }
774792
793+ // Special case for Next Plan enabled - if Next Plan is enabled and no other fields are touched,
794+ // consider the form valid (Next Plan fields are optional)
795+ if ( nextPlanEnabled && editingUser && ! isSubmit ) {
796+ const hasTouchedNonNextPlanFields = Object . keys ( touchedFields ) . some ( key =>
797+ key !== 'next_plan' && ! key . startsWith ( 'next_plan.' ) && touchedFields [ key ]
798+ )
799+ if ( ! hasTouchedNonNextPlanFields ) {
800+ form . clearErrors ( )
801+ return true
802+ }
803+ }
804+
775805 // Only validate fields that have been touched
776806 const fieldsToValidate = isSubmit
777807 ? currentValues
@@ -976,6 +1006,8 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
9761006 expire : preparedValues . expire ,
9771007 // Only include proxy_settings if they are filled
9781008 ...( hasProxySettings ? { proxy_settings : cleanedProxySettings } : { } ) ,
1009+ // Force send undefined when Next Plan is disabled
1010+ next_plan : nextPlanEnabled ? preparedValues . next_plan : undefined ,
9791011 }
9801012
9811013 // Remove proxy_settings from the payload if it's empty or undefined
@@ -1321,14 +1353,14 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
13211353 </ div >
13221354 { /* Data limit and expire fields - show data_limit only when no template is selected */ }
13231355 { activeTab === 'groups' && (
1324- < div className = "flex w-full flex-col gap-4 lg:flex-row lg:items-start " >
1356+ < div className = "flex w-full flex-col gap-4 lg:flex-row lg:items-end " >
13251357 { ! selectedTemplateId && (
13261358 < >
13271359 < FormField
13281360 control = { form . control }
13291361 name = "data_limit"
13301362 render = { ( { field } ) => (
1331- < FormItem className = "flex-1" >
1363+ < FormItem className = "flex-1 h-full " >
13321364 < FormLabel > { t ( 'userDialog.dataLimit' , { defaultValue : 'Data Limit (GB)' } ) } </ FormLabel >
13331365 < FormControl >
13341366 < Input
@@ -1391,7 +1423,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
13911423 ) }
13921424 </ >
13931425 ) }
1394- < div className = "flex items-start gap-4 lg:w-52" >
1426+ < div className = "flex items-start gap-4 lg:w-52 h-full " >
13951427 { status === 'on_hold' ? (
13961428 < FormField
13971429 control = { form . control }
@@ -1800,7 +1832,13 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
18001832 < ListStart className = "h-4 w-4" />
18011833 < div > { t ( 'userDialog.nextPlanTitle' , { defaultValue : 'Next Plan' } ) } </ div >
18021834 </ div >
1803- < Switch checked = { nextPlanEnabled } onCheckedChange = { setNextPlanEnabled } />
1835+ < Switch checked = { nextPlanEnabled } onCheckedChange = { value => {
1836+ setNextPlanEnabled ( value )
1837+ // Trigger validation when Next Plan toggle changes
1838+ const currentValues = form . getValues ( )
1839+ const isValid = validateAllFields ( currentValues , touchedFields )
1840+ setIsFormValid ( isValid )
1841+ } } />
18041842 </ div >
18051843 { nextPlanEnabled && (
18061844 < div className = "flex flex-col gap-4 py-4" >
@@ -1816,8 +1854,10 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
18161854 onValueChange = { val => {
18171855 if ( val === 'none' || ( field . value && String ( field . value ) === val ) ) {
18181856 field . onChange ( undefined )
1857+ handleFieldChange ( 'next_plan.user_template_id' , undefined )
18191858 } else {
18201859 field . onChange ( Number ( val ) )
1860+ handleFieldChange ( 'next_plan.user_template_id' , Number ( val ) )
18211861 }
18221862 } }
18231863 >
@@ -1858,6 +1898,7 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
18581898 const days = e . target . value ? Number ( e . target . value ) : 0
18591899 const seconds = dateUtils . daysToSeconds ( days )
18601900 field . onChange ( seconds )
1901+ handleFieldChange ( 'next_plan.expire' , seconds )
18611902 } }
18621903 />
18631904 </ FormControl >
@@ -1881,7 +1922,9 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
18811922 onChange = { e => {
18821923 const value = e . target . value ? Number ( e . target . value ) : 0
18831924 // Convert GB to bytes (1 GB = 1024 * 1024 * 1024 bytes)
1884- field . onChange ( value ? value * 1024 * 1024 * 1024 : 0 )
1925+ const bytesValue = value ? value * 1024 * 1024 * 1024 : 0
1926+ field . onChange ( bytesValue )
1927+ handleFieldChange ( 'next_plan.data_limit' , bytesValue )
18851928 } }
18861929 value = { field . value ? Math . round ( field . value / ( 1024 * 1024 * 1024 ) ) : '' }
18871930 />
@@ -1900,7 +1943,10 @@ export default function UserModal({ isDialogOpen, onOpenChange, form, editingUse
19001943 render = { ( { field } ) => (
19011944 < FormItem className = "flex flex-row items-center justify-between w-full" >
19021945 < FormLabel > { t ( 'userDialog.nextPlanAddRemainingTraffic' , { defaultValue : 'Add Remaining Traffic' } ) } </ FormLabel >
1903- < Switch checked = { ! ! field . value } onCheckedChange = { field . onChange } />
1946+ < Switch checked = { ! ! field . value } onCheckedChange = { value => {
1947+ field . onChange ( value )
1948+ handleFieldChange ( 'next_plan.add_remaining_traffic' , value )
1949+ } } />
19041950 < FormMessage />
19051951 </ FormItem >
19061952 ) }
0 commit comments