@@ -8,13 +8,14 @@ import moment from 'moment'
88
99import { EnvironmentConfig } from '~/config'
1010
11- import type { BackendResource , ChallengeInfo , ReviewAppContextModel } from '../../models'
11+ import type { BackendPhase , BackendResource , ChallengeInfo , ReviewAppContextModel } from '../../models'
1212import type { WinningDetailDto } from '../../services'
1313import { ChallengeDetailContext , ReviewAppContext } from '../../contexts'
1414import { useRole , useRoleProps } from '../../hooks'
1515import { fetchWinningsByExternalId } from '../../services'
1616import { ProgressBar } from '../ProgressBar'
1717import { SUBMITTER , TABLE_DATE_FORMAT } from '../../../config/index.config'
18+ import { formatDurationDate } from '../../utils'
1819
1920import styles from './ChallengePhaseInfo.module.scss'
2021
@@ -156,6 +157,17 @@ export const ChallengePhaseInfo: FC<Props> = (props: Props) => {
156157 isLoadingPayment ,
157158 isTopgearTask ,
158159 ] )
160+ const phaseDisplayInfo = useMemo (
161+ ( ) => computePhaseDisplayInfo ( props . challengeInfo ) ,
162+ [ props . challengeInfo ] ,
163+ )
164+ const {
165+ phaseEndDateString : displayPhaseEndDateString ,
166+ phaseLabel : displayPhaseLabel ,
167+ timeLeft : displayTimeLeft ,
168+ timeLeftColor : displayTimeLeftColor ,
169+ timeLeftStatus : displayTimeLeftStatus ,
170+ } = phaseDisplayInfo
159171
160172 useEffect ( ( ) => {
161173 const run = async ( ) : Promise < void > => {
@@ -219,7 +231,11 @@ export const ChallengePhaseInfo: FC<Props> = (props: Props) => {
219231 const reviewInProgress = props . reviewInProgress
220232
221233 return [
222- ...createPhaseItems ( variant , data , isTask ) ,
234+ ...createPhaseItems ( {
235+ displayPhaseLabel,
236+ isTask,
237+ variant,
238+ } ) ,
223239 createRolesItem ( myChallengeRoles ) ,
224240 ...createTaskItems ( {
225241 formattedStartDate,
@@ -230,13 +246,17 @@ export const ChallengePhaseInfo: FC<Props> = (props: Props) => {
230246 } ) ,
231247 ...createNonTaskItems ( {
232248 data,
249+ displayPhaseEndDateString,
233250 formattedNonTaskPayment,
234251 isTask,
235252 variant,
236253 walletUrl,
237254 } ) ,
238255 ...createActiveItems ( {
239256 data,
257+ displayTimeLeft,
258+ displayTimeLeftColor,
259+ displayTimeLeftStatus,
240260 isTask,
241261 progressType : PROGRESS_TYPE ,
242262 reviewInProgress,
@@ -251,6 +271,11 @@ export const ChallengePhaseInfo: FC<Props> = (props: Props) => {
251271 isTask ,
252272 myChallengeRoles ,
253273 props . challengeInfo ,
274+ displayPhaseLabel ,
275+ displayPhaseEndDateString ,
276+ displayTimeLeft ,
277+ displayTimeLeftColor ,
278+ displayTimeLeftStatus ,
254279 props . reviewProgress ,
255280 props . reviewInProgress ,
256281 props . variant ,
@@ -311,19 +336,23 @@ export const ChallengePhaseInfo: FC<Props> = (props: Props) => {
311336
312337export default ChallengePhaseInfo
313338
314- function createPhaseItems ( variant : ChallengeVariant , data : ChallengeInfo , isTask : boolean ) : ChallengePhaseItem [ ] {
315- if ( variant !== 'active' ) {
339+ function createPhaseItems ( config : {
340+ displayPhaseLabel : string
341+ isTask : boolean
342+ variant : ChallengeVariant
343+ } ) : ChallengePhaseItem [ ] {
344+ if ( config . variant !== 'active' ) {
316345 return [ ]
317346 }
318347
319- if ( isTask ) {
348+ if ( config . isTask ) {
320349 return [ ]
321350 }
322351
323352 return [ {
324353 icon : 'icon-review' ,
325354 title : 'Phase' ,
326- value : computeIterativeReviewLabel ( data ) || data . currentPhase || 'N/A' ,
355+ value : config . displayPhaseLabel || 'N/A' ,
327356 } ]
328357}
329358
@@ -385,6 +414,7 @@ function createNonTaskItems(config: {
385414 data : ChallengeInfo
386415 formattedNonTaskPayment ?: string
387416 isTask : boolean
417+ displayPhaseEndDateString ?: string
388418 variant : ChallengeVariant
389419 walletUrl : string
390420} ) : ChallengePhaseItem [ ] {
@@ -397,7 +427,7 @@ function createNonTaskItems(config: {
397427 title : config . variant === 'past' ? 'Challenge End Date' : 'Phase End Date' ,
398428 value : config . variant === 'past'
399429 ? getChallengeEndDateValue ( config . data )
400- : config . data . currentPhaseEndDateString || '-' ,
430+ : config . displayPhaseEndDateString || '-' ,
401431 } ]
402432
403433 if ( config . formattedNonTaskPayment ) {
@@ -421,6 +451,9 @@ function createNonTaskItems(config: {
421451
422452function createActiveItems ( config : {
423453 data : ChallengeInfo
454+ displayTimeLeft ?: string
455+ displayTimeLeftColor ?: string
456+ displayTimeLeftStatus ?: string
424457 progressType : typeof PROGRESS_TYPE
425458 reviewInProgress : boolean
426459 reviewProgress : number
@@ -437,12 +470,12 @@ function createActiveItems(config: {
437470
438471 const items : ChallengePhaseItem [ ] = [ {
439472 icon : 'icon-timer' ,
440- status : config . data . timeLeftStatus ,
473+ status : config . displayTimeLeftStatus ,
441474 style : {
442- color : config . data . timeLeftColor ,
475+ color : config . displayTimeLeftColor ,
443476 } ,
444477 title : 'Time Left' ,
445- value : config . data . timeLeft || '-' ,
478+ value : config . displayTimeLeft || '-' ,
446479 } ]
447480
448481 if ( ! config . reviewInProgress ) {
@@ -456,17 +489,152 @@ function createActiveItems(config: {
456489 return items
457490}
458491
492+ interface PhaseDisplayInfo {
493+ phaseEndDateString ?: string
494+ phaseLabel : string
495+ timeLeft ?: string
496+ timeLeftColor ?: string
497+ timeLeftStatus ?: string
498+ }
499+
500+ function computePhaseDisplayInfo ( data ?: ChallengeInfo ) : PhaseDisplayInfo {
501+ const fallbackPhaseLabel = ( data ?. currentPhase || '' ) . trim ( ) || 'N/A'
502+ const fallback : PhaseDisplayInfo = {
503+ phaseEndDateString : data ?. currentPhaseEndDateString ,
504+ phaseLabel : fallbackPhaseLabel ,
505+ timeLeft : data ?. timeLeft ,
506+ timeLeftColor : data ?. timeLeftColor ,
507+ timeLeftStatus : data ?. timeLeftStatus ,
508+ }
509+
510+ if ( ! data ) {
511+ return fallback
512+ }
513+
514+ const phaseForDisplay = selectPhaseForDisplay ( data )
515+ const phaseLabel = computeDisplayPhaseLabel ( data , phaseForDisplay ) || fallback . phaseLabel
516+ const timing = computePhaseTiming ( phaseForDisplay )
517+
518+ return {
519+ phaseEndDateString : timing . endDateString ?? fallback . phaseEndDateString ,
520+ phaseLabel,
521+ timeLeft : timing . timeLeft ?? fallback . timeLeft ,
522+ timeLeftColor : timing . timeLeftColor ?? fallback . timeLeftColor ,
523+ timeLeftStatus : timing . timeLeftStatus ?? fallback . timeLeftStatus ,
524+ }
525+ }
526+
527+ function selectPhaseForDisplay ( data ?: ChallengeInfo ) : BackendPhase | undefined {
528+ if ( ! data ) return undefined
529+
530+ const phases = Array . isArray ( data . phases ) ? data . phases : [ ]
531+ if ( ! phases . length ) {
532+ return data . currentPhaseObject
533+ }
534+
535+ const openPhases = phases . filter ( phase => phase ?. isOpen )
536+ if ( ! openPhases . length ) {
537+ return data . currentPhaseObject
538+ }
539+
540+ const submissionPhase = openPhases . find ( phase => normalizePhaseName ( phase ?. name ) === 'submission' )
541+ const registrationPhase = openPhases . find ( phase => normalizePhaseName ( phase ?. name ) === 'registration' )
542+
543+ if ( submissionPhase && registrationPhase ) {
544+ return submissionPhase
545+ }
546+
547+ if ( data . currentPhaseObject && data . currentPhaseObject . isOpen ) {
548+ return data . currentPhaseObject
549+ }
550+
551+ return submissionPhase ?? openPhases [ 0 ]
552+ }
553+
554+ function computeDisplayPhaseLabel (
555+ data ?: ChallengeInfo ,
556+ phase ?: BackendPhase ,
557+ ) : string {
558+ const fallback = ( data ?. currentPhase || '' ) . trim ( ) || 'N/A'
559+
560+ if ( ! data ) {
561+ return fallback
562+ }
563+
564+ if ( phase && isIterativePhaseName ( phase . name ) ) {
565+ const iterativeLabel = computeIterativeReviewLabel ( data , phase )
566+ if ( iterativeLabel ) {
567+ return iterativeLabel
568+ }
569+ } else if ( ! phase ) {
570+ const iterativeLabel = computeIterativeReviewLabel ( data )
571+ if ( iterativeLabel ) {
572+ return iterativeLabel
573+ }
574+ }
575+
576+ const name = ( phase ?. name || '' ) . trim ( )
577+ if ( name ) {
578+ return name
579+ }
580+
581+ return fallback
582+ }
583+
584+ function computePhaseTiming ( phase ?: BackendPhase ) : {
585+ endDateString ?: string
586+ timeLeft ?: string
587+ timeLeftColor ?: string
588+ timeLeftStatus ?: string
589+ } {
590+ if ( ! phase || ! phase . isOpen ) {
591+ return { }
592+ }
593+
594+ const rawEndDate = phase . actualEndDate || phase . scheduledEndDate
595+ if ( ! rawEndDate ) {
596+ return { }
597+ }
598+
599+ const endDate = new Date ( rawEndDate )
600+ if ( Number . isNaN ( endDate . getTime ( ) ) ) {
601+ return { }
602+ }
603+
604+ const formattedEndDate = moment ( endDate )
605+ . local ( )
606+ . format ( TABLE_DATE_FORMAT )
607+ const duration = formatDurationDate ( endDate , new Date ( ) )
608+
609+ return {
610+ endDateString : formattedEndDate ,
611+ timeLeft : duration . durationString ,
612+ timeLeftColor : duration . durationColor ,
613+ timeLeftStatus : duration . durationStatus ,
614+ }
615+ }
616+
617+ function normalizePhaseName ( name ?: string ) : string {
618+ return ( name || '' )
619+ . toString ( )
620+ . trim ( )
621+ . toLowerCase ( )
622+ }
623+
459624// Helpers extracted to keep component complexity manageable
460625function isIterativePhaseName ( name ?: string ) : boolean {
461626 return typeof name === 'string' && name . trim ( )
462627 . toLowerCase ( )
463628 . includes ( 'iterative review' )
464629}
465630
466- function computeIterativeReviewLabel ( data : any ) : string | undefined {
631+ function computeIterativeReviewLabel (
632+ data : any ,
633+ overridePhase ?: BackendPhase ,
634+ ) : string | undefined {
467635 const phases = Array . isArray ( data ?. phases ) ? data . phases : [ ]
468636
469- const current = data ?. currentPhaseObject
637+ const current = overridePhase ?? data ?. currentPhaseObject
470638 const currentIsIterative = isIterativePhaseName ( current ?. name )
471639
472640 const openIterative = currentIsIterative
0 commit comments