Skip to content

Commit e18ec2f

Browse files
Merge pull request #4803 from OneCommunityGlobal/vinay_k_edit_button
Vinay K Project Risk Profile
2 parents b165222 + 965bb06 commit e18ec2f

File tree

2 files changed

+493
-48
lines changed

2 files changed

+493
-48
lines changed

src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx

Lines changed: 267 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
Tooltip,
1010
Legend,
1111
ResponsiveContainer,
12+
LineChart,
13+
Line,
1214
} from 'recharts';
1315
import Select from 'react-select';
1416
import 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

Comments
 (0)