diff --git a/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.jsx b/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.jsx index 33d9fabb0b..39f0c6ae16 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.jsx @@ -1,13 +1,12 @@ import { useEffect, useRef, useState, useCallback } from 'react'; import { useSelector } from 'react-redux'; import axios from 'axios'; -import DatePicker, { CalendarContainer } from 'react-datepicker'; +import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import * as d3 from 'd3'; +import { FaTrash } from 'react-icons/fa'; import styles from './MostFrequentKeywords.module.css'; import Select, { components as selectComponents } from 'react-select'; -import PropTypes from 'prop-types'; - const formatCalendarMonth = date => date.toLocaleString('en-US', { month: 'long', @@ -20,7 +19,53 @@ const DropdownIndicator = props => ( ); -function MostFrequentKeywords({ darkMode: propDarkMode }) { +// Pick the most recent unique-tag items, capped at maxItems. +function getLatestData(data, isMobile) { + if (!data || data.length === 0) return []; + + const sorted = [...data].sort((a, b) => new Date(b.date) - new Date(a.date)); + const maxItems = isMobile ? 6 : 8; + + if (sorted.length < maxItems) return sorted; + + const latestItems = []; + const usedTags = new Set(); + + for (const item of sorted) { + if (!usedTags.has(item.tag)) { + latestItems.push(item); + usedTags.add(item.tag); + if (latestItems.length >= maxItems) break; + } + } + + return latestItems; +} + +// Items without a real date are always included; otherwise check the bounds. +function isWithinDateRange(item, startDate, endDate) { + if (!item.date) return true; + + const itemDate = new Date(item.date); + itemDate.setHours(0, 0, 0, 0); + + if (startDate) { + const start = new Date(startDate); + start.setHours(0, 0, 0, 0); + if (itemDate < start) return false; + } + + if (endDate) { + const end = new Date(endDate); + end.setHours(23, 59, 59, 999); + if (itemDate > end) return false; + } + + return true; +} + +function MostFrequentKeywords() { + const darkMode = useSelector(state => state.theme.darkMode); const svgRef = useRef(); const containerRef = useRef(); const [projects, setProjects] = useState([]); @@ -35,8 +80,6 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { const [isMobile, setIsMobile] = useState(false); const [tooltip, setTooltip] = useState({ visible: false, text: '', x: 0, y: 0 }); const API_BASE = process.env.REACT_APP_APIENDPOINT; - const reduxDarkMode = useSelector(state => state.theme.darkMode); - const darkMode = propDarkMode !== undefined ? propDarkMode : reduxDarkMode; const palette = darkMode ? { controlBg: '#243447', @@ -145,36 +188,7 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { } }; - // Generate clean data for any project - const generateProjectSpecificData = projectName => { - const isDuplicableCityCenter = projectName.toLowerCase().includes('duplicable city center'); - - if (isDuplicableCityCenter) { - return [ - { tag: 'Modular Design', count: 85, date: '2025-03-15' }, - { tag: 'Prefabrication', count: 78, date: '2025-04-22' }, - { tag: 'Replicable Units', count: 72, date: '2025-05-10' }, - { tag: 'Standard Parts', count: 64, date: '2025-06-18' }, - { tag: 'Urban Planning', count: 81, date: '2025-08-30' }, - { tag: 'Smart City Tech', count: 69, date: '2025-10-05' }, - { tag: 'Energy Efficiency', count: 76, date: '2026-01-19' }, - { tag: 'Mixed Use', count: 68, date: '2026-05-08' }, - ]; - } - - return [ - { tag: 'Site Planning', count: 72, date: '2024-03-15' }, - { tag: 'Foundation', count: 65, date: '2024-06-22' }, - { tag: 'Framing', count: 58, date: '2024-09-10' }, - { tag: 'Electrical', count: 62, date: '2025-01-18' }, - { tag: 'Plumbing', count: 54, date: '2025-04-25' }, - { tag: 'HVAC', count: 67, date: '2025-07-30' }, - { tag: 'Finishing', count: 59, date: '2025-11-14' }, - { tag: 'Landscaping', count: 51, date: '2026-02-05' }, - ]; - }; - - const fetchProjectData = async (projectId, projectName) => { + const fetchProjectData = async projectId => { try { setIsLoading(true); setError(''); @@ -192,29 +206,21 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { const responseData = response?.data?.data; if (responseData && responseData.length > 0) { - const dataWithDates = responseData.slice(0, 8).map((item, index) => { - const years = [2023, 2024, 2025, 2026]; - const year = years[index % 4]; - const month = ((index * 3) % 12) + 1; - const day = ((index * 5) % 28) + 1; - return { - ...item, - count: item.count || 50 + index * 5, - date: `${year}-${month.toString().padStart(2, '0')}-${day - .toString() - .padStart(2, '0')}`, - }; - }); - setAllTags(dataWithDates); + const normalizedData = responseData.slice(0, 8).map(item => ({ + tag: item.tag, + count: item.count || 1, + date: item.date || null, + })); + setAllTags(normalizedData); return; } + + // API returned empty — no keywords for this project + setAllTags([]); } catch { - // Use generated data when API fails + setError('Failed to load keywords. Please try again.'); + setAllTags([]); } - - // Fallback to generated data - const generatedData = generateProjectSpecificData(projectName); - setAllTags(generatedData); } finally { setIsLoading(false); } @@ -234,7 +240,7 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { } else if (selected.type === 'project') { const project = projects.find(p => p._id === selected.value); if (project) { - fetchProjectData(project._id, project.projectName); + fetchProjectData(project._id); } } }; @@ -280,79 +286,25 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { }; }, [dimensions, isMobile]); - const getLatestData = useCallback( - data => { - if (!data || data.length === 0) return []; - - const sorted = [...data].sort((a, b) => new Date(b.date) - new Date(a.date)); - const maxItems = isMobile ? 6 : 8; - - if (sorted.length >= maxItems) { - const latestItems = []; - const usedTags = new Set(); - - for (const item of sorted) { - if (!usedTags.has(item.tag)) { - latestItems.push(item); - usedTags.add(item.tag); - if (latestItems.length >= maxItems) break; - } - } - - return latestItems; - } - - return sorted; - }, - [isMobile], - ); - const filterTagsByDate = useCallback( tagsToFilter => { if (!tagsToFilter || tagsToFilter.length === 0) return []; if (!startDate && !endDate) { - return getLatestData(tagsToFilter); + return getLatestData(tagsToFilter, isMobile); } - const filtered = tagsToFilter.filter(item => { - const itemDate = new Date(item.date); - itemDate.setHours(0, 0, 0, 0); - - if (startDate && endDate) { - const start = new Date(startDate); - start.setHours(0, 0, 0, 0); - const end = new Date(endDate); - end.setHours(23, 59, 59, 999); - return itemDate >= start && itemDate <= end; - } - if (startDate) { - const start = new Date(startDate); - start.setHours(0, 0, 0, 0); - return itemDate >= start; - } - if (endDate) { - const end = new Date(endDate); - end.setHours(23, 59, 59, 999); - return itemDate <= end; - } - - return true; - }); + const filtered = tagsToFilter.filter(item => isWithinDateRange(item, startDate, endDate)); const sorted = [...filtered].sort((a, b) => b.count - a.count); const maxItems = isMobile ? 6 : 8; const result = sorted.slice(0, maxItems); - if (result.length === 0) { - setError('No data for selected range'); - } else { - setError(''); - } + setError(result.length === 0 ? 'No keywords available for selected filters' : ''); return result; }, - [startDate, endDate, getLatestData, isMobile], + [startDate, endDate, isMobile], ); useEffect(() => { @@ -1055,58 +1007,6 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { backgroundColor: palette.menuBg, }); - const applyDarkCalendarTheme = useCallback(() => { - requestAnimationFrame(() => { - const poppers = Array.from(document.querySelectorAll('.react-datepicker-popper')); - const activePopper = poppers.find(popper => popper.offsetParent !== null) || poppers.at(-1); - if (!activePopper) return; - - const datepicker = activePopper.querySelector('.react-datepicker'); - const monthContainer = activePopper.querySelector('.react-datepicker__month-container'); - const header = activePopper.querySelector('.react-datepicker__header'); - const currentMonth = activePopper.querySelector('.react-datepicker__current-month'); - const dayNames = activePopper.querySelectorAll('.react-datepicker__day-name'); - const days = activePopper.querySelectorAll('.react-datepicker__day'); - - if (datepicker) { - datepicker.style.backgroundColor = '#0f172a'; - datepicker.style.borderColor = '#334155'; - } - - if (monthContainer) { - monthContainer.style.backgroundColor = '#0f172a'; - } - - if (header) { - header.style.backgroundColor = '#1e293b'; - header.style.borderBottomColor = '#334155'; - } - - if (currentMonth) { - currentMonth.style.color = '#f8fafc'; - } - - dayNames.forEach(dayName => { - dayName.style.color = '#e2e8f0'; - dayName.style.backgroundColor = 'transparent'; - }); - - days.forEach(day => { - if (!day.classList.contains('react-datepicker__day--selected')) { - day.style.color = '#f8fafc'; - day.style.backgroundColor = 'transparent'; - } - }); - }); - }, []); - - const renderCalendarContainer = useCallback( - ({ className, children }) => ( - {children} - ), - [], - ); - const renderCalendarHeader = useCallback( ({ date, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled }) => (
@@ -1193,8 +1093,6 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { dateFormat={isMobile ? 'MM/dd/yyyy' : 'MM/dd/yy'} maxDate={endDate || today} minDate={new Date('2023-01-01')} - calendarContainer={renderCalendarContainer} - onCalendarOpen={applyDarkCalendarTheme} renderCustomHeader={darkMode ? renderCalendarHeader : undefined} />
@@ -1213,14 +1111,17 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { dateFormat={isMobile ? 'MM/dd/yyyy' : 'MM/dd/yy'} minDate={startDate || new Date('2023-01-01')} maxDate={today} - calendarContainer={renderCalendarContainer} - onCalendarOpen={applyDarkCalendarTheme} renderCustomHeader={darkMode ? renderCalendarHeader : undefined} /> {(startDate || endDate) && ( - )} @@ -1229,9 +1130,13 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { {isLoading &&
Loading...
} {!isLoading && error &&
{error}
} {!isLoading && !error && tags.length === 0 && ( -
{selectedOption ? 'No data' : 'Select source'}
+
+ {selectedOption + ? 'No keywords available for selected filters' + : 'Select a data source to view keywords'} +
)} - {!isLoading && !error && tags.length > 0 && ( + {!isLoading && !error && tags.length > 0 && dimensions.width > 0 && ( )} @@ -1239,12 +1144,4 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) { ); } -MostFrequentKeywords.propTypes = { - darkMode: PropTypes.bool, -}; - -MostFrequentKeywords.defaultProps = { - darkMode: false, -}; - export default MostFrequentKeywords; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.module.css b/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.module.css index b8a48690be..29644fbe46 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.module.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.module.css @@ -1,9 +1,18 @@ /* stylelint-disable */ .mfkContainer { + --bg-color: #ffffff; + --text-color: #000000; + --title-color: #1e3a8a; + --label-color: #374151; + --input-bg: #ffffff; + --border-color: #d1d5db; + --focus-border-color: #3b82f6; + --text-secondary: #6b7280; + --error-color: #dc2626; padding: 1rem; font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; - background-color: var(--bg-color, #fff); - color: var(--text-color, #000); + background-color: var(--bg-color); + color: var(--text-color); height: 100%; min-height: 100%; display: flex; @@ -50,16 +59,16 @@ min-width: 120px; } -/* Make data source dropdown wider */ +/* Data source dropdown spans its own row so the date fields can align together */ .controlGroup:first-of-type { - flex: 2 1 200px; + flex: 1 1 100%; min-width: 180px; } -/* Make date fields smaller and just big enough */ +/* Keep both date fields on the same line, sharing the row evenly */ .controlGroup:nth-of-type(2), .controlGroup:nth-of-type(3) { - flex: 0 1 110px; + flex: 1 1 0; min-width: 100px; } @@ -275,13 +284,13 @@ } .controlGroup:first-of-type { - flex: 2 1 180px; + flex: 1 1 100%; min-width: 160px; } .controlGroup:nth-of-type(2), .controlGroup:nth-of-type(3) { - flex: 0 1 100px; + flex: 1 1 0; min-width: 90px; } @@ -419,6 +428,11 @@ background-color: #0f172a !important; } +:global(.mfk-dark-popper .react-datepicker__month), +:global(.mfk-dark-calendar .react-datepicker__month) { + background-color: #0f172a !important; +} + :global(.mfk-dark-popper .react-datepicker__header), :global(.mfk-dark-calendar .react-datepicker__header) { background-color: #1e293b !important; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index dd4d174a07..11b5405861 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -322,14 +322,14 @@ function WeeklyProjectSummary() { { title: 'Lessons Learned', key: 'Lessons Learned', - className: 'half', + className: 'full', content: [
- +
,