Skip to content
43 changes: 43 additions & 0 deletions src/actions/bmdashboard/issueGraphActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import axios from 'axios';
import {
FETCH_ISSUES_SUMMARY_REQUEST,
FETCH_ISSUES_SUMMARY_SUCCESS,
FETCH_ISSUES_SUMMARY_FAILURE,
FETCH_ISSUES_TREND_REQUEST,
FETCH_ISSUES_TREND_SUCCESS,
FETCH_ISSUES_TREND_FAILURE,
} from '../../constants/bmdashboard/issueGraphConstants';
import { ENDPOINTS } from '../../utils/URL';

export const fetchIssueSummary = (params = {}) => async dispatch => {
try{
dispatch({ type: FETCH_ISSUES_SUMMARY_REQUEST });
const { data } = await axios.get(ENDPOINTS.BM_ISSUES_BARGRAPH_SUMMARY, { params });
const normalizedData = data.data ? {
total: data.data.totalIssues,
newThisWeek: data.data.newIssues,
resolved: data.data.resolvedIssues,
avgResolution: data.data.averageResolutionTimeDays,
} : {};
dispatch({ type: FETCH_ISSUES_SUMMARY_SUCCESS, payload: normalizedData });
}catch(error){
dispatch({
type: FETCH_ISSUES_SUMMARY_FAILURE,
payload: error.message || 'Failed to fetch issue summary',
});
}
}

export const fetchIssueTrend = (params = {}) => async dispatch => {
try{
dispatch({ type: FETCH_ISSUES_TREND_REQUEST });
const { data } = await axios.get(ENDPOINTS.BM_ISSUES_BARGRAPH_TREND, { params });
const trendData = data.data || [];
dispatch({ type: FETCH_ISSUES_TREND_SUCCESS, payload: trendData });
}catch(error){
dispatch({
type: FETCH_ISSUES_TREND_FAILURE,
payload: error.message || 'Failed to fetch issue trend',
});
}
};
195 changes: 195 additions & 0 deletions src/components/BMDashboard/Issues/IssueGraph.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styles from './IssueGraph.module.css';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
LabelList,
} from 'recharts';
import { fetchIssueSummary, fetchIssueTrend } from '../../../actions/bmdashboard/issueGraphActions';

