@@ -37,6 +37,8 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
3737 const [ isMinimized , setIsMinimized ] = useState ( false ) ;
3838 const [ isDismissed , setIsDismissed ] = useState ( false ) ;
3939 const [ isPoliciesExpanded , setIsPoliciesExpanded ] = useState ( false ) ;
40+ const [ isVendorsExpanded , setIsVendorsExpanded ] = useState ( false ) ;
41+ const [ isRisksExpanded , setIsRisksExpanded ] = useState ( false ) ;
4042
4143 // useRealtimeRun will automatically get the token from TriggerProvider context
4244 // This gives us real-time updates including metadata changes
@@ -63,6 +65,16 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
6365 risk : false ,
6466 policies : false ,
6567 currentStep : null ,
68+ vendorsTotal : 0 ,
69+ vendorsCompleted : 0 ,
70+ vendorsRemaining : 0 ,
71+ vendorsInfo : [ ] ,
72+ vendorsStatus : { } ,
73+ risksTotal : 0 ,
74+ risksCompleted : 0 ,
75+ risksRemaining : 0 ,
76+ risksInfo : [ ] ,
77+ risksStatus : { } ,
6678 policiesTotal : 0 ,
6779 policiesCompleted : 0 ,
6880 policiesRemaining : 0 ,
@@ -73,6 +85,24 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
7385
7486 const meta = run . metadata as Record < string , unknown > ;
7587
88+ // Build vendorsStatus object from individual vendor status keys
89+ const vendorsStatus : Record < string , 'pending' | 'processing' | 'completed' > = { } ;
90+ const vendorsInfo = ( meta . vendorsInfo as Array < { id : string ; name : string } > ) || [ ] ;
91+
92+ vendorsInfo . forEach ( ( vendor ) => {
93+ const statusKey = `vendor_${ vendor . id } _status` ;
94+ vendorsStatus [ vendor . id ] = ( meta [ statusKey ] as 'pending' | 'processing' | 'completed' ) || 'pending' ;
95+ } ) ;
96+
97+ // Build risksStatus object from individual risk status keys
98+ const risksStatus : Record < string , 'pending' | 'processing' | 'completed' > = { } ;
99+ const risksInfo = ( meta . risksInfo as Array < { id : string ; name : string } > ) || [ ] ;
100+
101+ risksInfo . forEach ( ( risk ) => {
102+ const statusKey = `risk_${ risk . id } _status` ;
103+ risksStatus [ risk . id ] = ( meta [ statusKey ] as 'pending' | 'processing' | 'completed' ) || 'pending' ;
104+ } ) ;
105+
76106 // Build policiesStatus object from individual policy status keys
77107 const policiesStatus : Record < string , 'pending' | 'processing' | 'completed' > = { } ;
78108 const policiesInfo = ( meta . policiesInfo as Array < { id : string ; name : string } > ) || [ ] ;
@@ -88,6 +118,16 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
88118 risk : meta . risk === true ,
89119 policies : meta . policies === true ,
90120 currentStep : ( meta . currentStep as string ) || null ,
121+ vendorsTotal : ( meta . vendorsTotal as number ) || 0 ,
122+ vendorsCompleted : ( meta . vendorsCompleted as number ) || 0 ,
123+ vendorsRemaining : ( meta . vendorsRemaining as number ) || 0 ,
124+ vendorsInfo,
125+ vendorsStatus,
126+ risksTotal : ( meta . risksTotal as number ) || 0 ,
127+ risksCompleted : ( meta . risksCompleted as number ) || 0 ,
128+ risksRemaining : ( meta . risksRemaining as number ) || 0 ,
129+ risksInfo,
130+ risksStatus,
91131 policiesTotal : ( meta . policiesTotal as number ) || 0 ,
92132 policiesCompleted : ( meta . policiesCompleted as number ) || 0 ,
93133 policiesRemaining : ( meta . policiesRemaining as number ) || 0 ,
@@ -107,6 +147,28 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
107147 return ONBOARDING_STEPS . find ( ( step ) => ! stepStatus [ step . key as keyof typeof stepStatus ] ) ;
108148 } , [ stepStatus ] ) ;
109149
150+ // Auto-expand current step and collapse others
151+ useEffect ( ( ) => {
152+ if ( ! currentStep ) return ;
153+
154+ const stepKey = currentStep . key ;
155+
156+ // Expand current step if it has items to show
157+ if ( stepKey === 'vendors' && stepStatus . vendorsTotal > 0 ) {
158+ setIsVendorsExpanded ( true ) ;
159+ setIsRisksExpanded ( false ) ;
160+ setIsPoliciesExpanded ( false ) ;
161+ } else if ( stepKey === 'risk' && stepStatus . risksTotal > 0 ) {
162+ setIsVendorsExpanded ( false ) ;
163+ setIsRisksExpanded ( true ) ;
164+ setIsPoliciesExpanded ( false ) ;
165+ } else if ( stepKey === 'policies' && stepStatus . policiesTotal > 0 ) {
166+ setIsVendorsExpanded ( false ) ;
167+ setIsRisksExpanded ( false ) ;
168+ setIsPoliciesExpanded ( true ) ;
169+ }
170+ } , [ currentStep ?. key , stepStatus . vendorsTotal , stepStatus . risksTotal , stepStatus . policiesTotal ] ) ;
171+
110172 // Build dynamic current step message with progress
111173 const currentStepMessage = useMemo ( ( ) => {
112174 if ( stepStatus . currentStep ) {
@@ -267,8 +329,180 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
267329 { ONBOARDING_STEPS . map ( ( step ) => {
268330 const isCompleted = stepStatus [ step . key as keyof typeof stepStatus ] ;
269331 const isCurrent = currentStep ?. key === step . key ;
332+ const isVendorsStep = step . key === 'vendors' ;
333+ const isRisksStep = step . key === 'risk' ;
270334 const isPoliciesStep = step . key === 'policies' ;
271335
336+ // Vendors step with expandable dropdown
337+ if ( isVendorsStep && stepStatus . vendorsTotal > 0 ) {
338+ return (
339+ < div key = { step . key } className = "flex flex-col gap-2" >
340+ < button
341+ onClick = { ( ) => setIsVendorsExpanded ( ! isVendorsExpanded ) }
342+ className = "flex items-center gap-2 w-full text-left"
343+ >
344+ { isCompleted ? (
345+ < CheckCircle2 className = "text-chart-positive h-5 w-5 flex-shrink-0" />
346+ ) : isCurrent ? (
347+ < Loader2 className = "h-5 w-5 flex-shrink-0 animate-spin text-primary" />
348+ ) : (
349+ < div className = "h-5 w-5 flex-shrink-0 rounded-full border-2 border-muted" />
350+ ) }
351+ < div className = "flex flex-1 items-center justify-between gap-2 min-w-0" >
352+ < span
353+ className = { `text-sm ${
354+ isCompleted
355+ ? 'text-chart-positive'
356+ : isCurrent
357+ ? 'text-primary font-medium'
358+ : 'text-muted-foreground'
359+ } `}
360+ >
361+ { step . label }
362+ </ span >
363+ < div className = "flex items-center gap-2 flex-shrink-0" >
364+ < span className = "text-muted-foreground text-sm" >
365+ { stepStatus . vendorsCompleted } /{ stepStatus . vendorsTotal }
366+ </ span >
367+ { isVendorsExpanded ? (
368+ < ChevronUp className = "h-4 w-4 text-muted-foreground" />
369+ ) : (
370+ < ChevronDown className = "h-4 w-4 text-muted-foreground" />
371+ ) }
372+ </ div >
373+ </ div >
374+ </ button >
375+
376+ { /* Expanded vendor list */ }
377+ { isVendorsExpanded && stepStatus . vendorsInfo . length > 0 && (
378+ < motion . div
379+ initial = { { opacity : 0 , height : 0 } }
380+ animate = { { opacity : 1 , height : 'auto' } }
381+ exit = { { opacity : 0 , height : 0 } }
382+ transition = { { duration : 0.2 } }
383+ className = "overflow-hidden"
384+ >
385+ < div className = "flex flex-col gap-1.5 pl-7" >
386+ { stepStatus . vendorsInfo . map ( ( vendor ) => {
387+ const vendorStatus = stepStatus . vendorsStatus [ vendor . id ] || 'pending' ;
388+ const isVendorCompleted = vendorStatus === 'completed' ;
389+ const isVendorProcessing = vendorStatus === 'processing' ;
390+
391+ return (
392+ < div key = { vendor . id } className = "flex items-center gap-2" >
393+ { isVendorCompleted ? (
394+ < CheckCircle2 className = "text-chart-positive h-4 w-4 flex-shrink-0" />
395+ ) : isVendorProcessing ? (
396+ < Loader2 className = "h-4 w-4 flex-shrink-0 animate-spin text-primary" />
397+ ) : (
398+ < div className = "h-4 w-4 flex-shrink-0 rounded-full border-2 border-muted" />
399+ ) }
400+ < span
401+ className = { `text-sm truncate ${
402+ isVendorCompleted
403+ ? 'text-chart-positive'
404+ : isVendorProcessing
405+ ? 'text-primary'
406+ : 'text-muted-foreground'
407+ } `}
408+ >
409+ { vendor . name }
410+ </ span >
411+ </ div >
412+ ) ;
413+ } ) }
414+ </ div >
415+ </ motion . div >
416+ ) }
417+ </ div >
418+ ) ;
419+ }
420+
421+ // Risks step with expandable dropdown
422+ if ( isRisksStep && stepStatus . risksTotal > 0 ) {
423+ return (
424+ < div key = { step . key } className = "flex flex-col gap-2" >
425+ < button
426+ onClick = { ( ) => setIsRisksExpanded ( ! isRisksExpanded ) }
427+ className = "flex items-center gap-2 w-full text-left"
428+ >
429+ { isCompleted ? (
430+ < CheckCircle2 className = "text-chart-positive h-5 w-5 flex-shrink-0" />
431+ ) : isCurrent ? (
432+ < Loader2 className = "h-5 w-5 flex-shrink-0 animate-spin text-primary" />
433+ ) : (
434+ < div className = "h-5 w-5 flex-shrink-0 rounded-full border-2 border-muted" />
435+ ) }
436+ < div className = "flex flex-1 items-center justify-between gap-2 min-w-0" >
437+ < span
438+ className = { `text-sm ${
439+ isCompleted
440+ ? 'text-chart-positive'
441+ : isCurrent
442+ ? 'text-primary font-medium'
443+ : 'text-muted-foreground'
444+ } `}
445+ >
446+ { step . label }
447+ </ span >
448+ < div className = "flex items-center gap-2 flex-shrink-0" >
449+ < span className = "text-muted-foreground text-sm" >
450+ { stepStatus . risksCompleted } /{ stepStatus . risksTotal }
451+ </ span >
452+ { isRisksExpanded ? (
453+ < ChevronUp className = "h-4 w-4 text-muted-foreground" />
454+ ) : (
455+ < ChevronDown className = "h-4 w-4 text-muted-foreground" />
456+ ) }
457+ </ div >
458+ </ div >
459+ </ button >
460+
461+ { /* Expanded risk list */ }
462+ { isRisksExpanded && stepStatus . risksInfo . length > 0 && (
463+ < motion . div
464+ initial = { { opacity : 0 , height : 0 } }
465+ animate = { { opacity : 1 , height : 'auto' } }
466+ exit = { { opacity : 0 , height : 0 } }
467+ transition = { { duration : 0.2 } }
468+ className = "overflow-hidden"
469+ >
470+ < div className = "flex flex-col gap-1.5 pl-7" >
471+ { stepStatus . risksInfo . map ( ( risk ) => {
472+ const riskStatus = stepStatus . risksStatus [ risk . id ] || 'pending' ;
473+ const isRiskCompleted = riskStatus === 'completed' ;
474+ const isRiskProcessing = riskStatus === 'processing' ;
475+
476+ return (
477+ < div key = { risk . id } className = "flex items-center gap-2" >
478+ { isRiskCompleted ? (
479+ < CheckCircle2 className = "text-chart-positive h-4 w-4 flex-shrink-0" />
480+ ) : isRiskProcessing ? (
481+ < Loader2 className = "h-4 w-4 flex-shrink-0 animate-spin text-primary" />
482+ ) : (
483+ < div className = "h-4 w-4 flex-shrink-0 rounded-full border-2 border-muted" />
484+ ) }
485+ < span
486+ className = { `text-sm truncate ${
487+ isRiskCompleted
488+ ? 'text-chart-positive'
489+ : isRiskProcessing
490+ ? 'text-primary'
491+ : 'text-muted-foreground'
492+ } `}
493+ >
494+ { risk . name }
495+ </ span >
496+ </ div >
497+ ) ;
498+ } ) }
499+ </ div >
500+ </ motion . div >
501+ ) }
502+ </ div >
503+ ) ;
504+ }
505+
272506 if ( isPoliciesStep && stepStatus . policiesTotal > 0 ) {
273507 // Policies step with expandable dropdown
274508 return (
0 commit comments