diff --git a/src/components/BMDashboard/Issues/openIssueCharts.jsx b/src/components/BMDashboard/Issues/openIssueCharts.jsx index c6a3274ec4..d4072daed5 100644 --- a/src/components/BMDashboard/Issues/openIssueCharts.jsx +++ b/src/components/BMDashboard/Issues/openIssueCharts.jsx @@ -1,11 +1,10 @@ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef, useMemo } from 'react'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import { useDispatch, useSelector } from 'react-redux'; import { BarChart, Bar, - Cell, XAxis, YAxis, CartesianGrid, @@ -90,39 +89,11 @@ const createSelectStyles = (isDark, textColor) => ({ }), }); -// Deterministic, collision-resistant color per projectId -const getStableProjectColor = projectId => { - if (!projectId) return '#94a3b8'; - - let hash = 0; - for (let i = 0; i < projectId.length; i++) { - hash = projectId.charCodeAt(i) + ((hash << 5) - hash); - } - - const hue = Math.abs(hash) % 360; - const saturation = 65; - const lightness = 50; - - return `hsl(${hue}, ${saturation}%, ${lightness}%)`; -}; - -const getProjectColorMap = issues => { - const map = {}; - (issues || []).forEach(issue => { - (issue.projects || []).forEach(p => { - if (p?.projectId && !map[p.projectId]) { - map[p.projectId] = getStableProjectColor(p.projectId); - } - }); - }); - return map; -}; - /* --------------------------- component --------------------------- */ function IssueCharts() { const dispatch = useDispatch(); - const darkMode = useSelector(state => state.theme.darkMode); + const darkMode = useSelector(state => state.theme?.darkMode); const { issues, loading, error, selectedProjects } = useSelector(state => state.bmissuechart); const projects = useSelector(state => state.bmProjects); @@ -155,6 +126,86 @@ function IssueCharts() { padding: '20px', }; + // Normalize issues for chart + // Number issues per project to avoid conflicts when multiple projects are selected + // Prefix with project name when multiple projects are selected to distinguish them + const normalizedIssues = useMemo(() => { + if (!issues || issues.length === 0) return []; + + // Check if multiple projects are selected + const uniqueProjectIds = new Set((issues || []).map(item => item.projectId).filter(Boolean)); + const multipleProjects = uniqueProjectIds.size > 1; + + // Group issues by projectId + const issuesByProject = new Map(); + (issues || []).forEach(item => { + const projectId = item.projectId || 'unknown'; + if (!issuesByProject.has(projectId)) { + issuesByProject.set(projectId, []); + } + issuesByProject.get(projectId).push(item); + }); + + // Create per-project numbering maps + const projectNumberMaps = new Map(); + + issuesByProject.forEach((projectIssues, projectId) => { + const issueIdToNumber = new Map(); + let counter = 1; + + // Sort issues by issueId within each project for consistent ordering + const sortedById = [...projectIssues].sort((a, b) => { + const idA = a.issueId || ''; + const idB = b.issueId || ''; + return idA.localeCompare(idB); + }); + + // Assign numbers to issues without names within this project + sortedById.forEach(item => { + if (!item.issueName && item.issueId && !issueIdToNumber.has(item.issueId)) { + issueIdToNumber.set(item.issueId, counter++); + } + }); + + projectNumberMaps.set(projectId, issueIdToNumber); + }); + + // Map back to original order (sorted by duration) but use per-project numbers + return (issues || []).map(item => { + const projectId = item.projectId || 'unknown'; + const projectNumberMap = projectNumberMaps.get(projectId); + + // Generate the base issue name + let baseIssueName = item.issueName; + let isGeneratedName = false; + + if (!baseIssueName) { + // If no name, generate Issue #X based on project numbering + if (item.issueId && projectNumberMap?.has(item.issueId)) { + baseIssueName = `Issue #${projectNumberMap.get(item.issueId)}`; + isGeneratedName = true; + } else { + baseIssueName = 'Untitled Issue'; + isGeneratedName = true; + } + } + + // Only prefix with project name if: + // 1. Multiple projects are selected AND + // 2. The issue name was generated (Issue #X), not a real name + // This keeps the display clean for named issues while distinguishing unnamed issues + const finalIssueName = + multipleProjects && isGeneratedName && item.projectName + ? `${item.projectName} - ${baseIssueName}` + : baseIssueName; + + return { + issueName: finalIssueName, + durationOpen: item.durationOpen ?? 0, + }; + }); + }, [issues]); + useEffect(() => { dispatch(fetchBMProjects()); }, [dispatch]); @@ -176,57 +227,6 @@ function IssueCharts() { dispatch(fetchLongestOpenIssues(dateRange, selectedProjects)); }, [dispatch, startDate, endDate, selectedProjects]); - const normalizedIssues = (issues || []).map(issue => { - if (Array.isArray(issue.projects) && issue.projects.length > 0) { - return issue; - } - // fallback when backend does not send projects - return { - ...issue, - projects: [ - { - projectId: 'unknown', - projectName: 'Unknown Project', - durationOpen: issue.durationOpen, - }, - ], - }; - }); - - // Step 1: Normalize missing issue names - const safeIssues = normalizedIssues.map(issue => { - const name = issue.issueName; - return { - ...issue, - issueName: - typeof name === 'string' && name.trim() && name !== 'undefined' - ? name.trim() - : 'Unknown Issue', - }; - }); - - // Step 2: Use safeIssues for chartData - const chartData = safeIssues.flatMap(issue => - (issue.projects || []).map(project => ({ - issueName: issue.issueName, - projectId: project.projectId, - durationOpen: project.durationOpen, - })), - ); - - // Step 3: Stable project color map and legend - const projectColorMap = getProjectColorMap(safeIssues); - - const projectLegend = Object.entries(projectColorMap).map(([projectId, color]) => { - const project = safeIssues.flatMap(i => i.projects || []).find(p => p.projectId === projectId); - - return { - projectId, - projectName: project?.projectName || 'Unknown Project', - color, - }; - }); - useEffect(() => { function handleResize() { if (chartContainerRef.current) { @@ -260,7 +260,66 @@ function IssueCharts() { /* ------------ decide what to show inside chart container ------------ */ - // (chartContent block removed; chart rendering is below in render) + let chartContent; + + if (error) { + chartContent =
There are currently no open issues matching your selected criteria.
+Try adjusting your date range or project filters to see more results.
+There are currently no open issues matching your selected criteria.
-Try adjusting your date range or project filters to see more results.
-