diff --git a/config-overrides.js b/config-overrides.js index afa2c65e4a..5642ee368a 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-extraneous-dependencies const webpack = require('webpack'); module.exports = function override(config) { diff --git a/src/components/ApplicantsChart/AgeChart.jsx b/src/components/ApplicantsChart/AgeChart.jsx index 90df74219b..999e89df77 100644 --- a/src/components/ApplicantsChart/AgeChart.jsx +++ b/src/components/ApplicantsChart/AgeChart.jsx @@ -8,77 +8,187 @@ import { ResponsiveContainer, LabelList, } from 'recharts'; -import { useSelector } from 'react-redux'; -import styles from './ApplicationChart.module.css'; +import styles from './ApplicantsChart.module.css'; -function AgeChart({ data, compareLabel }) { - const darkMode = useSelector(state => state.theme.darkMode); - const axisColor = darkMode ? '#f3f4f6' : '#111827'; - const axisLineColor = darkMode ? '#d1d5db' : '#374151'; - const gridColor = darkMode ? '#9ca3af' : '#d1d5db'; - const tickFontSize = 14; +function AgeChart({ data, compareLabel, darkMode }) { + // Guard against invalid data + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } - const formatTooltip = (value, name, props) => { - const { change } = props.payload; - if (compareLabel && change !== undefined) { - let changeText = ''; + // Validate data structure + const validData = data.filter( + item => item && item.ageGroup && typeof item.applicants === 'number' && !isNaN(item.applicants), + ); + + if (validData.length === 0) { + return null; + } + + // Custom tooltip content component + const CustomTooltip = ({ active, payload, label }) => { + if (!active || !payload || !payload.length) { + return null; + } + + const dataPoint = payload[0]; + if (!dataPoint) { + return null; + } + + // Try multiple ways to access the data - Recharts passes data in payload[0].payload + const payloadData = dataPoint.payload || {}; + // Also check if label is passed directly (from XAxis) + const ageGroup = label || payloadData.ageGroup || dataPoint.name || ''; + const applicants = + dataPoint.value !== undefined && dataPoint.value !== null + ? dataPoint.value + : payloadData.applicants !== undefined + ? payloadData.applicants + : 0; + const change = payloadData.change; + + // Debug: log to see what we're getting (remove in production) + // console.log('Tooltip data:', { active, payload, label, dataPoint, payloadData, ageGroup, applicants, change }); + + let changeText = ''; + if (compareLabel && change !== undefined && change !== null) { if (change > 0) { - changeText = `${change}% more than ${compareLabel}`; + changeText = `(${change}% more than ${compareLabel})`; } else if (change < 0) { - changeText = `${Math.abs(change)}% less than ${compareLabel}`; + changeText = `(${Math.abs(change)}% less than ${compareLabel})`; } else { - changeText = `No change from ${compareLabel}`; + changeText = `(No change from ${compareLabel})`; } - return [`${value} (${changeText})`, 'Applicants']; } - return [`${value}`, 'Applicants']; + + // Ensure we always render content, even if some values are missing + const displayAgeGroup = ageGroup || 'N/A'; + const displayApplicants = applicants !== undefined && applicants !== null ? applicants : 0; + + return ( +
+
+ {displayAgeGroup} +
+
+ Applicants :{' '} + {displayApplicants} +
+ {changeText && ( +
+ {changeText} +
+ )} +
+ ); }; return ( -
-

Applicants grouped by Age

- - - - - - +
+ + - - - - - + barSize={60} + > + + + + + + + + + +
); } diff --git a/src/components/ApplicantsChart/ApplicantsChart.module.css b/src/components/ApplicantsChart/ApplicantsChart.module.css new file mode 100644 index 0000000000..f8b662ed55 --- /dev/null +++ b/src/components/ApplicantsChart/ApplicantsChart.module.css @@ -0,0 +1,184 @@ +/* Dark mode hover styles for DatePicker calendar */ +.text-light .hgn-datepicker-dark-calendar .react-datepicker { + background-color: #1f2937 !important; + border: 1px solid #374151 !important; + border-radius: 0 !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__header { + background-color: #111827 !important; + border-bottom: 1px solid #374151 !important; + border-radius: 0 !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__month-container { + background-color: #1f2937 !important; + border-radius: 0 !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__day { + color: #e5e7eb !important; + border-radius: 0 !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__day:hover { + background-color: #374151 !important; + color: #ffffff !important; + border-radius: 0 !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__day--selected, +.text-light .hgn-datepicker-dark-calendar .react-datepicker__day--keyboard-selected { + background-color: #2563eb !important; + color: #ffffff !important; + border-radius: 0 !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__day--in-range, +.text-light .hgn-datepicker-dark-calendar .react-datepicker__day--in-selecting-range { + background-color: #1e3a8a !important; + color: #ffffff !important; + border-radius: 0 !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__current-month, +.text-light .hgn-datepicker-dark-calendar .react-datepicker__day-name { + color: #e5e7eb !important; +} + +.text-light .hgn-datepicker-dark-calendar .react-datepicker__navigation-icon::before { + border-color: #e5e7eb !important; +} + +/* Dark mode hover for DatePicker input */ +.text-light .hgn-datepicker-dark:hover { + background-color: #374151 !important; + border-color: #4b5563 !important; +} + +.text-light .hgn-datepicker-dark:focus { + background-color: #374151 !important; + border-color: #3b82f6 !important; + outline: none; +} + +/* Dark mode hover for select dropdown */ +.text-light select:hover { + background-color: #374151 !important; + border-color: #4b5563 !important; +} + +.text-light select:focus { + background-color: #374151 !important; + border-color: #3b82f6 !important; + outline: none; +} + +.datePickerWrapper { + display: inline-block; +} + +/* Ensure date picker input text is visible in dark mode */ +:global(.text-light) :global(.hgn-datepicker-dark), +:global(.text-light) :global(.hgn-datepicker-dark) input, +:global(.text-light) :global(.hgn-datepicker-dark) input[type="text"] { + color: #e5e7eb !important; + background-color: #1f2937 !important; +} + +:global(.text-light) :global(.hgn-datepicker-dark)::placeholder, +:global(.text-light) :global(.hgn-datepicker-dark) input::placeholder { + color: #9ca3af !important; + opacity: 1 !important; +} + +:global(.text-light) :global(.hgn-datepicker-dark):focus, +:global(.text-light) :global(.hgn-datepicker-dark):focus input, +:global(.text-light) :global(.hgn-datepicker-dark):focus input[type="text"] { + color: #e5e7eb !important; + background-color: #374151 !important; +} + +/* Additional styles for react-datepicker input in dark mode */ +:global(.text-light) .datePickerWrapper :global(input.react-datepicker-ignore-onclickoutside), +:global(.text-light) .datePickerWrapper :global(.react-datepicker__input-container input), +:global(.text-light) .datePickerWrapper :global(.react-datepicker__input-container) :global(input) { + color: #e5e7eb !important; + background-color: #1f2937 !important; +} + +:global(.text-light) .datePickerWrapper :global(input.react-datepicker-ignore-onclickoutside)::placeholder, +:global(.text-light) .datePickerWrapper :global(.react-datepicker__input-container input)::placeholder, +:global(.text-light) .datePickerWrapper :global(.react-datepicker__input-container) :global(input)::placeholder { + color: #9ca3af !important; + opacity: 1 !important; +} + +/* More specific targeting for react-datepicker input wrapper */ +:global(.text-light) :global(.react-datepicker-wrapper) :global(.hgn-datepicker-dark), +:global(.text-light) :global(.react-datepicker-wrapper) :global(input) { + color: #e5e7eb !important; + background-color: #1f2937 !important; +} + +:global(.text-light) :global(.react-datepicker-wrapper) :global(.hgn-datepicker-dark)::placeholder, +:global(.text-light) :global(.react-datepicker-wrapper) :global(input)::placeholder { + color: #9ca3af !important; + opacity: 1 !important; +} + +/* Dark mode styles for recharts surface */ +:global(.bg-oxford-blue) :global(.recharts-surface) { + background-color: #1b2a41 !important; +} + +:global(.bg-oxford-blue) :global(.recharts-wrapper) { + background-color: #1b2a41 !important; +} + +:global(.bg-oxford-blue) :global(.recharts-cartesian-grid) { + stroke: #555 !important; +} + +/* Custom tooltip styles - ensure text is always visible */ +.customTooltip { + background-color: #ffffff !important; + color: #000000 !important; +} + +.customTooltip * { + color: #000000 !important; +} + +.tooltipAgeGroup { + color: #000000 !important; + font-weight: 600 !important; +} + +.tooltipApplicants { + color: #000000 !important; +} + +.tooltipApplicants strong { + color: #000000 !important; + font-weight: 700 !important; +} + +.tooltipChange { + color: #000000 !important; +} + +/* Global tooltip styles to override any parent dark mode styles */ +:global(.recharts-tooltip-wrapper) .customTooltip, +:global(.recharts-tooltip-wrapper) .customTooltip * { + color: #000000 !important; + background-color: #ffffff !important; +} + +/* Override text-light class for tooltip */ +:global(.text-light) :global(.recharts-tooltip-wrapper) .customTooltip, +:global(.text-light) :global(.recharts-tooltip-wrapper) .customTooltip * { + color: #000000 !important; + background-color: #ffffff !important; +} + diff --git a/src/components/ApplicantsChart/index.jsx b/src/components/ApplicantsChart/index.jsx index 8c9a324d1a..8a3ad91b39 100644 --- a/src/components/ApplicantsChart/index.jsx +++ b/src/components/ApplicantsChart/index.jsx @@ -1,40 +1,322 @@ import { useState, useEffect } from 'react'; -import TimeFilter from './TimeFilter'; +import { useSelector } from 'react-redux'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; import AgeChart from './AgeChart'; import fetchApplicantsData from './api'; -import styles from './ApplicationChart.module.css'; -import { useSelector } from 'react-redux'; +import styles from './ApplicantsChart.module.css'; -function ApplicantsChart() { +function ApplicantsDashboard() { + const [selectedOption, setSelectedOption] = useState('weekly'); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); const [chartData, setChartData] = useState([]); const [compareLabel, setCompareLabel] = useState('last week'); const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + // dark mode from Redux const darkMode = useSelector(state => state.theme.darkMode); - const handleFilterChange = async filter => { + // Extract validation logic + const validateCustomDates = (start, end) => { + if (!start || !end) { + return { valid: false, error: null }; + } + if (start > end) { + return { valid: false, error: 'Start date cannot be after end date.' }; + } + return { valid: true, error: null }; + }; + + // Extract data fetching logic + const fetchData = async (option, start, end) => { + try { + const filter = { selectedOption: option, startDate: start, endDate: end }; + const data = await fetchApplicantsData(filter); + + if (!data || data.length === 0) { + setError('⚠️ No data available for the selected filter.'); + setChartData([]); + return; + } + + setChartData(data); + const label = option === 'custom' ? null : `last ${option.slice(0, -2)}`; + setCompareLabel(label); + setError(null); + } catch (err) { + // Handle exception properly + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; + // eslint-disable-next-line no-console + console.error('Failed to fetch applicants data:', errorMessage); + setError('❌ Failed to load data. Please try again.'); + setChartData([]); + } + }; + + const handleFilterChange = async (option, start, end) => { setLoading(true); - const data = await fetchApplicantsData(filter); - setChartData(data); + setError(null); - setCompareLabel( - filter.selectedOption === 'custom' ? null : `last ${filter.selectedOption.slice(0, -2)}`, - ); + // validation for custom dates + if (option === 'custom') { + const validation = validateCustomDates(start, end); + if (!validation.valid) { + if (validation.error) { + setError(validation.error); + setChartData([]); + } + setLoading(false); + return; + } + } + + await fetchData(option, start, end); setLoading(false); }; + // Extract select change handler + const handleSelectChange = e => { + const val = e.target.value; + setSelectedOption(val); + setStartDate(null); + setEndDate(null); + handleFilterChange(val, null, null); + }; + + // Extract start date change handler + const handleStartDateChange = date => { + setStartDate(date); + handleFilterChange('custom', date, endDate); + }; + + // Extract end date change handler + const handleEndDateChange = date => { + setEndDate(date); + handleFilterChange('custom', startDate, date); + }; + + // Extract loading state rendering + const renderLoadingState = () => ( +
+

+ Loading... +

+
+ ); + + // Extract empty state rendering + const renderEmptyState = () => { + // Check if it's a validation error (start date > end date) + const isValidationError = error && error.includes('Start date cannot be after end date'); + + return ( +
+

