diff --git a/src/actions/bmdashboard/issueChartActions.js b/src/actions/bmdashboard/issueChartActions.js index e5b005bf69..12f8b4ad4f 100644 --- a/src/actions/bmdashboard/issueChartActions.js +++ b/src/actions/bmdashboard/issueChartActions.js @@ -3,20 +3,23 @@ import { FETCH_ISSUES_BARCHART_REQUEST, FETCH_ISSUES_BARCHART_SUCCESS, FETCH_ISSUES_BARCHART_FAILURE, + 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, SET_DATE_FILTER, SET_PROJECT_FILTER, } from '../../constants/bmdashboard/issueConstants'; -import { ENDPOINTS, ApiEndpoint } from '../../utils/URL'; +import { ENDPOINTS } from '../../utils/URL'; -// eslint-disable-next-line import/prefer-default-export export const fetchIssues = () => async dispatch => { try { dispatch({ type: FETCH_ISSUES_BARCHART_REQUEST }); - - // Fetch all data without applying any filters const { data } = await axios.get(ENDPOINTS.BM_ISSUE_CHART); dispatch({ type: FETCH_ISSUES_BARCHART_SUCCESS, payload: data }); } catch (error) { @@ -27,34 +30,70 @@ export const fetchIssues = () => async dispatch => { } }; - -export const fetchLongestOpenIssues = (dates = [], projects = []) => async dispatch => { +export const fetchIssueTypesAndYears = () => async dispatch => { try { - dispatch({ type: FETCH_LONGEST_OPEN_ISSUES_REQUEST }); + dispatch({ type: FETCH_ISSUE_TYPES_YEARS_REQUEST }); + const { data } = await axios.get(ENDPOINTS.BM_ISSUE_CHART); + const issueTypes = [...new Set(data.map(item => item._id.issueType))]; + const years = [...new Set(data.map(item => item._id.issueYear))]; + dispatch({ type: FETCH_ISSUE_TYPES_YEARS_SUCCESS, payload: { issueTypes, years } }); + } catch (error) { + dispatch({ + type: FETCH_ISSUE_TYPES_YEARS_FAILURE, + payload: error.message || 'Failed to fetch issue types and years', + }); + } +}; - const params = new URLSearchParams(); - if (dates.length) params.append('dates', dates.join(',')); - if (projects.length) params.append('projects', projects.join(',')); +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; +}; - const response = await axios.get(`${ApiEndpoint}/bm/issues/longest-open?${params}`); - dispatch({ - type: FETCH_LONGEST_OPEN_ISSUES_SUCCESS, - payload: response.data, +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, }); + dispatch({ type: FETCH_LONGEST_OPEN_ISSUES_SUCCESS, payload: response.data }); } catch (error) { dispatch({ type: FETCH_LONGEST_OPEN_ISSUES_FAILURE, - payload: error.message, + 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, + }); + 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', }); } }; -export const setDateFilter = dates => ({ - type: SET_DATE_FILTER, - payload: dates, -}); +export const setDateFilter = dates => ({ type: SET_DATE_FILTER, payload: dates }); -export const setProjectFilter = projects => ({ - type: SET_PROJECT_FILTER, - payload: projects, -}); +export const setProjectFilter = projects => ({ type: SET_PROJECT_FILTER, payload: projects }); diff --git a/src/components/BMDashboard/Issues/IssueCharts.module.css b/src/components/BMDashboard/Issues/IssueCharts.module.css new file mode 100644 index 0000000000..d063acd937 --- /dev/null +++ b/src/components/BMDashboard/Issues/IssueCharts.module.css @@ -0,0 +1,444 @@ +/* stylelint-disable no-descending-specificity */ + +/* =========================== +Issue Charts Module CSS +=========================== */ + +/* ----- Longest/Most Expensive Bar Charts ----- */ +.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; +} + +.dateInputs { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.multiSelectWrapper { + min-width: 220px; + flex: 1; +} + +.noData { + text-align: center; + padding: 2rem; + color: #6c757d; + font-style: italic; +} + +.dateInput { + border: 1px solid #ced4da; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +.dateDark { + background: #2b3e59; + color: white; + border: 1px solid #4a5a72; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +/* Fix react-datepicker navigation arrow contrast */ +:global(.react-datepicker__navigation-icon::before) { + border-color: #555 !important; + border-width: 2px 2px 0 0 !important; +} + +:global(.react-datepicker__navigation:hover .react-datepicker__navigation-icon::before) { + border-color: #111 !important; +} + +/* Dark mode datepicker — scoped via calendarClassName prop */ +.darkCalendar { + background-color: #1e2d42 !important; + border-color: #4a5a72 !important; +} + +.darkCalendar :global(.react-datepicker__header) { + background-color: #243450 !important; + border-bottom-color: #4a5a72 !important; +} + +.darkCalendar :global(.react-datepicker__current-month), +.darkCalendar :global(.react-datepicker__day-name) { + color: #e0e6f0 !important; +} + +.darkCalendar :global(.react-datepicker__day) { + color: #ccd1dc !important; +} + +.darkCalendar :global(.react-datepicker__day:hover) { + background-color: #4a5a72 !important; + color: #fff !important; +} + +.darkCalendar :global(.react-datepicker__day--selected), +.darkCalendar :global(.react-datepicker__day--selected:hover) { + background-color: #1e40af !important; + color: #fff !important; +} + +.darkCalendar :global(.react-datepicker__day--today) { + background-color: #1e3a5f !important; + color: #fff !important; + font-weight: bold !important; +} + +.darkCalendar :global(.react-datepicker__day--outside-month) { + color: #6b7a8d !important; +} + +.darkCalendar :global(.react-datepicker__navigation-icon::before) { + border-color: #ccc !important; +} + +.select { + padding: 0.25rem 0.5rem; + border: 1px solid #ced4da; + border-radius: 0.25rem; + font-size: 0.75rem; +} + +.selectDark { + background: #2b3e59; + color: white; + border: 1px solid #4a5a72; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +/* ----- Containers ----- */ +.issueChartContainer, +.issueChartEventContainer { + width: 100%; + max-width: 100%; + margin: 0; + padding: 30px 40px; + background: #f9f9f9; + border-radius: 8px; + box-shadow: 0 4px 6px rgb(0 0 0 / 10%); + display: flex; /* match dark mode layout */ + flex-direction: column; /* match dark mode */ + align-items: center; /* center filters like dark mode */ +} + +.issueChartContainerDark, +.issueChartEventContainerDark { + width: 100%; + max-width: 100%; + margin: 0; + padding: 30px 40px; + background:#1B2A41; + border-radius: 0; + box-shadow: none; + color: #ccd1dc; + display: flex; + flex-direction: column; + align-items: center; +} + +/* ----- Titles ----- */ +.issueChartEventTitle { + text-align: center; + font-size: 24px; + color: #333; + margin-bottom: 20px; +} + +.issueChartEventTitleDark { + color: #e0e6f0; +} + +/* ----- Buttons ----- */ +.buttonGroup { + display: flex; + justify-content: center; + margin-bottom: 20px; +} + +.chartButton { + padding: 10px 20px; + margin: 0 10px; + border: 2px solid #888; + background-color: white; + color: #333; + font-size: 16px; + cursor: pointer; + border-radius: 4px; + transition: all 0.3s ease; +} + +.chartButtonDark { + border-color: #3d444d; + background-color: #22272e; + color: #cfd7e3; +} + +.chartButtonActive, +.chartButtonActiveDark { + background-color: #276228; + color: #fff; + border-color: #276228; +} + +/* ----- Chart Wrapper ----- */ +.chartWrapper { + width:100%; + padding: 20px; + background: #fff; + border-radius: 8px; + box-shadow: 0 4px 6px rgb(0 0 0 / 10%); +} + +.chartWrapperDark { + background: #1a1c20; + width:100%; + padding: 25px; + border-radius: 12px; + box-shadow: 0 2px 6px rgb(0 0 0 / 60%); +} + +/* ----- Labels & Select Controls ----- */ +.issueChartLabel { + width:100%; + font-size: 16px; + color: #555; + margin-right: 10px; +} + +.issueChartLabelDark { + color: #aab1bf; +} + +.selectContainer { + width:100%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; +} + +.issueChartSelect { + padding: 2px 10px; + font-size: 16px; + border-radius: 4px; + border: 1px solid #ccc; + background-color: #fff; + color: #333; + cursor: pointer; + transition: border-color 0.3s ease; + outline: none; + margin-bottom: 20px; +} + +.issueChartSelectDark { + background-color: #22272e; + border: 1px solid #3d444d; + color: #cfd7e3; +} + +.filterSelect, +.issueChartSelect { + height: 60px; + padding: 8px 12px; + font-size: 15px; + border-radius: 6px; + border: 1px solid #cbd5e0; + background-color: #fff; + color: #2d3748; + width: 100%; + box-sizing: border-box; +} + +.filterSelectDark, +.issueChartSelectDark { + background-color: #2d3748; + border: 1px solid #4a5568; + color: #edf2f7; +} + + +.issueChartSelect:focus, +.issueChartSelectDark:focus { + border-color: #4caf50; +} + +/* ----- Filters ----- */ +.filtersContainer { + display: flex; + gap: 45px; + margin-top: 10px; + margin-bottom: 25px; + flex-wrap: wrap; +} + +.filter { + display: flex; + flex-direction: column; + gap: 8px; +} + +.dateRangePicker { + display: flex; + align-items: center; + gap: 8px; +} + +.projectFilter{ + width: 220px; +} + +.filterSelect { + padding: 6px 8px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #fff; + color: #333; +} + +.filterSelectDark { + background-color: #22272e; + border: 1px solid #3d444d; + color: #cfd7e3; +} + +/* fixed width for each DatePicker wrapper div from react-datepicker */ +.dateRangePickerStart :global(.react-datepicker-wrapper), +.dateRangePickerEnd :global(.react-datepicker-wrapper) { + display: inline-block; + width: 220px; /* tweak width if needed */ +} + +/* inner container full width */ +.dateRangePickerStart :global(.react-datepicker__input-container), +.dateRangePickerEnd :global(.react-datepicker__input-container) { + width: 100%; +} + +/* actual input fills wrapper and doesn’t change layout */ +.dateRangePickerStart :global(.react-datepicker__input-container input), +.dateRangePickerEnd :global(.react-datepicker__input-container input) { + width: 100%; + box-sizing: border-box; + height: 38px; +} + +.filterSelect:focus, +.issueChartSelect:focus { + border-color: #4caf50; + outline: none; +} + +/* ---------- Chart container ---------- */ + +/* ----- Chart Container ----- */ +.chartContainer { + margin: 20px; + width: 100%; + height: 400px; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); + color: #000; +} + +.chartContainerDark { + width: 100%; + max-width: 900px; /* centers the chart while panel stays 100% */ + background: #1a1c20; + padding: 25px; + border-radius: 12px; + margin: 0 auto; + box-shadow: 0 2px 6px rgb(0 0 0 / 60%); + color: #cfd7e3; +} + +/* ----- No Data Messages ----- */ +.noDataMessage, +.noDataContent { + text-align: center; +} + +.noDataMessage { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + color: #666; +} + +.noDataMessageDark { + background: #1e1e1e; + color: #999; +} + +.noDataContent { + padding: 40px 20px; + max-width: 400px; +} + +.noDataContent h3 { + color: #666; + font-size: 20px; + margin-bottom: 16px; + font-weight: 500; +} + +.noDataContentDark h3 { + color: #ccc; +} + +.noDataContent p { + color: #888; + font-size: 14px; + line-height: 1.5; + margin-bottom: 8px; +} + +.noDataContentDark p { + color: #aaa; +} + +/* ----- Responsive ----- */ +@media (width <= 768px) { + .chartContainer { + height: 250px; + padding: 10px; + } + + .filtersContainer { + flex-direction: column; + } + + .filter { + width: 100%; + } +} diff --git a/src/components/BMDashboard/Issues/LongestOpenIssuesChart.jsx b/src/components/BMDashboard/Issues/LongestOpenIssuesChart.jsx new file mode 100644 index 0000000000..804a3dbf60 --- /dev/null +++ b/src/components/BMDashboard/Issues/LongestOpenIssuesChart.jsx @@ -0,0 +1,216 @@ +import { useState, useEffect, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Bar } from 'react-chartjs-2'; +import 'chart.js/auto'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; +import DatePicker from 'react-datepicker'; +import Select from 'react-select'; +import styles from './IssueCharts.module.css'; + +import { + fetchLongestOpenIssues, + fetchMostExpensiveIssues, +} from '../../../actions/bmdashboard/issueChartActions'; + +const truncateLabel = str => (str.length > 28 ? `${str.substring(0, 28)}…` : str); + +function IssuesCharts({ bmProjects = [] }) { + const dispatch = useDispatch(); + + const [graphType, setGraphType] = useState('Longest Open'); + const [selectedProjects, setSelectedProjects] = useState([]); + const [dateRange, setDateRange] = useState({ start: null, end: null }); + + const { longestOpenIssues = [], mostExpensiveIssues = [] } = useSelector( + state => state.issue || {}, + ); + const darkMode = useSelector(state => state.theme.darkMode); + + useEffect(() => { + const params = { + projectIds: selectedProjects.length > 0 ? selectedProjects : undefined, + startDate: dateRange.start || undefined, + endDate: dateRange.end || undefined, + }; + + if (graphType === 'Longest Open') { + dispatch(fetchLongestOpenIssues(params)); + } else { + dispatch(fetchMostExpensiveIssues(params)); + } + }, [graphType, selectedProjects, dateRange.start, dateRange.end, dispatch]); + + const chartData = graphType === 'Longest Open' ? longestOpenIssues : mostExpensiveIssues; + + const data = { + labels: chartData.map(issue => truncateLabel(issue.title || String(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(() => { + const vals = chartData.map(d => (graphType === 'Longest Open' ? d.daysOpen : d.totalCost)); + const dataMax = vals.length > 0 ? Math.max(...vals) : 600; + const xMax = Math.ceil((dataMax + 50) / 250) * 250; + + return { + indexAxis: 'y', + responsive: true, + maintainAspectRatio: false, + layout: { + padding: { right: 80, left: 10 }, + }, + plugins: { + legend: { display: false }, + datalabels: { + anchor: 'end', + align: 'right', + clip: false, + formatter: value => (graphType === 'Longest Open' ? `${value} days` : `$${value}`), + color: darkMode ? '#fff' : '#000', + font: { weight: 'bold' }, + }, + title: { + display: true, + text: + graphType === 'Longest Open' + ? 'Top 5 Longest Open Issues' + : 'Most Expensive Issues by Time Open', + font: { size: 14, weight: 'bold' }, + color: darkMode ? '#fff' : '#000', + }, + }, + scales: { + x: { + max: xMax, + title: { + display: true, + text: graphType === 'Longest Open' ? 'Days Open' : 'Total Cost ($)', + font: { size: 12 }, + color: darkMode ? '#fff' : '#000', + }, + ticks: { stepSize: 250, color: darkMode ? '#ccc' : '#333' }, + }, + y: { + afterFit: scale => { + scale.width = 200; + }, + title: { + display: true, + text: 'Issue Title', + font: { size: 12 }, + color: darkMode ? '#fff' : '#000', + }, + ticks: { + color: darkMode ? '#ccc' : '#333', + maxRotation: 0, + autoSkip: false, + }, + }, + }, + elements: { + bar: { borderRadius: 4, borderSkipped: false }, + }, + }; + }, [graphType, darkMode, chartData]); + + const projectOptions = bmProjects.map(p => ({ value: p._id, label: p.name })); + const selectedProjectOptions = projectOptions.filter(opt => selectedProjects.includes(opt.value)); + + const darkSelectStyles = darkMode + ? { + control: base => ({ + ...base, + background: '#2b3e59', + borderColor: '#4a5a72', + color: '#fff', + }), + menu: base => ({ ...base, background: '#2b3e59' }), + option: (base, { isFocused }) => ({ + ...base, + background: isFocused ? '#4a5a72' : '#2b3e59', + color: '#fff', + }), + multiValue: base => ({ ...base, background: '#4a5a72' }), + multiValueLabel: base => ({ ...base, color: '#fff' }), + singleValue: base => ({ ...base, color: '#fff' }), + input: base => ({ ...base, color: '#fff' }), + placeholder: base => ({ ...base, color: '#aaa' }), + } + : {}; + + return ( +
+
+
+ setDateRange(prev => ({ ...prev, start: value }))} + placeholderText="Start date" + calendarClassName={darkMode ? styles.darkCalendar : ''} + className={darkMode ? styles.dateDark : styles.dateInput} + /> + to + setDateRange(prev => ({ ...prev, end: value }))} + placeholderText="End date" + calendarClassName={darkMode ? styles.darkCalendar : ''} + className={darkMode ? styles.dateDark : styles.dateInput} + /> +
+
+ setGraphType(e.target.value)} + className={darkMode ? styles.selectDark : styles.select} + > + + + +
+
+
+ {chartData.length > 0 ? ( + + ) : ( +

No issues found.

+ )} +
+
+ ); +} + +IssuesCharts.propTypes = { + bmProjects: PropTypes.arrayOf(PropTypes.shape({ _id: PropTypes.string, name: PropTypes.string })), +}; + +export default IssuesCharts; diff --git a/src/components/BMDashboard/Issues/issueCharts.module.css b/src/components/BMDashboard/Issues/issueCharts.module.css index 90eedc8457..d063acd937 100644 --- a/src/components/BMDashboard/Issues/issueCharts.module.css +++ b/src/components/BMDashboard/Issues/issueCharts.module.css @@ -4,6 +4,136 @@ Issue Charts Module CSS =========================== */ +/* ----- Longest/Most Expensive Bar Charts ----- */ +.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; +} + +.dateInputs { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.multiSelectWrapper { + min-width: 220px; + flex: 1; +} + +.noData { + text-align: center; + padding: 2rem; + color: #6c757d; + font-style: italic; +} + +.dateInput { + border: 1px solid #ced4da; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +.dateDark { + background: #2b3e59; + color: white; + border: 1px solid #4a5a72; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +/* Fix react-datepicker navigation arrow contrast */ +:global(.react-datepicker__navigation-icon::before) { + border-color: #555 !important; + border-width: 2px 2px 0 0 !important; +} + +:global(.react-datepicker__navigation:hover .react-datepicker__navigation-icon::before) { + border-color: #111 !important; +} + +/* Dark mode datepicker — scoped via calendarClassName prop */ +.darkCalendar { + background-color: #1e2d42 !important; + border-color: #4a5a72 !important; +} + +.darkCalendar :global(.react-datepicker__header) { + background-color: #243450 !important; + border-bottom-color: #4a5a72 !important; +} + +.darkCalendar :global(.react-datepicker__current-month), +.darkCalendar :global(.react-datepicker__day-name) { + color: #e0e6f0 !important; +} + +.darkCalendar :global(.react-datepicker__day) { + color: #ccd1dc !important; +} + +.darkCalendar :global(.react-datepicker__day:hover) { + background-color: #4a5a72 !important; + color: #fff !important; +} + +.darkCalendar :global(.react-datepicker__day--selected), +.darkCalendar :global(.react-datepicker__day--selected:hover) { + background-color: #1e40af !important; + color: #fff !important; +} + +.darkCalendar :global(.react-datepicker__day--today) { + background-color: #1e3a5f !important; + color: #fff !important; + font-weight: bold !important; +} + +.darkCalendar :global(.react-datepicker__day--outside-month) { + color: #6b7a8d !important; +} + +.darkCalendar :global(.react-datepicker__navigation-icon::before) { + border-color: #ccc !important; +} + +.select { + padding: 0.25rem 0.5rem; + border: 1px solid #ced4da; + border-radius: 0.25rem; + font-size: 0.75rem; +} + +.selectDark { + background: #2b3e59; + color: white; + border: 1px solid #4a5a72; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + /* ----- Containers ----- */ .issueChartContainer, .issueChartEventContainer { @@ -73,9 +203,9 @@ Issue Charts Module CSS .chartButtonActive, .chartButtonActiveDark { - background-color: #4caf50; + background-color: #276228; color: #fff; - border-color: #4caf50; + border-color: #276228; } /* ----- Chart Wrapper ----- */ diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index 11b5405861..0d13593e7c 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -10,7 +10,10 @@ import { toast } from 'react-toastify'; import WeeklyProjectSummaryHeader from './WeeklyProjectSummaryHeader'; import PaidLaborCost from './PaidLaborCost/PaidLaborCost'; import { fetchAllMaterials } from '../../../actions/bmdashboard/materialsActions'; + +import { fetchBMProjects } from '../../../actions/bmdashboard/projectActions'; import QuantityOfMaterialsUsed from './QuantityOfMaterialsUsed/QuantityOfMaterialsUsed'; +import IssuesCharts from '../Issues/LongestOpenIssuesChart'; import ProjectRiskProfileOverview from './ProjectRiskProfileOverview'; import IssuesBreakdownChart from './IssuesBreakdownChart'; import InjuryCategoryBarChart from './GroupedBarGraphInjurySeverity/InjuryCategoryBarChart'; @@ -28,7 +31,6 @@ import MostFrequentKeywords from './MostFrequentKeywords/MostFrequentKeywords'; import LessonsLearntChart from '../LessonsLearnt/LessonsLearntChart'; import DistributionLaborHours from './DistributionLaborHours/DistributionLaborHours'; import ToolsStoppageHorizontalBarChart from './Tools/ToolsStoppageHorizontalBarChart/ToolsStoppageHorizontalBarChart'; -import IssueCharts from '../Issues/openIssueCharts'; import ToolStatusDonutChart from './ToolStatusDonutChart/ToolStatusDonutChart'; const projectStatusButtons = [ @@ -209,6 +211,19 @@ function WeeklyProjectSummary() { })); }; + const bmProjects = useSelector(state => state.bmProjects || []); + + // Fetch initial data + useEffect(() => { + if (materials.length === 0) { + dispatch(fetchAllMaterials()); + } + + if (bmProjects.length === 0) { + dispatch(fetchBMProjects()); + } + }, [dispatch, materials.length, bmProjects.length]); + const sections = useMemo( () => [ { @@ -284,12 +299,8 @@ function WeeklyProjectSummary() { { title: 'Issue Tracking', key: 'Issue Tracking', - className: 'full', - content: ( -
- -
- ), + className: 'small', + content: , }, { title: 'Tools and Equipment Tracking', diff --git a/src/constants/bmdashboard/issueConstants.js b/src/constants/bmdashboard/issueConstants.js index 71963001d6..c64b4a1790 100644 --- a/src/constants/bmdashboard/issueConstants.js +++ b/src/constants/bmdashboard/issueConstants.js @@ -8,8 +8,14 @@ export const FETCH_ISSUE_TYPES_YEARS_SUCCESS = 'FETCH_ISSUE_TYPES_YEARS_SUCCESS' export const FETCH_ISSUE_TYPES_YEARS_FAILURE = 'FETCH_ISSUE_TYPES_YEARS_FAILURE'; export const SET_ISSUES = 'SET_ISSUES'; + export const FETCH_LONGEST_OPEN_ISSUES_REQUEST = 'FETCH_LONGEST_OPEN_ISSUES_REQUEST'; export const FETCH_LONGEST_OPEN_ISSUES_SUCCESS = 'FETCH_LONGEST_OPEN_ISSUES_SUCCESS'; export const FETCH_LONGEST_OPEN_ISSUES_FAILURE = 'FETCH_LONGEST_OPEN_ISSUES_FAILURE'; + +export const FETCH_MOST_EXPENSIVE_ISSUES_REQUEST = 'FETCH_MOST_EXPENSIVE_ISSUES_REQUEST'; +export const FETCH_MOST_EXPENSIVE_ISSUES_SUCCESS = 'FETCH_MOST_EXPENSIVE_ISSUES_SUCCESS'; +export const FETCH_MOST_EXPENSIVE_ISSUES_FAILURE = 'FETCH_MOST_EXPENSIVE_ISSUES_FAILURE'; + export const SET_DATE_FILTER = 'SET_DATE_FILTER'; export const SET_PROJECT_FILTER = 'SET_PROJECT_FILTER'; diff --git a/src/reducers/bmdashboard/issueReducer.js b/src/reducers/bmdashboard/issueReducer.js index 2c8175e5c7..5be89bcaf2 100644 --- a/src/reducers/bmdashboard/issueReducer.js +++ b/src/reducers/bmdashboard/issueReducer.js @@ -9,6 +9,9 @@ import { 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, SET_DATE_FILTER, SET_PROJECT_FILTER, } from '../../constants/bmdashboard/issueConstants'; @@ -16,8 +19,10 @@ import { const initialState = { loading: false, issues: [], - issueTypes: [], // Store for issue types - years: [], // Store for years + issueTypes: [], + years: [], + longestOpenIssues: [], + mostExpensiveIssues: [], error: null, selectedDates: [], selectedProjects: [], @@ -33,7 +38,6 @@ const issueReducer = (state = initialState, action) => { case FETCH_ISSUES_BARCHART_FAILURE: return { ...state, loading: false, error: action.payload }; - // New cases for fetching issue types and years case FETCH_ISSUE_TYPES_YEARS_REQUEST: return { ...state, loading: true }; case FETCH_ISSUE_TYPES_YEARS_SUCCESS: @@ -45,37 +49,29 @@ const issueReducer = (state = initialState, action) => { }; case FETCH_ISSUE_TYPES_YEARS_FAILURE: return { ...state, loading: false, error: action.payload }; + case SET_ISSUES: return { ...state, loading: false, issues: action.payload }; + case FETCH_LONGEST_OPEN_ISSUES_REQUEST: - return { - ...state, - loading: true, - error: null, - }; + return { ...state, loading: true, error: null }; case FETCH_LONGEST_OPEN_ISSUES_SUCCESS: - return { - ...state, - loading: false, - issues: action.payload, - error: null, - }; + return { ...state, loading: false, longestOpenIssues: action.payload.data }; case FETCH_LONGEST_OPEN_ISSUES_FAILURE: - return { - ...state, - loading: false, - error: action.payload, - }; + return { ...state, loading: false, error: action.payload }; + + case FETCH_MOST_EXPENSIVE_ISSUES_REQUEST: + return { ...state, loading: true, error: null }; + case FETCH_MOST_EXPENSIVE_ISSUES_SUCCESS: + return { ...state, loading: false, mostExpensiveIssues: action.payload.data }; + case FETCH_MOST_EXPENSIVE_ISSUES_FAILURE: + return { ...state, loading: false, error: action.payload }; + case SET_DATE_FILTER: - return { - ...state, - selectedDates: action.payload, - }; + return { ...state, selectedDates: action.payload }; case SET_PROJECT_FILTER: - return { - ...state, - selectedProjects: action.payload, - }; + return { ...state, selectedProjects: action.payload }; + default: return state; } diff --git a/src/reducers/index.js b/src/reducers/index.js index 68ca63e94b..d30312681a 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -173,6 +173,7 @@ const localReducers = { dashboard: dashboardReducer, injuries: injuriesReducer, weeklyProjectSummary: weeklyProjectSummaryReducer, + issue: issueReducer, costBreakdown: costBreakdownReducer, // lbdashboard diff --git a/src/utils/URL.js b/src/utils/URL.js index a3274bfff3..8668f0e08e 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -459,6 +459,9 @@ export const ENDPOINTS = { BM_TAG_ADD: `${APIEndpoint}/bm/tags`, BM_TAGS_DELETE: `${APIEndpoint}/bm/tags`, + BM_LONGEST_OPEN_ISSUES: `${APIEndpoint}/bm/issues/longest-open`, + BM_MOST_EXPENSIVE_ISSUES: `${APIEndpoint}/bm/issues/most-expensive`, + BM_ORGS_WITH_LOCATION: `${APIEndpoint}/bm/orgLocation`, ORG_DETAILS: projectId => `${APIEndpoint}/bm/orgLocation/${projectId}`, BM_PROJECT_MEMBERS: projectId => `${APIEndpoint}/bm/project/${projectId}/users`,