diff --git a/src/actions/bmdashboard/issueGraphActions.js b/src/actions/bmdashboard/issueGraphActions.js
new file mode 100644
index 0000000000..4c67fdc146
--- /dev/null
+++ b/src/actions/bmdashboard/issueGraphActions.js
@@ -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',
+ });
+ }
+};
\ No newline at end of file
diff --git a/src/components/BMDashboard/Issues/IssueGraph.jsx b/src/components/BMDashboard/Issues/IssueGraph.jsx
new file mode 100644
index 0000000000..354f5562ff
--- /dev/null
+++ b/src/components/BMDashboard/Issues/IssueGraph.jsx
@@ -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;
+ const minEndDate = startDate ? startDate : minStartDate;
+
+ 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 (
+
+
+
+
+
+ setStartDate(e.target.value)}
+ min={minStartDate}
+ max={maxStartDate}
+ />
+
+
+
+
+
+ setEndDate(e.target.value)}
+ min={minEndDate}
+ max={maxEndDate}
+ />
+
+
+
+
+
+
+
+
+
+
+ {startDate && endDate && new Date(startDate) > new Date(endDate) && (
+
Start date cannot be after end date.
+ )}
+ {/* issue tiles */}
+ {issueSummary && (
+
+
+
Total Issues
+
{issueSummary.total}
+
+
+
New Issues This Week
+
{issueSummary.newThisWeek}
+
+
+
Resolved Issues
+
{issueSummary.resolved}
+
+
+
Avg. Resolution Time
+
{issueSummary.avgResolution} days
+
+
+ )}
+ {/* charts */}
+
+
Issues Created vs. Resolved
+ {loading &&
Loading...
}
+ {error &&
{error}
}
+ {graphData.length > 0 && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+}
+
+export default IssueGraph;
diff --git a/src/components/BMDashboard/Issues/IssueGraph.module.css b/src/components/BMDashboard/Issues/IssueGraph.module.css
new file mode 100644
index 0000000000..23b8ec42a0
--- /dev/null
+++ b/src/components/BMDashboard/Issues/IssueGraph.module.css
@@ -0,0 +1,214 @@
+.issueGraphPage {
+ min-height: 100vh;
+ width: 100%;
+ background-color: #f0f4f8;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 40px 0;
+ box-sizing: border-box;
+ overflow-y: auto;
+}
+
+.issueGraphEventContainer {
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 24px;
+ padding: 24px 24px 40px 24px;
+ background: #f9f9f9;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ box-sizing: border-box;
+ max-height: calc(100vh - 80px);
+ overflow: auto;
+}
+
+.graphWrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: stretch;
+ height: 420px;
+ min-height: 300px;
+ padding: 20px;
+}
+
+.graphWrapper .recharts-responsive-container {
+ width: 100%;
+ height: 100%;
+}
+
+
+.filterRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ background: #fff;
+ padding: 16px 20px;
+ border-radius: 8px;
+ box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
+ margin-bottom: 5px;
+ flex-wrap: wrap;
+ gap: 20px;
+}
+
+.filterGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-width: 200px;
+ flex: 1;
+}
+
+.filterRow input,
+.filterRow select {
+ padding: 7px 10px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ font-size: 0.9rem;
+ height: 38px;
+ box-sizing: border-box;
+ width: 100%;
+ appearance: none;
+}
+
+.goButton {
+ background-color: #007bff;
+ color: white;
+ padding: 7px 14px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ height: 38px;
+ transition: background-color 0.2s ease;
+ white-space: nowrap;
+}
+
+.inputWithButton {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.goButton:hover {
+ background-color: #0056b3;
+}
+
+.goButton:focus {
+ outline: 2px solid #0056b3;
+ outline-offset: 2px;
+}
+
+.tileRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: stretch;
+ gap: 20px;
+ margin-bottom: 5px;
+ flex-wrap: nowrap;
+ padding: 15px 30px;
+}
+
+.tile {
+ flex: 1;
+ background: #fff;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
+ text-align: center;
+ min-width: 0;
+}
+
+.tile h3 {
+ margin-bottom: 10px;
+ font-size: 18px;
+ color: #333;
+}
+
+.tile p {
+ font-size: 24px;
+ font-weight: bold;
+ color: #000;
+}
+
+@media (max-width: 768px) {
+ .tileRow {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ }
+
+ .tile {
+ min-width: 250px;
+ flex: 0 0 auto;
+ }
+}
+
+/* dark mode */
+.darkMode {
+ background-color: #1b2a41;
+ color: #f5f5f5;
+}
+.darkMode .issueGraphPage {
+ background-color: #1b2a41;
+ color: #f5f5f5;
+}
+.darkMode .issueGraphEventContainer {
+ background: #253342;
+ color: #f5f5f5;
+}
+.darkMode .filterRow,
+.darkMode .tileRow,
+.darkMode .graphWrapper {
+ background-color: #1b2a41;
+ color: #f5f5f5;
+}
+
+/* Filter groups */
+.darkMode .filterGroup label {
+ color: #f5f5f5;
+}
+.darkMode .filterRow label {
+ background-color: transparent;
+ color: #f5f5f5;
+ border: none;
+}
+.darkMode input[type='date'],
+.darkMode select,
+.darkMode .goButton {
+ background-color: #253342;
+ color: #f5f5f5;
+ border: 1px solid #555;
+}
+
+/* Tiles */
+.darkMode .tile {
+ background-color: #2b3e59;
+ color: #f5f5f5;
+ border: 1px solid #30b8f8;
+}
+.darkMode .tile h3 {
+ color: #ffffff;
+}
+.darkMode .graphWrapper h2 {
+ color: #ffffff;
+}
+
+.darkMode .recharts-cartesian-axis-tick-value {
+ fill: #ffffff;
+}
+.darkMode .recharts-legend-item-text {
+ fill: #ffffff;
+}
+
+.darkMode .goButton :hover {
+ background-color: #1a73e8;
+}
+.darkMode .goButton :focus {
+ outline: 2px solid #1a73e8;
+ outline-offset: 2px;
+}
\ No newline at end of file
diff --git a/src/constants/bmdashboard/issueGraphConstants.js b/src/constants/bmdashboard/issueGraphConstants.js
new file mode 100644
index 0000000000..bb4f41b1ce
--- /dev/null
+++ b/src/constants/bmdashboard/issueGraphConstants.js
@@ -0,0 +1,7 @@
+export const FETCH_ISSUES_SUMMARY_REQUEST = 'FETCH_ISSUES_SUMMARY_REQUEST';
+export const FETCH_ISSUES_SUMMARY_SUCCESS = 'FETCH_ISSUES_SUMMARY_SUCCESS';
+export const FETCH_ISSUES_SUMMARY_FAILURE = 'FETCH_ISSUES_SUMMARY_FAILURE';
+
+export const FETCH_ISSUES_TREND_REQUEST = 'FETCH_ISSUES_TREND_REQUEST';
+export const FETCH_ISSUES_TREND_SUCCESS = 'FETCH_ISSUES_TREND_SUCCESS';
+export const FETCH_ISSUES_TREND_FAILURE = 'FETCH_ISSUES_TREND_FAILURE';
diff --git a/src/reducers/bmdashboard/issueGraphReducer.js b/src/reducers/bmdashboard/issueGraphReducer.js
new file mode 100644
index 0000000000..05086c7da6
--- /dev/null
+++ b/src/reducers/bmdashboard/issueGraphReducer.js
@@ -0,0 +1,36 @@
+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';
+
+const initialState = {
+ loading: false,
+ issueSummary: null,
+ issueTrend: null,
+ error: null,
+};
+
+const issueGraphReducer = (state = initialState, action) => {
+ switch (action.type) {
+ case FETCH_ISSUES_SUMMARY_REQUEST:
+ return { ...state, loading: true, error: null };
+ case FETCH_ISSUES_TREND_REQUEST:
+ return { ...state, loading: true, error: null };
+ case FETCH_ISSUES_SUMMARY_SUCCESS:
+ return { ...state, loading: false, issueSummary: action.payload };
+ case FETCH_ISSUES_TREND_SUCCESS:
+ return { ...state, loading: false, issueTrend: action.payload };
+ case FETCH_ISSUES_SUMMARY_FAILURE:
+ return { ...state, loading: false, error: action.payload };
+ case FETCH_ISSUES_TREND_FAILURE:
+ return { ...state, loading: false, error: action.payload };
+ default:
+ return state;
+ }
+};
+
+export default issueGraphReducer;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 484037ee36..92f007e027 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -63,6 +63,7 @@ import { equipmentReducer } from './bmdashboard/equipmentReducer';
import { bmProjectMemberReducer } from './bmdashboard/projectMemberReducer';
import { bmTimeLoggerReducer } from './bmdashboard/timeLoggerReducer';
import bmInjuryReducer from './bmdashboard/injuryReducer';
+import issueGraphReducer from './bmdashboard/issueGraphReducer';
import dashboardReducer from './dashboardReducer';
import { timeOffRequestsReducer } from './timeOffRequestReducer';
@@ -172,7 +173,7 @@ const localReducers = {
// lbdashboard
wishlistItem: wishListReducer,
-
+ issueGraph: issueGraphReducer,
bmissuechart: issueReducer,
noShowViz: noShowVizReducer,
eventFeedback: eventFeedbackReducer,
diff --git a/src/routes.jsx b/src/routes.jsx
index 850a4eb1e9..3255d42ebc 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -52,6 +52,7 @@ import IssueChart from './components/BMDashboard/Issues/issueCharts';
import BMTimeLogger from './components/BMDashboard/BMTimeLogger/BMTimeLogger';
import AddTeamMember from './components/BMDashboard/AddTeamMember/AddTeamMember';
import AnalyticsDashboard from './components/JobCCDashboard/JobAnalytics/JobAnalytics.jsx';
+import IssueGraph from './components/BMDashboard/Issues/IssueGraph';
import FaqSearch from './components/Faq/FaqSearch';
import FaqManagement from './components/Faq/FaqManagement';
import FaqHistory from './components/Faq/FaqHistory';
@@ -794,6 +795,7 @@ export default (
/>
+
diff --git a/src/utils/URL.js b/src/utils/URL.js
index 1d768d34e3..601c718418 100644
--- a/src/utils/URL.js
+++ b/src/utils/URL.js
@@ -404,6 +404,8 @@ export const ENDPOINTS = {
BM_INJURY_ISSUE: `${APIEndpoint}/bm/issues`,
BM_INJURY_SEVERITY: `${APIEndpoint}/bm/injuries/severity-by-project`,
BM_RENTAL_CHART: `${APIEndpoint}/bm/rentalChart`,
+ BM_ISSUES_BARGRAPH_SUMMARY: `${APIEndpoint}/issues/summary`,
+ BM_ISSUES_BARGRAPH_TREND: `${APIEndpoint}/issues/trends`,
BM_TOOLS_RETURNED_LATE: `${APIEndpoint}/bm/tools/returned-late`,
BM_TOOLS_RETURNED_LATE_PROJECTS: `${APIEndpoint}/bm/tools/returned-late/projects`,