+ {isValidationError + ? error + : error + ? 'Unable to load chart data.' + : 'No data available to display.'} +

+
+ ); + }; + + // Extract chart content rendering + const renderChartContent = () => { + if (loading) { + return renderLoadingState(); + } + + const hasData = !error && chartData.length > 0; + if (hasData) { + return ; + } + + return renderEmptyState(); + }; + + // Extract date picker styles + const getDatePickerStyles = () => ({ + backgroundColor: darkMode ? '#1f2937' : '#fff', + color: darkMode ? '#e5e7eb' : '#000', + border: `1px solid ${darkMode ? '#374151' : '#ccc'}`, + borderRadius: darkMode ? '0' : '4px', + padding: '6px 12px', + fontSize: '14px', + cursor: 'pointer', + width: '150px', + }); + + // Extract date picker props + const getDatePickerProps = () => ({ + dateFormat: 'yyyy/MM/dd', + className: darkMode ? 'hgn-datepicker-dark' : '', + calendarClassName: darkMode ? 'hgn-datepicker-dark-calendar' : '', + wrapperClassName: darkMode ? styles.datePickerWrapper : '', + style: getDatePickerStyles(), + }); + + // Extract date pickers rendering + const renderDatePickers = () => { + if (selectedOption !== 'custom') { + return null; + } + + return ( + <> + + to + + + ); + }; + + // Extract select styles + const getSelectStyles = () => ({ + padding: '6px 12px', + borderRadius: darkMode ? '0' : '4px', + border: `1px solid ${darkMode ? '#374151' : '#ccc'}`, + backgroundColor: darkMode ? '#1f2937' : '#fff', + color: darkMode ? '#e5e7eb' : '#000', + fontSize: '14px', + cursor: 'pointer', + }); + + // initial load useEffect(() => { - handleFilterChange({ selectedOption: 'weekly' }); + handleFilterChange('weekly', null, null); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( -
-
- - {loading ?

Loading...

: } +
+ {/* Time Filter */} +
+ + + + + {renderDatePickers()} +
+ + {/* Chart Title - Always visible */} +

+ Applicants Grouped by Age +

+ + {/* Chart */} +
+ {renderChartContent()}
); } -export default ApplicantsChart; +export default ApplicantsDashboard; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx index 832ecc4237..484c699aaf 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ToolStatusDonutChart/ToolStatusDonutChart.jsx @@ -203,7 +203,6 @@ export default function ToolStatusDonutChart() { return (

Proportion of Tools/Equipment

-