diff --git a/src/actions/bmdashboard/issueChartActions.js b/src/actions/bmdashboard/issueChartActions.js index 961a2c41e9..0e7228c9aa 100644 --- a/src/actions/bmdashboard/issueChartActions.js +++ b/src/actions/bmdashboard/issueChartActions.js @@ -6,6 +6,12 @@ import { FETCH_ISSUE_TYPES_YEARS_REQUEST, FETCH_ISSUE_TYPES_YEARS_SUCCESS, FETCH_ISSUE_TYPES_YEARS_FAILURE, + FETCH_LONGEST_OPEN_ISSUES_REQUEST, + FETCH_LONGEST_OPEN_ISSUES_SUCCESS, + FETCH_LONGEST_OPEN_ISSUES_FAILURE, + FETCH_MOST_EXPENSIVE_ISSUES_REQUEST, + FETCH_MOST_EXPENSIVE_ISSUES_SUCCESS, + FETCH_MOST_EXPENSIVE_ISSUES_FAILURE } from '../../constants/bmdashboard/issueConstants'; import { ENDPOINTS } from '../../utils/URL'; // Import the endpoints @@ -52,3 +58,77 @@ export const fetchIssueTypesAndYears = () => async dispatch => { }); } }; + +const formatFilters = (filters) => { + const { projectIds, startDate, endDate } = filters; + const formatted = {}; + + // Only add projectIds if it's a non-empty string or non-empty array + if ( + (typeof projectIds === 'string' && projectIds.trim() !== '') || + (Array.isArray(projectIds) && projectIds.length > 0) + ) { + formatted.projectIds = Array.isArray(projectIds) + ? projectIds.join(',') + : projectIds.trim(); + } + + if (startDate !== undefined) { + formatted.startDate = startDate; + } + + if (endDate !== undefined) { + formatted.endDate = endDate; + } + return formatted; +}; + +export const fetchLongestOpenIssues = (filters) => async (dispatch) => { + try { + dispatch({ type: FETCH_LONGEST_OPEN_ISSUES_REQUEST }); + + const formattedFilters = formatFilters(filters); + + const response = await axios.get(ENDPOINTS.BM_LONGEST_OPEN_ISSUES, { params: formattedFilters }); +// console.log( +// "DEBUG - filters --> ",formattedFilters +// ) + if (!response.data) { + throw new Error('Empty response from server'); + } + + dispatch({ + type: FETCH_LONGEST_OPEN_ISSUES_SUCCESS, + payload: response.data + }); + } catch (error) { + dispatch({ + type: FETCH_LONGEST_OPEN_ISSUES_FAILURE, + payload: error.message || 'Failed to fetch longest open issues' + }); + } +}; + +export const fetchMostExpensiveIssues = (filters) => async (dispatch) => { + try { + dispatch({ type: FETCH_MOST_EXPENSIVE_ISSUES_REQUEST }); + + const formattedFilters = formatFilters(filters); + + const response = await axios.get(ENDPOINTS.BM_MOST_EXPENSIVE_ISSUES, { params: formattedFilters }); + + if (!response.data) { + throw new Error('Empty response from server'); + } + + dispatch({ + type: FETCH_MOST_EXPENSIVE_ISSUES_SUCCESS, + payload: response.data + }); + } catch (error) { + dispatch({ + type: FETCH_MOST_EXPENSIVE_ISSUES_FAILURE, + payload: error.message || 'Failed to fetch most expensive issues', + }); + } +}; diff --git a/src/components/BMDashboard/Issues/IssueCharts.module.css b/src/components/BMDashboard/Issues/IssueCharts.module.css new file mode 100644 index 0000000000..2f26dd4778 --- /dev/null +++ b/src/components/BMDashboard/Issues/IssueCharts.module.css @@ -0,0 +1,65 @@ +.dark { + background: #2b3e59; + color: white; +} +.dark h2 { + color: white +} +.container { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + flex-wrap: wrap; + align-items: center; + padding:1rem; +} + +.inputGroup { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.inputGroup label { + font-size: 0.75rem; + font-weight: 500; +} + +.inputGroup input, +.inputGroup select { + padding: 0.25rem 0.5rem; + border: 1px solid #ced4da; + border-radius: 0.25rem; + font-size: 0.75rem; +} + +.dateInputs { + display: flex; + gap: 0.5rem; +} + +.dateInputs input { + max-width: 120px; +} + +.noData { + text-align: center; + padding: 2rem; + color: #6c757d; + font-style: italic; +} + +.chartContainer { + width: 100%; + margin: 0; +} + +.dateDark { + background: #2b3e59; + color: white; +} + +.selectDark{ + background: #2b3e59; + color: white; +} diff --git a/src/components/BMDashboard/Issues/LongestOpenIssuesChart.jsx b/src/components/BMDashboard/Issues/LongestOpenIssuesChart.jsx new file mode 100644 index 0000000000..03c2e373c9 --- /dev/null +++ b/src/components/BMDashboard/Issues/LongestOpenIssuesChart.jsx @@ -0,0 +1,215 @@ +import { useState, useEffect, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { Bar } from 'react-chartjs-2'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; +import DatePicker from 'react-datepicker'; +import styles from './IssueCharts.module.css'; + +// Import Redux actions +import { + fetchLongestOpenIssues, + fetchMostExpensiveIssues, +} from '../../../actions/bmdashboard/issueChartActions'; + +function IssuesCharts({ bmProjects = [] }) { + const dispatch = useDispatch(); + + const [graphType, setGraphType] = useState('Longest Open'); + const [selectedProject, setSelectedProject] = useState('all'); + const [dateRange, setDateRange] = useState({ start: '', end: '' }); + + const { longestOpenIssues = [], mostExpensiveIssues = [] } = useSelector( + state => state.issue || {}, + ); + const darkMode = useSelector(state => state.theme.darkMode); + + const formatFilters = ({ projectIds, startDate, endDate }) => { + const formatted = {}; + if ( + (typeof projectIds === 'string' && projectIds.trim() !== '') || + (Array.isArray(projectIds) && projectIds.length > 0) + ) { + formatted.projectIds = Array.isArray(projectIds) ? projectIds.join(',') : projectIds.trim(); + } + if (startDate !== undefined && startDate !== '') { + formatted.startDate = startDate; + } + if (endDate !== undefined && endDate !== '') { + formatted.endDate = endDate; + } + return formatted; + }; + useEffect(() => { + const params = formatFilters({ + projectIds: selectedProject === 'all' ? undefined : selectedProject, + startDate: dateRange.start, + endDate: dateRange.end, + }); + + if (graphType === 'Longest Open') { + dispatch(fetchLongestOpenIssues(params)); + } else { + dispatch(fetchMostExpensiveIssues(params)); + } + }, [graphType, selectedProject, dateRange.start, dateRange.end, dispatch, darkMode]); + + const chartData = graphType === 'Longest Open' ? longestOpenIssues : mostExpensiveIssues; + + const data = { + labels: chartData.map(issue => issue.title || issue.IssueId), + datasets: [ + { + label: graphType === 'Longest Open' ? 'Days Open' : 'Total Cost ($)', + data: chartData.map(issue => + graphType === 'Longest Open' ? issue.daysOpen : issue.totalCost, + ), + backgroundColor: + graphType === 'Longest Open' ? 'rgba(54, 162, 235, 0.7)' : 'rgba(255, 99, 132, 0.7)', + borderColor: + graphType === 'Longest Open' ? 'rgba(54, 162, 235, 1)' : 'rgba(255, 99, 132, 1)', + borderWidth: 1, + }, + ], + }; + + const options = useMemo( + () => ({ + indexAxis: 'y', + responsive: true, + plugins: { + legend: { display: false }, + datalabels: { + anchor: 'end', + align: 'right', + formatter: value => (graphType === 'Longest Open' ? `${value} days` : `$${value}`), + color: darkMode ? '#fff' : '#000', + font: { weight: 'bold' }, + }, + title: { + display: true, + text: + graphType === 'Longest Open' + ? `Longest Open Issues${ + selectedProject !== 'all' + ? ` (${bmProjects.find(p => p._id === selectedProject)?.name || ''})` + : '' + }` + : `Most Expensive Issues${ + selectedProject !== 'all' + ? ` (${bmProjects.find(p => p._id === selectedProject)?.name || ''})` + : '' + }`, + font: { size: 12 }, + color: darkMode ? '#fff' : '#000', + }, + }, + scales: { + x: { + title: { + display: true, + text: graphType === 'Longest Open' ? 'Days Open' : 'Total Cost ($)', + font: { size: 12 }, + color: darkMode ? '#fff' : '#000', + }, + ticks: { + color: darkMode ? '#ccc' : '#333', + }, + }, + y: { + title: { + display: true, + text: 'Issue Title', + font: { size: 12 }, + color: darkMode ? '#fff' : '#000', + }, + ticks: { + color: darkMode ? '#ccc' : '#333', + }, + }, + }, + elements: { + bar: { + borderRadius: 4, + borderSkipped: false, + }, + }, + }), + [graphType, selectedProject, darkMode, bmProjects], + ); + + const handleDateChange = (dateName, dateValue) => { + setDateRange({ ...dateRange, [dateName]: dateValue }); + }; + + return ( +
No issues found.
+ )} +