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`,