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 =
Error: {error}
; + } else if (loading) { + chartContent =
Loading chart data...
; + } else if (!normalizedIssues || normalizedIssues.length === 0) { + chartContent = ( +
+
+

No Open Issues Found

+

There are currently no open issues matching your selected criteria.

+

Try adjusting your date range or project filters to see more results.

+
+
+ ); + } else { + chartContent = ( + + + + + + `${value} months`} + labelFormatter={label => `Issue : ${label}`} + cursor={{ fill: hoverBg, opacity: 0.8 }} + /> + + `${v} months`} + style={{ fill: textColor }} + /> + + + + ); + } /* --------------------------- render --------------------------- */ @@ -373,79 +432,7 @@ function IssueCharts() {
- {/* Step 6: Project Legend above the chart */} -
- {projectLegend.map(p => ( -
- - {p.projectName} -
- ))} -
- {!issues || issues.length === 0 ? ( -
-
-

No Open Issues Found

-

There are currently no open issues matching your selected criteria.

-

Try adjusting your date range or project filters to see more results.

-
-
- ) : ( - - - - - - `${value} months`} - labelFormatter={label => `Issue: ${label}`} - /> - - {chartData.map((entry, index) => ( - - ))} - - - - )} + {chartContent}
); diff --git a/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css b/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css index 09e5a10631..c487d567e3 100644 --- a/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css +++ b/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css @@ -143,6 +143,19 @@ .reactCalendar :global(.react-calendar__month-view__weekdays__weekday) abbr { text-decoration: none !important; + color: #000 !important; +} + +.reactCalendar :global(.react-calendar__month-view__weekdays__weekday) { + color: #000 !important; +} + +.reactCalendarDarkMode :global(.react-calendar__month-view__weekdays__weekday) { + color: #FFF !important; +} + +.reactCalendarDarkMode :global(.react-calendar__month-view__weekdays__weekday) abbr { + color: #FFF !important; } .reactCalendar :global(.react-calendar__month-view__days__day) { @@ -152,10 +165,16 @@ box-sizing: border-box; vertical-align: top; padding: 2px; + color: #000 !important; +} + +.reactCalendarDarkMode :global(.react-calendar__month-view__days__day) { + color: #FFF !important; + border: 1px solid #666 !important; } .reactCalendar :global(.react-calendar__navigation) { - background-color: #f9f9f9; + background-color: #f9f9f9 !important; border-bottom: 1px solid #ddd; margin-bottom: 0 !important; } @@ -169,7 +188,7 @@ overflow: hidden !important; position: relative; padding: 4px; - color: #1a202c; + color: #000 !important; } .reactCalendarDarkMode :global(.react-calendar__tile) { @@ -182,8 +201,13 @@ } .reactCalendar :global(.react-calendar__tile):hover { - background-color: #f5f5f5; - border-color: #999; + background-color: #f5f5f5 !important; + border-color: #999 !important; +} + +.reactCalendarDarkMode :global(.react-calendar__tile):hover { + background-color: #3A506B !important; + border-color: #666 !important; } /* Prevent hover from overriding selected date styling - light mode */ @@ -216,11 +240,6 @@ filter: brightness(1.8) contrast(1.5) !important; } -.reactCalendarDarkMode :global(.react-calendar__tile):hover { - background-color: #2d3748; - border-color: #4a5568; -} - .reactCalendar :global(.react-calendar__tile) .tileEvents { display: flex; flex-direction: column; @@ -1278,14 +1297,6 @@ border-bottom-color: #4a5568; } -.reactCalendarDarkMode :global(.react-calendar__month-view__weekdays__weekday) { - color: #e2e8f0; -} - -.reactCalendarDarkMode :global(.react-calendar__month-view__weekdays__weekday) abbr { - color: #e2e8f0; -} - .selectedDate { box-shadow: inset 0 0 0 2px #3182ce; border-radius: 8px; @@ -1864,25 +1875,3 @@ .selectedEventTitleDark { color: #f7fafc; } - -.reactCalendar :global(.react-calendar__tile) .eventItem.statusHoliday { - background-color: #e7dfd2; - color: #5f5142; - padding: 4px 8px; - font-weight: 600; - border-radius: 6px; - border: 1px solid #d6cbbd; - width: fit-content; - max-width: 100%; -} - -.reactCalendarDarkMode :global(.react-calendar__tile) .eventItem.statusHoliday { - background-color: #5a4d40; - color: #f5eadf; - border: 1px solid #7a6a58; -} - -.selectedEventCard .statusHoliday { - background-color: #e8e1d8; - color: #5c4b3b; -} \ No newline at end of file