@@ -278,26 +278,93 @@ export default function ScheduleListModal({ isOpen, onClose }: ScheduleListModal
278278 const now = new Date ( ) ;
279279 const [ hours , minutes ] = schedule . time_of_day . split ( ':' ) . map ( Number ) ;
280280
281- // Helper function to find next occurrence for weekly schedules with specific days
282- const findNextWeeklyOccurrence = ( startDate : Date , daysOfWeek : number [ ] ) : Date => {
283- const result = new Date ( startDate ) ;
284- result . setHours ( hours , minutes , 0 , 0 ) ;
281+ // Helper function to convert schedule time to UTC-aware date
282+ const createScheduledDate = ( localDate : Date ) : Date => {
283+ // Format date for the target timezone
284+ const year = localDate . getFullYear ( ) ;
285+ const month = String ( localDate . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
286+ const day = String ( localDate . getDate ( ) ) . padStart ( 2 , '0' ) ;
287+ const hourStr = String ( hours ) . padStart ( 2 , '0' ) ;
288+ const minuteStr = String ( minutes ) . padStart ( 2 , '0' ) ;
289+
290+ // Create an ISO string for the target timezone time
291+ const dateTimeStr = `${ year } -${ month } -${ day } T${ hourStr } :${ minuteStr } :00` ;
292+
293+ // Use Intl.DateTimeFormat to handle timezone conversion properly
294+ // This simulates the SQL AT TIME ZONE behavior
295+ const formatter = new Intl . DateTimeFormat ( 'en-US' , {
296+ timeZone : schedule . timezone ,
297+ year : 'numeric' ,
298+ month : '2-digit' ,
299+ day : '2-digit' ,
300+ hour : '2-digit' ,
301+ minute : '2-digit' ,
302+ second : '2-digit' ,
303+ hour12 : false
304+ } ) ;
305+
306+ // Parse the target date/time in the schedule's timezone
307+ const testDate = new Date ( dateTimeStr ) ;
308+ const parts = formatter . formatToParts ( testDate ) ;
309+ const dateParts : any = { } ;
310+ parts . forEach ( part => dateParts [ part . type ] = part . value ) ;
285311
286- // If the time has already passed today, start checking from tomorrow
287- if ( result <= now ) {
288- result . setDate ( result . getDate ( ) + 1 ) ;
312+ // Get timezone offset for this specific date/time
313+ const tzFormatter = new Intl . DateTimeFormat ( 'en-US' , {
314+ timeZone : schedule . timezone ,
315+ timeZoneName : 'longOffset'
316+ } ) ;
317+ const tzParts = tzFormatter . formatToParts ( new Date ( dateTimeStr ) ) ;
318+ const offsetStr = tzParts . find ( p => p . type === 'timeZoneName' ) ?. value || 'GMT+00:00' ;
319+ const match = offsetStr . match ( / G M T ( [ + - ] \d { 2 } ) : ( \d { 2 } ) / ) ;
320+ let offsetMinutes = 0 ;
321+ if ( match ) {
322+ const offsetHours = parseInt ( match [ 1 ] ) ;
323+ const offsetMins = parseInt ( match [ 2 ] ) ;
324+ offsetMinutes = offsetHours * 60 + ( offsetHours < 0 ? - offsetMins : offsetMins ) ;
289325 }
290326
291- // Find the next matching day of week (max 7 days to check)
327+ // Create the date in the local timezone and adjust for the schedule's timezone offset
328+ const localTime = new Date ( dateTimeStr ) ;
329+ const utcTime = localTime . getTime ( ) - ( offsetMinutes * 60 * 1000 ) ;
330+ return new Date ( utcTime ) ;
331+ } ;
332+
333+ // Helper function to find next occurrence for weekly schedules with specific days
334+ const findNextWeeklyOccurrence = ( startDate : Date , daysOfWeek : number [ ] ) : Date => {
335+ let checkDate = new Date ( startDate ) ;
336+
337+ // Check up to 7 days ahead
292338 for ( let i = 0 ; i < 7 ; i ++ ) {
293- if ( daysOfWeek . includes ( result . getDay ( ) ) ) {
294- return result ;
339+ // Get day of week in the schedule's timezone
340+ const formatter = new Intl . DateTimeFormat ( 'en-US' , {
341+ timeZone : schedule . timezone ,
342+ weekday : 'long'
343+ } ) ;
344+ const dayName = formatter . format ( checkDate ) ;
345+ const dayMap : { [ key : string ] : number } = {
346+ 'Sunday' : 0 ,
347+ 'Monday' : 1 ,
348+ 'Tuesday' : 2 ,
349+ 'Wednesday' : 3 ,
350+ 'Thursday' : 4 ,
351+ 'Friday' : 5 ,
352+ 'Saturday' : 6
353+ } ;
354+ const currentDay = dayMap [ dayName ] ;
355+
356+ if ( daysOfWeek . includes ( currentDay ) ) {
357+ const scheduledTime = createScheduledDate ( checkDate ) ;
358+ if ( scheduledTime > now ) {
359+ return scheduledTime ;
360+ }
295361 }
296- result . setDate ( result . getDate ( ) + 1 ) ;
362+
363+ checkDate . setDate ( checkDate . getDate ( ) + 1 ) ;
297364 }
298365
299- // Fallback (should never reach here if daysOfWeek is valid)
300- return result ;
366+ // Fallback
367+ return createScheduledDate ( checkDate ) ;
301368 } ;
302369
303370 // If never executed, calculate from current date
@@ -307,69 +374,70 @@ export default function ScheduleListModal({ isOpen, onClose }: ScheduleListModal
307374 return findNextWeeklyOccurrence ( now , schedule . day_of_week ) ;
308375 }
309376
310- // Create a date in the schedule's timezone
311- const nextRun = new Date ( now ) ;
312- nextRun . setHours ( hours , minutes , 0 , 0 ) ;
377+ // Create a date at the scheduled time in the target timezone
378+ let nextRun = createScheduledDate ( now ) ;
313379
314380 // If that time has already passed today, add the interval
315381 if ( nextRun <= now ) {
382+ const nextDate = new Date ( now ) ;
316383 switch ( schedule . interval_unit ) {
317384 case 'days' :
318- nextRun . setDate ( nextRun . getDate ( ) + schedule . interval_value ) ;
385+ nextDate . setDate ( nextDate . getDate ( ) + schedule . interval_value ) ;
319386 break ;
320387 case 'weeks' :
321- nextRun . setDate ( nextRun . getDate ( ) + ( schedule . interval_value * 7 ) ) ;
388+ nextDate . setDate ( nextDate . getDate ( ) + ( schedule . interval_value * 7 ) ) ;
322389 break ;
323390 case 'months' :
324- nextRun . setMonth ( nextRun . getMonth ( ) + schedule . interval_value ) ;
391+ nextDate . setMonth ( nextDate . getMonth ( ) + schedule . interval_value ) ;
325392 break ;
326393 }
394+ nextRun = createScheduledDate ( nextDate ) ;
327395 }
328396
329397 return nextRun ;
330398 }
331399
332400 // Calculate from last execution
333401 const lastRun = new Date ( schedule . last_executed_at ) ;
334- let nextRun = new Date ( lastRun ) ;
402+ let nextDate = new Date ( lastRun ) ;
335403
336404 // For weekly schedules with specific days
337405 if ( schedule . interval_unit === 'weeks' && schedule . day_of_week && schedule . day_of_week . length > 0 ) {
338406 // Start from the day after last execution
339- nextRun . setDate ( nextRun . getDate ( ) + 1 ) ;
340- return findNextWeeklyOccurrence ( nextRun , schedule . day_of_week ) ;
407+ nextDate . setDate ( nextDate . getDate ( ) + 1 ) ;
408+ return findNextWeeklyOccurrence ( nextDate , schedule . day_of_week ) ;
341409 }
342410
343411 // Add the interval for regular schedules
344412 switch ( schedule . interval_unit ) {
345413 case 'days' :
346- nextRun . setDate ( nextRun . getDate ( ) + schedule . interval_value ) ;
414+ nextDate . setDate ( nextDate . getDate ( ) + schedule . interval_value ) ;
347415 break ;
348416 case 'weeks' :
349- nextRun . setDate ( nextRun . getDate ( ) + ( schedule . interval_value * 7 ) ) ;
417+ nextDate . setDate ( nextDate . getDate ( ) + ( schedule . interval_value * 7 ) ) ;
350418 break ;
351419 case 'months' :
352- nextRun . setMonth ( nextRun . getMonth ( ) + schedule . interval_value ) ;
420+ nextDate . setMonth ( nextDate . getMonth ( ) + schedule . interval_value ) ;
353421 break ;
354422 }
355423
356- // Set the proper time
357- nextRun . setHours ( hours , minutes , 0 , 0 ) ;
424+ let nextRun = createScheduledDate ( nextDate ) ;
358425
359426 // IMPORTANT: If the calculated next run is in the past (e.g., schedule was paused),
360427 // advance it to the next valid future time
361428 while ( nextRun <= now ) {
362429 switch ( schedule . interval_unit ) {
363430 case 'days' :
364- nextRun . setDate ( nextRun . getDate ( ) + schedule . interval_value ) ;
431+ nextDate . setDate ( nextDate . getDate ( ) + schedule . interval_value ) ;
365432 break ;
366433 case 'weeks' :
367- nextRun . setDate ( nextRun . getDate ( ) + ( schedule . interval_value * 7 ) ) ;
434+ nextDate . setDate ( nextDate . getDate ( ) + ( schedule . interval_value * 7 ) ) ;
368435 break ;
369436 case 'months' :
370- nextRun . setMonth ( nextRun . getMonth ( ) + schedule . interval_value ) ;
437+ nextDate . setMonth ( nextDate . getMonth ( ) + schedule . interval_value ) ;
371438 break ;
372439 }
440+ nextRun = createScheduledDate ( nextDate ) ;
373441 }
374442
375443 return nextRun ;
0 commit comments