99 Tooltip ,
1010 Legend ,
1111 ResponsiveContainer ,
12+ LineChart ,
13+ Line ,
1214} from 'recharts' ;
1315import Select from 'react-select' ;
1416import httpService from '../../../services/httpService' ;
@@ -27,6 +29,7 @@ export default function ProjectRiskProfileOverview() {
2729 const [ allDates , setAllDates ] = useState ( [ ] ) ;
2830 const [ selectedDates , setSelectedDates ] = useState ( [ ] ) ;
2931 const [ showDateDropdown , setShowDateDropdown ] = useState ( false ) ;
32+ const [ trendData , setTrendData ] = useState ( { } ) ;
3033
3134 // Refs for focusing dropdowns
3235 const projectWrapperRef = useRef ( null ) ;
@@ -65,6 +68,8 @@ export default function ProjectRiskProfileOverview() {
6568 const dates = Array . from ( new Set ( result . flatMap ( p => p . dates || [ ] ) ) ) ;
6669 setAllDates ( dates ) ;
6770 setSelectedDates ( dates ) ;
71+ // create trend history used for movement indicators and timeline
72+ generateTrendData ( result ) ;
6873 } catch ( err ) {
6974 setError ( 'Failed to fetch project risk profile data.' ) ;
7075 } finally {
@@ -74,7 +79,38 @@ export default function ProjectRiskProfileOverview() {
7479 fetchData ( ) ;
7580 } , [ ] ) ;
7681
77- // Filter projects that are ongoing on ALL selected dates and in selectedProjects
82+ const generateTrendData = riskData => {
83+ const trends = { } ;
84+ riskData . forEach ( item => {
85+ const key = item . projectName || 'Unknown' ;
86+ if ( ! trends [ key ] ) trends [ key ] = [ ] ;
87+ const date = ( item . dates && item . dates [ 0 ] ) || item . date || new Date ( ) . toLocaleDateString ( ) ;
88+ trends [ key ] . push ( {
89+ date,
90+ // Risk metrics for timeline
91+ costOverrun : item . predictedCostOverrun || 0 ,
92+ issues : item . totalOpenIssues || 0 ,
93+ timeDelay : item . predictedTimeDelay || 0 ,
94+ // Risk attributes for historical tracking
95+ severity : item . severity || 'N/A' ,
96+ likelihood : item . likelihood || 'N/A' ,
97+ status : item . status || 'N/A' ,
98+ owner : item . owner || 'N/A' ,
99+ mitigationState : item . mitigationState || 'N/A' ,
100+ } ) ;
101+ } ) ;
102+ Object . keys ( trends ) . forEach ( k => {
103+ trends [ k ] . sort ( ( a , b ) => new Date ( a . date ) - new Date ( b . date ) ) ;
104+ } ) ;
105+ setTrendData ( trends ) ;
106+ } ;
107+
108+ const getTrendIndicator = ( current , previous ) => {
109+ if ( previous === undefined || previous === null ) return null ;
110+ if ( current > previous ) return { symbol : '↑' , color : '#EA4335' , label : 'Increased' } ;
111+ if ( current < previous ) return { symbol : '↓' , color : '#34A853' , label : 'Decreased' } ;
112+ return { symbol : '→' , color : '#FBBC05' , label : 'Unchanged' } ;
113+ } ;
78114 const filteredData = data . filter (
79115 p =>
80116 ( selectedProjects . length === 0 || selectedProjects . includes ( p . projectName ) ) &&
@@ -186,6 +222,12 @@ export default function ProjectRiskProfileOverview() {
186222 if ( loading ) return < div > Loading project risk profiles...</ div > ;
187223 if ( error ) return < div style = { { color : 'red' } } > { error } </ div > ;
188224
225+ const getTimelineData = ( ) => {
226+ if ( selectedProjects . length !== 1 ) return [ ] ;
227+ const projectName = selectedProjects [ 0 ] ;
228+ return trendData [ projectName ] || [ ] ;
229+ } ;
230+
189231 return (
190232 < div className = { `${ styles . chartCard } ${ darkMode ? styles . darkMode : '' } ` } >
191233 < h2 className = { styles . chartTitle } > Project Risk Profile Overview</ h2 >
@@ -256,82 +298,259 @@ export default function ProjectRiskProfileOverview() {
256298 </ div >
257299 </ div >
258300
259- { /* Chart Section */ }
260- < div className = { styles . chartContainer } >
261- < ResponsiveContainer width = "100%" height = { 400 } >
301+ < div className = { `${ styles . chartWrapper } ` } >
302+ < div className = { `${ styles . legendWrapper } ` } >
303+ < div className = { `${ styles . legendItem } ` } >
304+ < span
305+ className = { `${ styles . legendSquare } ` }
306+ style = { { backgroundColor : '#4285F4' } }
307+ > </ span >
308+ < span > Predicted Cost Overrun Percentage</ span >
309+ </ div >
310+ < div className = { `${ styles . legendItem } ` } >
311+ < span
312+ className = { `${ styles . legendSquare } ` }
313+ style = { { backgroundColor : '#EA4335' } }
314+ > </ span >
315+ < span > Issues</ span >
316+ </ div >
317+ < div className = { `${ styles . legendItem } ` } >
318+ < span
319+ className = { `${ styles . legendSquare } ` }
320+ style = { { backgroundColor : '#FBBC05' } }
321+ > </ span >
322+ < span > Predicted Time Delay Percentage</ span >
323+ </ div >
324+ </ div >
325+ < ResponsiveContainer width = "100%" height = "100%" >
262326 < BarChart
263- data = { filteredData . map ( item => {
264- return {
265- ...item ,
266- predictedCostOverrun : item . predictedCostOverrun ,
267- } ;
268- } ) }
269- margin = { { top : 20 , right : 40 , left : 60 , bottom : 80 } }
270- barCategoryGap = "20%"
271- barGap = { 4 }
327+ data = { filteredData }
328+ margin = { { top : 20 , right : 40 , left : 60 , bottom : 24 } }
329+ barGap = "5%"
330+ barCategoryGap = "28%"
272331 >
273- < CartesianGrid strokeDasharray = "3 3" horizontal = { false } stroke = { chartColors . grid } />
332+ < CartesianGrid
333+ strokeDasharray = "5 5"
334+ stroke = { darkMode ? '#3a3a3a' : '#e8e8e8' }
335+ horizontal = { true }
336+ vertical = { false }
337+ />
274338 < XAxis
275339 dataKey = "projectName"
276- tick = { { fontSize : 12 , fill : chartColors . text } }
277340 angle = { - 45 }
278341 textAnchor = "end"
279- height = { 80 }
342+ height = { 110 }
343+ tick = { { fontSize : 13 , fill : darkMode ? '#888' : '#666' , fontWeight : 500 } }
344+ axisLine = { { stroke : darkMode ? '#555' : '#d5d5d5' , strokeWidth : 1.5 } }
345+ tickLine = { { stroke : darkMode ? '#555' : '#d5d5d5' } }
280346 />
281347 < YAxis
348+ tick = { { fontSize : 12 , fill : darkMode ? '#888' : '#666' , fontWeight : 500 } }
349+ axisLine = { { stroke : darkMode ? '#555' : '#d5d5d5' , strokeWidth : 1.5 } }
350+ tickLine = { { stroke : darkMode ? '#555' : '#d5d5d5' } }
282351 label = { {
283352 value : 'Percentage (%)' ,
284353 angle : - 90 ,
285354 position : 'insideLeft' ,
286- offset : 15 ,
287- style : {
288- textAnchor : 'middle' ,
289- fontSize : 14 ,
290- fill : chartColors . text ,
291- fontWeight : '500' ,
292- } ,
355+ offset : - 10 ,
356+ style : { fontSize : 13 , fill : darkMode ? '#888' : '#666' , fontWeight : 500 } ,
293357 } }
294- tickFormatter = { value => ( Number . isInteger ( value ) ? value : value . toFixed ( 0 ) ) }
295- tick = { { fontSize : 12 , fill : chartColors . text } }
296358 />
297359 < Tooltip
298360 contentStyle = { {
299- backgroundColor : chartColors . tooltipBg ,
300- border : `1px solid ${ chartColors . tooltipBorder } ` ,
301- color : chartColors . tooltipText ,
302- borderRadius : '4px' ,
303- } }
304- cursor = { { fill : darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' } }
305- itemStyle = { { color : chartColors . tooltipText } }
306- formatter = { ( value , name ) => {
307- if ( typeof value === 'number' ) {
308- // Format Time Delay specifically to 2 decimal places
309- if ( name === 'Predicted Time Delay (%)' ) {
310- return value . toFixed ( 2 ) ;
311- }
312- // For other values, use 2 decimal places if not integer
313- return Number . isInteger ( value ) ? value . toString ( ) : value . toFixed ( 2 ) ;
314- }
315- return value ;
361+ backgroundColor : darkMode ? '#333' : '#fff' ,
362+ border : `2px solid ${ darkMode ? '#666' : '#e0e0e0' } ` ,
363+ borderRadius : '8px' ,
364+ padding : '14px' ,
365+ color : darkMode ? '#fff' : '#333' ,
366+ fontSize : '13px' ,
367+ fontWeight : 500 ,
368+ boxShadow : '0 4px 12px rgba(0, 0, 0, 0.15)' ,
316369 } }
370+ cursor = { { fill : 'rgba(66, 133, 244, 0.08)' } }
317371 />
318- < Legend wrapperStyle = { { marginTop : 20 , color : chartColors . text } } />
319372 < Bar
320373 dataKey = "predictedCostOverrun"
321- name = "Predicted Cost Overrun (%) "
374+ name = "Predicted Cost Overrun Percentage "
322375 fill = "#4285F4"
323- barSize = { 35 }
376+ radius = { [ 3 , 3 , 0 , 0 ] }
324377 />
325- < Bar dataKey = "totalOpenIssues" name = "Issues" fill = "#EA4335" barSize = { 35 } />
378+ < Bar dataKey = "totalOpenIssues" name = "Issues" fill = "#EA4335" radius = { [ 3 , 3 , 0 , 0 ] } />
326379 < Bar
327380 dataKey = "predictedTimeDelay"
328- name = "Predicted Time Delay (%) "
381+ name = "Predicted Time Delay Percentage "
329382 fill = "#FBBC05"
330- barSize = { 35 }
383+ radius = { [ 3 , 3 , 0 , 0 ] }
331384 />
332385 </ BarChart >
333386 </ ResponsiveContainer >
334387 </ div >
388+
389+ { /* Trend Indicators Section */ }
390+ { filteredData . length > 0 && (
391+ < div className = { `${ styles . trendSection } ` } >
392+ < h3 className = { `${ styles . trendTitle } ` } > Risk Movement Tracking</ h3 >
393+ < div className = { `${ styles . trendGrid } ` } >
394+ { filteredData . map ( project => {
395+ const prevData =
396+ trendData [ project . projectName ] && trendData [ project . projectName ] . length > 1
397+ ? trendData [ project . projectName ] [ trendData [ project . projectName ] . length - 2 ]
398+ : trendData [ project . projectName ] ?. [ 0 ] ;
399+ const costTrend = getTrendIndicator (
400+ project . predictedCostOverrun ,
401+ prevData ?. costOverrun ,
402+ ) ;
403+ const issueTrend = getTrendIndicator ( project . totalOpenIssues , prevData ?. issues ) ;
404+ const timeTrend = getTrendIndicator ( project . predictedTimeDelay , prevData ?. timeDelay ) ;
405+ const currentData =
406+ trendData [ project . projectName ] ?. [ trendData [ project . projectName ] ?. length - 1 ] ;
407+
408+ return (
409+ < div key = { project . projectName } className = { `${ styles . trendCard } ` } >
410+ < div className = { `${ styles . projectName } ` } > { project . projectName } </ div >
411+
412+ { /* Risk Metrics with Trends */ }
413+ < div className = { `${ styles . trendRow } ` } >
414+ < span > Cost Overrun:</ span >
415+ { costTrend && (
416+ < span
417+ className = { `${ styles . trendIndicator } ` }
418+ style = { { color : costTrend . color } }
419+ title = { costTrend . label }
420+ >
421+ { costTrend . symbol }
422+ </ span >
423+ ) }
424+ < span > { project . predictedCostOverrun || 0 } %</ span >
425+ </ div >
426+ < div className = { `${ styles . trendRow } ` } >
427+ < span > Issues:</ span >
428+ { issueTrend && (
429+ < span
430+ className = { `${ styles . trendIndicator } ` }
431+ style = { { color : issueTrend . color } }
432+ title = { issueTrend . label }
433+ >
434+ { issueTrend . symbol }
435+ </ span >
436+ ) }
437+ < span > { project . totalOpenIssues || 0 } </ span >
438+ </ div >
439+ < div className = { `${ styles . trendRow } ` } >
440+ < span > Time Delay:</ span >
441+ { timeTrend && (
442+ < span
443+ className = { `${ styles . trendIndicator } ` }
444+ style = { { color : timeTrend . color } }
445+ title = { timeTrend . label }
446+ >
447+ { timeTrend . symbol }
448+ </ span >
449+ ) }
450+ < span > { project . predictedTimeDelay || 0 } %</ span >
451+ </ div >
452+
453+ { /* Risk Attributes */ }
454+ { currentData && (
455+ < >
456+ < div className = { `${ styles . attributeRow } ` } >
457+ < span className = { `${ styles . label } ` } > Status:</ span >
458+ < span className = { `${ styles . value } ` } > { currentData . status } </ span >
459+ </ div >
460+ < div className = { `${ styles . attributeRow } ` } >
461+ < span className = { `${ styles . label } ` } > Severity:</ span >
462+ < span className = { `${ styles . value } ` } > { currentData . severity } </ span >
463+ </ div >
464+ < div className = { `${ styles . attributeRow } ` } >
465+ < span className = { `${ styles . label } ` } > Likelihood:</ span >
466+ < span className = { `${ styles . value } ` } > { currentData . likelihood } </ span >
467+ </ div >
468+ < div className = { `${ styles . attributeRow } ` } >
469+ < span className = { `${ styles . label } ` } > Owner:</ span >
470+ < span className = { `${ styles . value } ` } > { currentData . owner } </ span >
471+ </ div >
472+ < div className = { `${ styles . attributeRow } ` } >
473+ < span className = { `${ styles . label } ` } > Mitigation:</ span >
474+ < span className = { `${ styles . value } ` } > { currentData . mitigationState } </ span >
475+ </ div >
476+ </ >
477+ ) }
478+ </ div >
479+ ) ;
480+ } ) }
481+ </ div >
482+ </ div >
483+ ) }
484+
485+ { /* Timeline Chart for Single Project */ }
486+ { selectedProjects . length === 1 && getTimelineData ( ) . length > 1 && (
487+ < div className = { `${ styles . timelineSection } ` } >
488+ < h3 className = { `${ styles . trendTitle } ` } > Historical Risk Trend - { selectedProjects [ 0 ] } </ h3 >
489+ < ResponsiveContainer width = "100%" height = { 300 } >
490+ < LineChart
491+ data = { getTimelineData ( ) }
492+ margin = { { top : 20 , right : 30 , left : 0 , bottom : 30 } }
493+ >
494+ < CartesianGrid
495+ strokeDasharray = "5 5"
496+ stroke = { darkMode ? '#3a3a3a' : '#e8e8e8' }
497+ horizontal = { true }
498+ vertical = { false }
499+ />
500+ < XAxis
501+ dataKey = "date"
502+ tick = { { fontSize : 11 , fill : darkMode ? '#888' : '#666' } }
503+ axisLine = { { stroke : darkMode ? '#555' : '#d5d5d5' , strokeWidth : 1 } }
504+ />
505+ < YAxis
506+ tick = { { fontSize : 11 , fill : darkMode ? '#888' : '#666' } }
507+ axisLine = { { stroke : darkMode ? '#555' : '#d5d5d5' , strokeWidth : 1 } }
508+ />
509+ < Tooltip
510+ contentStyle = { {
511+ backgroundColor : darkMode ? '#333' : '#fff' ,
512+ border : `1px solid ${ darkMode ? '#666' : '#e0e0e0' } ` ,
513+ borderRadius : '6px' ,
514+ padding : '10px' ,
515+ color : darkMode ? '#fff' : '#333' ,
516+ fontSize : '12px' ,
517+ } }
518+ />
519+ < Legend
520+ wrapperStyle = { {
521+ paddingTop : '20px' ,
522+ fontSize : '12px' ,
523+ color : darkMode ? '#ddd' : '#444' ,
524+ } }
525+ />
526+ < Line
527+ type = "monotone"
528+ dataKey = "costOverrun"
529+ stroke = "#4285F4"
530+ name = "Cost Overrun %"
531+ dot = { { r : 4 } }
532+ strokeWidth = { 2 }
533+ />
534+ < Line
535+ type = "monotone"
536+ dataKey = "issues"
537+ stroke = "#EA4335"
538+ name = "Issues"
539+ dot = { { r : 4 } }
540+ strokeWidth = { 2 }
541+ />
542+ < Line
543+ type = "monotone"
544+ dataKey = "timeDelay"
545+ stroke = "#FBBC05"
546+ name = "Time Delay %"
547+ dot = { { r : 4 } }
548+ strokeWidth = { 2 }
549+ />
550+ </ LineChart >
551+ </ ResponsiveContainer >
552+ </ div >
553+ ) }
335554 </ div >
336555 ) ;
337556}
0 commit comments