Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
Tooltip,
Legend,
ResponsiveContainer,
LineChart,
Line,
} from 'recharts';
import Select from 'react-select';
import httpService from '../../../services/httpService';
Expand All @@ -16,7 +18,7 @@

// Fetch project risk profile data from backend

export default function ProjectRiskProfileOverview() {

Check failure on line 21 in src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 28 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2sUMb_kb8XHn6cLalc&open=AZ2sUMb_kb8XHn6cLalc&pullRequest=4803
const darkMode = useSelector(state => state.theme?.darkMode || false);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
Expand All @@ -27,6 +29,7 @@
const [allDates, setAllDates] = useState([]);
const [selectedDates, setSelectedDates] = useState([]);
const [showDateDropdown, setShowDateDropdown] = useState(false);
const [trendData, setTrendData] = useState({});

// Refs for focusing dropdowns
const projectWrapperRef = useRef(null);
Expand Down Expand Up @@ -65,6 +68,8 @@
const dates = Array.from(new Set(result.flatMap(p => p.dates || [])));
setAllDates(dates);
setSelectedDates(dates);
// create trend history used for movement indicators and timeline
generateTrendData(result);
} catch (err) {
setError('Failed to fetch project risk profile data.');
} finally {
Expand All @@ -74,7 +79,38 @@
fetchData();
}, []);

// Filter projects that are ongoing on ALL selected dates and in selectedProjects
const generateTrendData = riskData => {
const trends = {};
riskData.forEach(item => {
const key = item.projectName || 'Unknown';
if (!trends[key]) trends[key] = [];
const date = (item.dates && item.dates[0]) || item.date || new Date().toLocaleDateString();

Check warning on line 87 in src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2sUMb_kb8XHn6cLald&open=AZ2sUMb_kb8XHn6cLald&pullRequest=4803
trends[key].push({
date,
// Risk metrics for timeline
costOverrun: item.predictedCostOverrun || 0,
issues: item.totalOpenIssues || 0,
timeDelay: item.predictedTimeDelay || 0,
// Risk attributes for historical tracking
severity: item.severity || 'N/A',
likelihood: item.likelihood || 'N/A',
status: item.status || 'N/A',
owner: item.owner || 'N/A',
mitigationState: item.mitigationState || 'N/A',
});
});
Object.keys(trends).forEach(k => {
trends[k].sort((a, b) => new Date(a.date) - new Date(b.date));
});
setTrendData(trends);
};

const getTrendIndicator = (current, previous) => {
if (previous === undefined || previous === null) return null;
if (current > previous) return { symbol: '↑', color: '#EA4335', label: 'Increased' };
if (current < previous) return { symbol: '↓', color: '#34A853', label: 'Decreased' };
return { symbol: '→', color: '#FBBC05', label: 'Unchanged' };
};
const filteredData = data.filter(
p =>
(selectedProjects.length === 0 || selectedProjects.includes(p.projectName)) &&
Expand Down Expand Up @@ -175,7 +211,7 @@
};

