diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx
index 1effa1946b..53aa3a0190 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx
+++ b/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.jsx
@@ -9,6 +9,8 @@ import {
Tooltip,
Legend,
ResponsiveContainer,
+ LineChart,
+ Line,
} from 'recharts';
import Select from 'react-select';
import httpService from '../../../services/httpService';
@@ -27,6 +29,7 @@ export default function ProjectRiskProfileOverview() {
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);
@@ -65,6 +68,8 @@ export default function ProjectRiskProfileOverview() {
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 {
@@ -74,7 +79,38 @@ export default function ProjectRiskProfileOverview() {
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();
+ 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)) &&
@@ -186,6 +222,12 @@ export default function ProjectRiskProfileOverview() {
if (loading) return
Loading project risk profiles...
;
if (error) return {error}
;
+ const getTimelineData = () => {
+ if (selectedProjects.length !== 1) return [];
+ const projectName = selectedProjects[0];
+ return trendData[projectName] || [];
+ };
+
return (
Project Risk Profile Overview
@@ -256,82 +298,259 @@ export default function ProjectRiskProfileOverview() {
- {/* Chart Section */}
-
-
+
+
+
+
+ Predicted Cost Overrun Percentage
+
+
+
+ Issues
+
+
+
+ Predicted Time Delay Percentage
+
+
+
{
- 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%"
>
-
+
(Number.isInteger(value) ? value : value.toFixed(0))}
- tick={{ fontSize: 12, fill: chartColors.text }}
/>
{
- 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)' }}
/>
-
-
+
+
+ {/* Trend Indicators Section */}
+ {filteredData.length > 0 && (
+
+
Risk Movement Tracking
+
+ {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 (
+
+
{project.projectName}
+
+ {/* Risk Metrics with Trends */}
+
+ Cost Overrun:
+ {costTrend && (
+
+ {costTrend.symbol}
+
+ )}
+ {project.predictedCostOverrun || 0}%
+
+
+ Issues:
+ {issueTrend && (
+
+ {issueTrend.symbol}
+
+ )}
+ {project.totalOpenIssues || 0}
+
+
+ Time Delay:
+ {timeTrend && (
+
+ {timeTrend.symbol}
+
+ )}
+ {project.predictedTimeDelay || 0}%
+
+
+ {/* Risk Attributes */}
+ {currentData && (
+ <>
+
+ Status:
+ {currentData.status}
+
+
+ Severity:
+ {currentData.severity}
+
+
+ Likelihood:
+ {currentData.likelihood}
+
+
+ Owner:
+ {currentData.owner}
+
+
+ Mitigation:
+ {currentData.mitigationState}
+
+ >
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ {/* Timeline Chart for Single Project */}
+ {selectedProjects.length === 1 && getTimelineData().length > 1 && (
+
+
Historical Risk Trend - {selectedProjects[0]}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
);
}
diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.module.css b/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.module.css
index 85f5697447..de8ed50350 100644
--- a/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.module.css
+++ b/src/components/BMDashboard/WeeklyProjectSummary/ProjectRiskProfileOverview.module.css
@@ -78,6 +78,87 @@
margin-top: 2px;
}
+/* React-select overrides */
+.customSelect__control {
+ font-size: 14px;
+ min-height: 34px;
+ background: none;
+ border: none;
+ box-shadow: none;
+}
+
+.darkMode .customSelect__control {
+ color: #eee;
+ background: #2c2c2c;
+}
+
+.customSelect__multi-value {
+ background: #e6f7ff;
+ font-size: 12px;
+ margin: 2px;
+}
+
+.darkMode .customSelect__multi-value {
+ background: #444;
+ color: #eee;
+}
+
+.customSelect__option {
+ color: #000;
+ background-color: #fff;
+}
+
+.customSelect__option:hover {
+ background-color: #f0f0f0;
+}
+
+.darkMode .customSelect__option {
+ color: #eee;
+ background-color: #2c2c2c;
+}
+
+.darkMode .customSelect__option:hover {
+ background-color: #3a3a3a;
+}
+
+.customSelect__menu {
+ z-index: 9999;
+}
+
+.error {
+ color: red;
+}
+
+.loading {
+ font-size: 16px;
+ font-weight: 500;
+}
+
+/* ===================== DARK MODE ===================== */
+.darkMode .container {
+ background: #1e1e1e;
+ box-shadow: 0 2px 8px #111;
+ color: #eee;
+}
+
+.darkMode .dropdownButton {
+ color: #eee;
+ background: #2b2b2b;
+ border: 1px solid #555;
+}
+.darkMode .dropdownButton:hover {
+ background: #3a3a3a;
+}
+
+.darkMode .dropdownMenu {
+ background: #2c2c2c;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
+}
+
+.darkMode .heading {
+ color: #eee;
+}
+
.darkMode .dropdownMenu {
background-color: #1c2541; /* --color-space-cadet */
box-shadow: 2px 2px 4px 1px black;
@@ -88,3 +169,148 @@
margin: 0 -24px;
padding: 0 24px;
}
+
+/* ===================== TREND / TIMELINE STYLES ===================== */
+.chartWrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 450px;
+}
+
+.legendWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 40px;
+ padding-bottom: 16px;
+ margin-bottom: 12px;
+}
+
+.legendItem {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 500;
+ color: #444;
+}
+
+.legendSquare {
+ width: 14px;
+ height: 14px;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.trendSection {
+ margin-top: 28px;
+ padding: 18px;
+ background: #fafafa;
+ border-radius: 8px;
+ border: 1px solid #e8e8e8;
+}
+
+.darkMode .trendSection {
+ background: #252525;
+ border-color: #404040;
+}
+
+.trendTitle {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #1a1a1a;
+}
+
+.darkMode .trendTitle {
+ color: #ddd;
+}
+
+.trendGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
+}
+
+.trendCard {
+ background: #fff;
+ border: 1px solid #e0e0e0;
+ border-radius: 6px;
+ padding: 12px;
+}
+
+.darkMode .trendCard {
+ background: #333;
+ border-color: #555;
+}
+
+.projectName {
+ font-weight: 600;
+ font-size: 14px;
+ color: #1a1a1a;
+ margin-bottom: 8px;
+}
+
+.darkMode .projectName {
+ color: #eee;
+}
+
+.trendRow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ margin-bottom: 8px;
+}
+
+.trendRow span:first-child { flex: 1; color: #666; font-weight: 500; }
+.darkMode .trendRow span:first-child { color: #aaa; }
+.trendRow span:last-child { font-weight: 600; color: #1a1a1a; min-width: 50px; text-align: right; }
+.darkMode .trendRow span:last-child { color: #ddd; }
+
+.trendIndicator { font-size: 16px; font-weight: 700; display: inline-block; width: 24px; text-align: center; }
+
+.attributeRow {
+ display: flex;
+ gap: 8px;
+ font-size: 12px;
+ margin-bottom: 6px;
+ padding-top: 6px;
+ border-top: 1px solid #e8e8e8;
+}
+
+.darkMode .attributeRow {
+ border-top-color: #404040;
+}
+
+.attributeRow:first-of-type {
+ border-top: none;
+ margin-top: 8px;
+ padding-top: 0;
+}
+
+.attributeRow .label {
+ flex: 1;
+ color: #666;
+ font-weight: 500;
+}
+
+.darkMode .attributeRow .label {
+ color: #aaa;
+}
+
+.attributeRow .value {
+ font-weight: 600;
+ color: #1a1a1a;
+ min-width: 60px;
+ text-align: right;
+}
+
+.darkMode .attributeRow .value {
+ color: #ddd;
+}
+
+.timelineSection { margin-top: 28px; padding: 18px; background: #fafafa; border-radius: 8px; border: 1px solid #e8e8e8; }
+.darkMode .timelineSection { background: #252525; border-color: #404040; }
+