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; } +