Skip to content
80 changes: 80 additions & 0 deletions src/actions/bmdashboard/issueChartActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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',
});
}
};
65 changes: 65 additions & 0 deletions src/components/BMDashboard/Issues/IssueCharts.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
215 changes: 215 additions & 0 deletions src/components/BMDashboard/Issues/LongestOpenIssuesChart.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={darkMode ? styles.dark : ''}>
<h2>Issue Chart</h2>
<div className={styles.container}>
<div className={styles.dateInputs}>
<div className={styles.inputGroup}>
{/* <label htmlFor="startDate">Start:</label> */}

<DatePicker
selected={dateRange.start}
onChange={value => handleDateChange('start', value)}
className={darkMode ? styles.dateDark : ''}
/>
</div>
to
<div className={styles.inputGroup}>
{/* <label htmlFor="endDate">End:</label> */}

<DatePicker
selected={dateRange.end}
onChange={value => handleDateChange('end', value)}
className={darkMode ? styles.dateDark : ''}
/>
</div>
</div>
<div className={styles.inputGroup}>
{/* <label htmlFor="project">Project:</label> */}
<select
id="project"
value={selectedProject}
onChange={e => setSelectedProject(e.target.value)}
className={darkMode ? styles.selectDark : styles.select}
>
<option value="all">All Projects</option>
{bmProjects.map(project => (
<option key={project._id} value={project._id}>
{project.name}
</option>
))}
</select>
</div>
<div className={styles.inputGroup}>
{/* <label htmlFor="type">Type:</label> */}
<select
id="type"
value={graphType}
onChange={e => setGraphType(e.target.value)}
className={darkMode ? styles.selectDark : styles.select}
>
<option value="Longest Open">Longest Open</option>
<option value="Most Expensive">Most Expensive</option>
</select>
</div>
</div>
<div className={styles.chartContainer}>
{chartData.length > 0 ? (
<Bar
key={`${graphType}-${selectedProject}-${dateRange.start}-${dateRange.end}`}
data={data}
options={options}
plugins={[ChartDataLabels]}
height={300}
/>
) : (
<p className={styles.noData}>No issues found.</p>
)}
</div>
</div>
);
}

export default IssuesCharts;
Loading