// Colors aligned with your global theme
const chartColors = {

Check warning on line 214 in src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of the unused 'chartColors' variable.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2sUMb_kb8XHn6cLale&open=AZ2sUMb_kb8XHn6cLale&pullRequest=4803

Check warning on line 214 in src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "chartColors".

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2sUMb_kb8XHn6cLalf&open=AZ2sUMb_kb8XHn6cLalf&pullRequest=4803
grid: darkMode ? 'rgba(255,255,255,0.1)' : '#e5e5e5',
text: darkMode ? '#e5e5e5' : '#333',
tooltipBg: darkMode ? '#1c2541' : '#ffffff',
Expand All @@ -186,6 +222,12 @@
if (loading) return <div>Loading project risk profiles...</div>;
if (error) return <div style={{ color: 'red' }}>{error}</div>;

const getTimelineData = () => {
if (selectedProjects.length !== 1) return [];
const projectName = selectedProjects[0];
return trendData[projectName] || [];
};

return (
<div className={`${styles.chartCard} ${darkMode ? styles.darkMode : ''}`}>
<h2 className={styles.chartTitle}>Project Risk Profile Overview</h2>
Expand Down Expand Up @@ -256,82 +298,259 @@
</div>
</div>

{/* Chart Section */}
<div className={styles.chartContainer}>
<ResponsiveContainer width="100%" height={400}>
<div className={`${styles.chartWrapper}`}>
<div className={`${styles.legendWrapper}`}>
<div className={`${styles.legendItem}`}>
<span
className={`${styles.legendSquare}`}
style={{ backgroundColor: '#4285F4' }}
></span>
<span>Predicted Cost Overrun Percentage</span>
</div>
<div className={`${styles.legendItem}`}>
<span
className={`${styles.legendSquare}`}
style={{ backgroundColor: '#EA4335' }}
></span>
<span>Issues</span>
</div>
<div className={`${styles.legendItem}`}>
<span
className={`${styles.legendSquare}`}
style={{ backgroundColor: '#FBBC05' }}
></span>
<span>Predicted Time Delay Percentage</span>
</div>
</div>
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={filteredData.map(item => {
return {
...item,
predictedCostOverrun: item.predictedCostOverrun,
};
})}
margin={{ top: 20, right: 40, left: 60, bottom: 80 }}
barCategoryGap="20%"
barGap={4}
data={filteredData}
margin={{ top: 20, right: 40, left: 60, bottom: 24 }}
barGap="5%"
barCategoryGap="28%"
>
<CartesianGrid strokeDasharray="3 3" horizontal={false} stroke={chartColors.grid} />
<CartesianGrid
strokeDasharray="5 5"
stroke={darkMode ? '#3a3a3a' : '#e8e8e8'}
horizontal={true}
vertical={false}
/>
<XAxis
dataKey="projectName"
tick={{ fontSize: 12, fill: chartColors.text }}
angle={-45}
textAnchor="end"
height={80}
height={110}
tick={{ fontSize: 13, fill: darkMode ? '#888' : '#666', fontWeight: 500 }}
axisLine={{ stroke: darkMode ? '#555' : '#d5d5d5', strokeWidth: 1.5 }}
tickLine={{ stroke: darkMode ? '#555' : '#d5d5d5' }}
/>
<YAxis
tick={{ fontSize: 12, fill: darkMode ? '#888' : '#666', fontWeight: 500 }}
axisLine={{ stroke: darkMode ? '#555' : '#d5d5d5', strokeWidth: 1.5 }}
tickLine={{ stroke: darkMode ? '#555' : '#d5d5d5' }}
label={{
value: 'Percentage (%)',
angle: -90,
position: 'insideLeft',
offset: 15,
style: {
textAnchor: 'middle',
fontSize: 14,
fill: chartColors.text,
fontWeight: '500',
},
offset: -10,
style: { fontSize: 13, fill: darkMode ? '#888' : '#666', fontWeight: 500 },
}}
tickFormatter={value => (Number.isInteger(value) ? value : value.toFixed(0))}
tick={{ fontSize: 12, fill: chartColors.text }}
/>
<Tooltip
contentStyle={{
backgroundColor: chartColors.tooltipBg,
border: `1px solid ${chartColors.tooltipBorder}`,
color: chartColors.tooltipText,
borderRadius: '4px',
}}
cursor={{ fill: darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' }}
itemStyle={{ color: chartColors.tooltipText }}
formatter={(value, name) => {
if (typeof value === 'number') {
// Format Time Delay specifically to 2 decimal places
if (name === 'Predicted Time Delay (%)') {
return value.toFixed(2);
}
// For other values, use 2 decimal places if not integer
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
}
return value;
backgroundColor: darkMode ? '#333' : '#fff',
border: `2px solid ${darkMode ? '#666' : '#e0e0e0'}`,
borderRadius: '8px',
padding: '14px',
color: darkMode ? '#fff' : '#333',
fontSize: '13px',
fontWeight: 500,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
}}
cursor={{ fill: 'rgba(66, 133, 244, 0.08)' }}
/>
<Legend wrapperStyle={{ marginTop: 20, color: chartColors.text }} />
<Bar
dataKey="predictedCostOverrun"
name="Predicted Cost Overrun (%)"
name="Predicted Cost Overrun Percentage"
fill="#4285F4"
barSize={35}
radius={[3, 3, 0, 0]}
/>
<Bar dataKey="totalOpenIssues" name="Issues" fill="#EA4335" barSize={35} />
<Bar dataKey="totalOpenIssues" name="Issues" fill="#EA4335" radius={[3, 3, 0, 0]} />
<Bar
dataKey="predictedTimeDelay"
name="Predicted Time Delay (%)"
name="Predicted Time Delay Percentage"
fill="#FBBC05"
barSize={35}
radius={[3, 3, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>

{/* Trend Indicators Section */}
{filteredData.length > 0 && (
<div className={`${styles.trendSection}`}>
<h3 className={`${styles.trendTitle}`}>Risk Movement Tracking</h3>
<div className={`${styles.trendGrid}`}>
{filteredData.map(project => {
const prevData =
trendData[project.projectName] && trendData[project.projectName].length > 1
? trendData[project.projectName][trendData[project.projectName].length - 2]
: trendData[project.projectName]?.[0];
const costTrend = getTrendIndicator(
project.predictedCostOverrun,
prevData?.costOverrun,
);
const issueTrend = getTrendIndicator(project.totalOpenIssues, prevData?.issues);
const timeTrend = getTrendIndicator(project.predictedTimeDelay, prevData?.timeDelay);
const currentData =
trendData[project.projectName]?.[trendData[project.projectName]?.length - 1];

return (
<div key={project.projectName} className={`${styles.trendCard}`}>
<div className={`${styles.projectName}`}>{project.projectName}</div>

{/* Risk Metrics with Trends */}
<div className={`${styles.trendRow}`}>
<span>Cost Overrun:</span>
{costTrend && (
<span
className={`${styles.trendIndicator}`}
style={{ color: costTrend.color }}
title={costTrend.label}
>
{costTrend.symbol}
</span>
)}
<span>{project.predictedCostOverrun || 0}%</span>
</div>
<div className={`${styles.trendRow}`}>
<span>Issues:</span>
{issueTrend && (
<span
className={`${styles.trendIndicator}`}
style={{ color: issueTrend.color }}
title={issueTrend.label}
>
{issueTrend.symbol}
</span>
)}
<span>{project.totalOpenIssues || 0}</span>
</div>
<div className={`${styles.trendRow}`}>
<span>Time Delay:</span>
{timeTrend && (
<span
className={`${styles.trendIndicator}`}
style={{ color: timeTrend.color }}
title={timeTrend.label}
>
{timeTrend.symbol}
</span>
)}
<span>{project.predictedTimeDelay || 0}%</span>
</div>

{/* Risk Attributes */}
{currentData && (
<>
<div className={`${styles.attributeRow}`}>
<span className={`${styles.label}`}>Status:</span>
<span className={`${styles.value}`}>{currentData.status}</span>
</div>
<div className={`${styles.attributeRow}`}>
<span className={`${styles.label}`}>Severity:</span>
<span className={`${styles.value}`}>{currentData.severity}</span>
</div>
<div className={`${styles.attributeRow}`}>
<span className={`${styles.label}`}>Likelihood:</span>
<span className={`${styles.value}`}>{currentData.likelihood}</span>
</div>
<div className={`${styles.attributeRow}`}>
<span className={`${styles.label}`}>Owner:</span>
<span className={`${styles.value}`}>{currentData.owner}</span>
</div>
<div className={`${styles.attributeRow}`}>
<span className={`${styles.label}`}>Mitigation:</span>
<span className={`${styles.value}`}>{currentData.mitigationState}</span>
</div>
</>
)}
</div>
);
})}
</div>
</div>
)}

{/* Timeline Chart for Single Project */}
{selectedProjects.length === 1 && getTimelineData().length > 1 && (
<div className={`${styles.timelineSection}`}>
<h3 className={`${styles.trendTitle}`}>Historical Risk Trend - {selectedProjects[0]}</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={getTimelineData()}
margin={{ top: 20, right: 30, left: 0, bottom: 30 }}
>
<CartesianGrid
strokeDasharray="5 5"
stroke={darkMode ? '#3a3a3a' : '#e8e8e8'}
horizontal={true}
vertical={false}
/>
<XAxis
dataKey="date"
tick={{ fontSize: 11, fill: darkMode ? '#888' : '#666' }}
axisLine={{ stroke: darkMode ? '#555' : '#d5d5d5', strokeWidth: 1 }}
/>
<YAxis
tick={{ fontSize: 11, fill: darkMode ? '#888' : '#666' }}
axisLine={{ stroke: darkMode ? '#555' : '#d5d5d5', strokeWidth: 1 }}
/>
<Tooltip
contentStyle={{
backgroundColor: darkMode ? '#333' : '#fff',
border: `1px solid ${darkMode ? '#666' : '#e0e0e0'}`,
borderRadius: '6px',
padding: '10px',
color: darkMode ? '#fff' : '#333',
fontSize: '12px',
}}
/>
<Legend
wrapperStyle={{
paddingTop: '20px',
fontSize: '12px',
color: darkMode ? '#ddd' : '#444',
}}
/>
<Line
type="monotone"
dataKey="costOverrun"
stroke="#4285F4"
name="Cost Overrun %"
dot={{ r: 4 }}
strokeWidth={2}
/>
<Line
type="monotone"
dataKey="issues"
stroke="#EA4335"
name="Issues"
dot={{ r: 4 }}
strokeWidth={2}
/>
<Line
type="monotone"
dataKey="timeDelay"
stroke="#FBBC05"
name="Time Delay %"
dot={{ r: 4 }}
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
);
}
Loading
Loading