function IssueGraph() {
const dispatch = useDispatch();
const darkMode = useSelector(state => state.theme.darkMode);
const { loading, issueSummary, issueTrend, error } = useSelector(state => state.issueGraph);

const [weeks, setWeeks] = useState(8);
const [graphData, setGraphData] = useState([]);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');

const today = new Date();
const formattedDate = date => date.toISOString().split('T')[0];
const maxEndDate = formattedDate(today);
const minStartDate = formattedDate(new Date(today.getTime() - 12 * 7 * 24 * 60 * 60 * 1000));
const maxStartDate = endDate ? endDate : maxEndDate;

Check warning on line 31 in src/components/BMDashboard/Issues/IssueGraph.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unnecessary use of conditional expression for default assignment.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ3nIHAOcLcG3PUCVb1X&open=AZ3nIHAOcLcG3PUCVb1X&pullRequest=4272
const minEndDate = startDate ? startDate : minStartDate;

Check warning on line 32 in src/components/BMDashboard/Issues/IssueGraph.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unnecessary use of conditional expression for default assignment.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ3nIHAOcLcG3PUCVb1Y&open=AZ3nIHAOcLcG3PUCVb1Y&pullRequest=4272

useEffect(() => {
dispatch(fetchIssueSummary({ weeks }));
dispatch(fetchIssueTrend({ weeks }));
}, [dispatch, weeks]);

useEffect(() => {
if (issueTrend && Array.isArray(issueTrend)) {
const sortedData = [...issueTrend].sort((a, b) => new Date(a.week) - new Date(b.week));
setGraphData(sortedData);
}
}, [issueTrend]);

const handleWeeksChange = e => {
const val = Number(e.target.value);
setWeeks(val);
setStartDate('');
setEndDate('');
};

const handleGoClick = () => {
if (!startDate || !endDate) return;
if (new Date(startDate) > new Date(endDate)) {
alert('Start date must be before end date');
return;
}
dispatch(fetchIssueTrend({ start: startDate, end: endDate }));
dispatch(fetchIssueSummary({ start: startDate, end: endDate }));
};

return (
<div className={`${styles.issueGraphPage} ${darkMode ? styles.darkMode : ''}`}>
<div className={styles.issueGraphEventContainer}>
<div className={styles.filterRow}>
<div className={styles.filterGroup}>
<label htmlFor="start-date">Start Date:</label>
<input
id="start-date"
type="date"
value={startDate}
onChange={e => setStartDate(e.target.value)}
min={minStartDate}
max={maxStartDate}
/>
</div>

<div className={styles.filterGroup}>
<label htmlFor="end-date">End Date:</label>
<div className={styles.inputWithButton}>
<input
id="end-date"
type="date"
value={endDate}
onChange={e => setEndDate(e.target.value)}
min={minEndDate}
max={maxEndDate}
/>
<button className={styles.goButton} onClick={handleGoClick}>
Go
</button>
</div>
</div>

<div className={styles.filterGroup}>
<label htmlFor="weeks-select">Weeks:</label>
<select id="weeks-select" value={weeks} onChange={handleWeeksChange}>
<option value={4}>Last 4 Weeks</option>
<option value={8}>Last 8 Weeks</option>
<option value={12}>Last 12 Weeks</option>
</select>
</div>
</div>

{startDate && endDate && new Date(startDate) > new Date(endDate) && (
<p style={{ color: 'red' }}>Start date cannot be after end date.</p>
)}
{/* issue tiles */}
{issueSummary && (
<div className={styles.tileRow}>
<div className={styles.tile}>
<h3>Total Issues</h3>
<p>{issueSummary.total}</p>
</div>
<div className={styles.tile}>
<h3>New Issues This Week</h3>
<p>{issueSummary.newThisWeek}</p>
</div>
<div className={styles.tile}>
<h3>Resolved Issues</h3>
<p>{issueSummary.resolved}</p>
</div>
<div className={styles.tile}>
<h3>Avg. Resolution Time</h3>
<p>{issueSummary.avgResolution} days</p>
</div>
</div>
)}
{/* charts */}
<div className={styles.graphWrapper}>
<h2>Issues Created vs. Resolved</h2>
{loading && <p>Loading...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
{graphData.length > 0 && (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={graphData} margin={{ top: 20, right: 20, left: 0, bottom: 30 }}>
<CartesianGrid strokeDasharray="3 3" stroke={darkMode ? '#3a4a5a' : '#e0e0e0'} />

<XAxis dataKey="week" tick={{ fill: darkMode ? '#ffffff' : '#666' }} />

<YAxis tick={{ fill: darkMode ? '#ffffff' : '#666' }} />

<Tooltip
cursor={{
fill: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.05)',
}}
contentStyle={{
backgroundColor: darkMode ? '#253342' : '#fff',
border: '1px solid #555',
color: darkMode ? '#fff' : '#000',
}}
labelStyle={{
color: darkMode ? '#fff' : '#000',
}}
itemStyle={{
color: darkMode ? '#fff' : '#000',
}}
/>

<Legend verticalAlign="bottom" height={36} />

<Bar
dataKey="created"
fill={darkMode ? '#4fc3f7' : '#007bff'}
name="Created Issues"
>
<LabelList
dataKey="created"
position="top"
fill={darkMode ? '#ffffff' : '#000000'}
/>
</Bar>

<Bar
dataKey="resolved"
fill={darkMode ? '#81c784' : '#28a745'}
name="Resolved Issues"
>
<LabelList
dataKey="resolved"
position="top"
fill={darkMode ? '#ffffff' : '#000000'}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
)}
</div>
</div>
</div>
);
}

export default IssueGraph;
Loading
Loading