From fb524881b606d1c5d36fc0a43e50f3b831506e2b Mon Sep 17 00:00:00 2001 From: One Community Date: Sun, 14 Sep 2025 20:44:49 -0700 Subject: [PATCH 01/16] Revert "Frontend Release to Main [4.49]" --- eslint.config.js | 3 +- package-lock.json | 20 +- package.json | 6 +- src/actions/projectMembers.js | 9 +- src/components/ApplicantsChart/AgeChart.jsx | 67 +- .../ApplicantsChart/ApplicantsAgeChart.jsx | 55 + src/components/ApplicantsChart/TimeFilter.jsx | 117 +- src/components/ApplicantsChart/index.jsx | 169 +- .../Collaboration/SuggestedJobsList.css | 259 -- .../Collaboration/SuggestedJobsList.jsx | 310 -- src/components/Collaboration/index.jsx | 4 +- src/components/LBDashboard/LBDashboard.jsx | 266 +- .../LBDashboard/LBDashboard.module.css | 269 +- .../__tests__/UserPermissionsPopup.test.jsx | 4 +- .../Projects/Members/FoundUser/FoundUser.jsx | 1 - .../FoundUser/__tests__/FoundUser.test.jsx | 2 - src/components/Projects/Members/Members.jsx | 107 +- src/components/SummaryBar/SummaryBar.jsx | 2 +- .../TeamMemberTasks/ReviewButton.jsx | 3 +- .../TeamMemberTasks/TeamMemberTask.jsx | 2 - .../TeamMemberTaskIconsInfo.jsx | 4 +- .../TeamMemberTasks/TeamMemberTasks.jsx | 22 +- .../__tests__/DiffedText.test.jsx | 10 +- .../__tests__/TaskButton.test.jsx | 12 +- .../__tests__/TeamMemberTasks.test.jsx | 113 +- .../components/TaskCompletedModal.jsx | 3 +- .../components/TaskDifferenceModal.jsx | 10 +- .../__tests__/TaskDifferenceModal.test.jsx | 90 +- src/components/Teams/TeamMembersPopup.jsx | 7 +- .../QuickSetupModal/AddNewTitleModal.jsx | 316 +- .../QuickSetupModal/AssignSetupModal.jsx | 4 +- .../QuickSetupModal/AssignTeamField.jsx | 167 +- src/components/UserProfile/UserProfile.jsx | 98 +- .../AccessManagementModal.css | 1 - .../AccessManagementModal.jsx | 629 +-- src/routes.jsx | 12 +- yarn.lock | 3673 +++++++++-------- 37 files changed, 2756 insertions(+), 4090 deletions(-) create mode 100644 src/components/ApplicantsChart/ApplicantsAgeChart.jsx delete mode 100644 src/components/Collaboration/SuggestedJobsList.css delete mode 100644 src/components/Collaboration/SuggestedJobsList.jsx diff --git a/eslint.config.js b/eslint.config.js index 20e666f523..673800f70f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,7 +34,8 @@ module.exports = [ // Ignore entire component folders 'src/components/Badge/**', 'src/components/SummaryManagement/**', - 'src/components/UserProfile/**', + 'src/components/TeamMemberTasks/**', + 'src/components/Teams/TeamMembersPopup.jsx', 'src/components/Announcements/index.jsx', ], }, diff --git a/package-lock.json b/package-lock.json index 8ae12ed14a..5c65e066ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "@date-io/dayjs": "^3.2.0", "@emotion/react": "^11.8.1", "@emotion/styled": "^11.14.0", - "@fortawesome/fontawesome-svg-core": "^6.7.2", - "@fortawesome/free-brands-svg-icons": "^7.0.1", + "@fortawesome/fontawesome-svg-core": "^6", + "@fortawesome/free-brands-svg-icons": "^7.0.0", "@fortawesome/free-regular-svg-icons": "^6", "@fortawesome/free-solid-svg-icons": "^6.7.2", - "@fortawesome/react-fontawesome": "^0.2.6", + "@fortawesome/react-fontawesome": "^0.2.3", "@hello-pangea/dnd": "^18.0.1", "@mui/lab": "^7.0.0-beta.12", "@mui/material": "^7.1.0", @@ -2875,21 +2875,21 @@ } }, "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.0.1.tgz", - "integrity": "sha512-6xPmn5SrND/GM0+W33E77x05+aDn6RpR02eWd8eLdN0IxY0vXa5yU/ugaAKloOVxiG9w2330TSRsbJYL6c57Ow==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.0.0.tgz", + "integrity": "sha512-C8oY28gq/Qx/cHReJa2AunKJUHvUZDVoPlSTHtAvjriaNfi+5nugW4cx7yA/xN3f/nYkElw11gFBoJ2xUDDFgg==", "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "7.0.1" + "@fortawesome/fontawesome-common-types": "7.0.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.1.tgz", - "integrity": "sha512-0VpNtO5cNe1/HQWMkl4OdncYK/mv9hnBte0Ew0n6DMzmo3Q3WzDFABHm6LeNTipt5zAyhQ6Ugjiu8aLaEjh1gg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.0.tgz", + "integrity": "sha512-PGMrIYXLGA5K8RWy8zwBkd4vFi4z7ubxtet6Yn13Plf6krRTwPbdlCwlcfmoX0R7B4Z643QvrtHmdQ5fNtfFCg==", "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index 66b1d7bf56..6a330d02c9 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,11 @@ "@date-io/dayjs": "^3.2.0", "@emotion/react": "^11.8.1", "@emotion/styled": "^11.14.0", - "@fortawesome/fontawesome-svg-core": "^6.7.2", - "@fortawesome/free-brands-svg-icons": "^7.0.1", + "@fortawesome/fontawesome-svg-core": "^6", + "@fortawesome/free-brands-svg-icons": "^7.0.0", "@fortawesome/free-regular-svg-icons": "^6", "@fortawesome/free-solid-svg-icons": "^6.7.2", - "@fortawesome/react-fontawesome": "^0.2.6", + "@fortawesome/react-fontawesome": "^0.2.3", "@hello-pangea/dnd": "^18.0.1", "@mui/lab": "^7.0.0-beta.12", "@mui/material": "^7.1.0", diff --git a/src/actions/projectMembers.js b/src/actions/projectMembers.js index 239c57f4a9..c68ffbb6ac 100644 --- a/src/actions/projectMembers.js +++ b/src/actions/projectMembers.js @@ -137,7 +137,7 @@ export const fetchProjectsWithActiveUsers = () => { /** * Call API to assign/ unassign project */ -export const assignProject = (projectId, userId, operation, firstName, lastName, isActive) => { +export const assignProject = (projectId, userId, operation, firstName, lastName) => { const request = axios.post(ENDPOINTS.PROJECT_MEMBER(projectId), { projectId, users: [ @@ -158,10 +158,9 @@ export const assignProject = (projectId, userId, operation, firstName, lastName, _id: userId, firstName, lastName, - isActive }), ); - // dispatch(removeFoundUser(userId)); + dispatch(removeFoundUser(userId)); } else { dispatch(deleteMember(userId)); } @@ -193,8 +192,8 @@ export const findProjectMembers = (_projectId, query) => { ); const list = Array.isArray(data) ? data - : Array.isArray(data?.users) ? data.users - : []; + : Array.isArray(data?.users) ? data.users + : []; const assigned = new Set(getState().projectMembers.members.map(m => m._id)); const users = list.map(u => ({ ...u, assigned: assigned.has(u._id) })); diff --git a/src/components/ApplicantsChart/AgeChart.jsx b/src/components/ApplicantsChart/AgeChart.jsx index e4f702de7c..49ccdaef5c 100644 --- a/src/components/ApplicantsChart/AgeChart.jsx +++ b/src/components/ApplicantsChart/AgeChart.jsx @@ -9,11 +9,11 @@ import { LabelList, } from 'recharts'; -function AgeChart({ data, compareLabel, darkMode }) { +function AgeChart({ data, compareLabel }) { const formatTooltip = (value, name, props) => { const { change } = props.payload; + let changeText = ''; if (compareLabel && change !== undefined) { - let changeText = ''; if (change > 0) { changeText = `${change}% more than ${compareLabel}`; } else if (change < 0) { @@ -27,56 +27,19 @@ function AgeChart({ data, compareLabel, darkMode }) { }; return ( -
-

- Applicants Grouped by Age -

-
- - - - - - - - - - - -
+
+

Applicants grouped by Age

+ + + + + + + + + + +
); } diff --git a/src/components/ApplicantsChart/ApplicantsAgeChart.jsx b/src/components/ApplicantsChart/ApplicantsAgeChart.jsx new file mode 100644 index 0000000000..1f39f1cd89 --- /dev/null +++ b/src/components/ApplicantsChart/ApplicantsAgeChart.jsx @@ -0,0 +1,55 @@ +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + LabelList, +} from 'recharts'; + +const data = [ + { ageGroup: '18 - 21', applicants: 25, change: 10 }, + { ageGroup: '21 - 24', applicants: 60, change: -5 }, + { ageGroup: '24 - 27', applicants: 45, change: 15 }, + { ageGroup: '27 - 30', applicants: 7, change: -3 }, + { ageGroup: '30 - 33', applicants: 10, change: 0 }, +]; + +function ApplicantsChartPage() { + const formatTooltip = (value, name, props) => { + const { change } = props.payload; + let changeText = ''; + if (change > 0) { + changeText = `${change}% more than last week`; + } else if (change < 0) { + changeText = `${Math.abs(change)}% less than last week`; + } else { + changeText = `No change from last week`; + } + return [`${value} (${changeText})`, 'Applicants']; + }; + + return ( +
+

Applicants grouped by Age

+ + + + + + + + + + + +
+ ); +} + +export default ApplicantsChartPage; diff --git a/src/components/ApplicantsChart/TimeFilter.jsx b/src/components/ApplicantsChart/TimeFilter.jsx index 7d34573341..d032cecb02 100644 --- a/src/components/ApplicantsChart/TimeFilter.jsx +++ b/src/components/ApplicantsChart/TimeFilter.jsx @@ -2,98 +2,63 @@ import { useState, useEffect } from 'react'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; -function TimeFilter({ onFilterChange, darkMode }) { +function TimeFilter({ onFilterChange }) { const [selectedOption, setSelectedOption] = useState('weekly'); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); - const [error, setError] = useState(''); useEffect(() => { - if (selectedOption === 'custom' && startDate && endDate) { - if (startDate > endDate) { - setError('🚨 Start date cannot be after end date.'); - return; - } else { - setError(''); - } - } else { - setError(''); - } - - onFilterChange({ selectedOption, startDate, endDate, error: '' }); + onFilterChange({ selectedOption, startDate, endDate }); }, [selectedOption, startDate, endDate]); return (
- {/* Top row */} -
- - - - - {selectedOption === 'custom' && ( - <> - setStartDate(date)} - placeholderText="Start Date" - dateFormat="yyyy/MM/dd" - /> - to - setEndDate(date)} - placeholderText="End Date" - dateFormat="yyyy/MM/dd" - /> - - )} -
+ + - {/* Error message */} - {error && ( -

- {error} -

+ {selectedOption === 'custom' && ( + <> + setStartDate(date)} + placeholderText="Start Date" + dateFormat="yyyy/MM/dd" + style={{ marginRight: '10px' }} + /> + to + setEndDate(date)} + placeholderText="End Date" + dateFormat="yyyy/MM/dd" + /> + )}
); diff --git a/src/components/ApplicantsChart/index.jsx b/src/components/ApplicantsChart/index.jsx index 6762819ede..994ec580ca 100644 --- a/src/components/ApplicantsChart/index.jsx +++ b/src/components/ApplicantsChart/index.jsx @@ -1,177 +1,34 @@ import { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; +import TimeFilter from './TimeFilter'; import AgeChart from './AgeChart'; import fetchApplicantsData from './api'; -function ApplicantsDashboard() { - const [selectedOption, setSelectedOption] = useState('weekly'); - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(null); +function ApplicantsChart() { 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 (option, start, end) => { + const handleFilterChange = async filter => { setLoading(true); - setError(null); - - // validation for custom dates - if (option === 'custom') { - if (!start || !end) { - setLoading(false); - return; - } - if (start > end) { - setError('🚨 Start date cannot be after end date.'); - setChartData([]); - setLoading(false); - return; - } - } - - 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([]); - } else { - setChartData(data); - setCompareLabel(option === 'custom' ? null : `last ${option.slice(0, -2)}`); - } - } catch (err) { - setError('❌ Failed to load data. Please try again.'); - setChartData([]); - } + const data = await fetchApplicantsData(filter); + setChartData(data); + setCompareLabel( + filter.selectedOption === 'custom' ? null : `last ${filter.selectedOption.slice(0, -2)}`, + ); setLoading(false); }; - // initial load useEffect(() => { - handleFilterChange('weekly', null, null); + handleFilterChange({ selectedOption: 'weekly' }); }, []); return ( -
- {/* Time Filter */} -
- - - - - {selectedOption === 'custom' && ( - <> - { - setStartDate(date); - handleFilterChange('custom', date, endDate); - }} - placeholderText="Start Date" - dateFormat="yyyy/MM/dd" - /> - to - { - setEndDate(date); - handleFilterChange('custom', startDate, date); - }} - placeholderText="End Date" - dateFormat="yyyy/MM/dd" - /> - - )} -
- - {/* Error message */} - {error && ( -
-

- {error} -

-
- )} - - {/* Chart */} - {loading ? ( -
-

- Loading... -

-
- ) : !error ? ( - - ) : null} +
+ + {loading ?

Loading...

: }
); } -export default ApplicantsDashboard; +export default ApplicantsChart; diff --git a/src/components/Collaboration/SuggestedJobsList.css b/src/components/Collaboration/SuggestedJobsList.css deleted file mode 100644 index d08a28ad7c..0000000000 --- a/src/components/Collaboration/SuggestedJobsList.css +++ /dev/null @@ -1,259 +0,0 @@ -/* Dark mode placeholder style */ -input.dark-mode-placeholder::placeholder { - color: #ffffff; - opacity: 0.7; /* Optional: adjust to make it a bit subtle */ -} -.no-jobs-notice img { - filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3)); -} - -.job-placeholder button, -.no-jobs-notice button { - min-width: 120px; -} - -.job-location-tag { - padding: 6px 12px; - border-radius: 20px; - font-weight: 600; - text-align: center; - width: fit-content; - margin: 0 auto 1rem; - font-size: 0.9rem; -} - -.job-location-tag.remote { - background-color: #d1ecf1; - color: #0c5460; -} - -.job-location-tag.in-person { - background-color: #ffeb3b; - color: #333; -} - -.job-details { - text-align: center; - margin-bottom: 1rem; -} - -.job-requirements h4 { - margin-bottom: 0.25rem; -} - -.job-requirements ul { - list-style-type: disc; - padding-left: 1.5rem; - margin-bottom: 1rem; -} - -.apply-now-btn { - background-color: #007bff; - color: white; - padding: 0.5rem 1rem; - border: none; - cursor: pointer; - font-size: 1rem; - border-radius: 4px; - text-align: center; -} - -.apply-now-btn:hover { - background-color: #0056b3; -} -.job-ad a { - display: flex; - justify-content: center; -} - -.pagination-controls { - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - margin-top: 1rem; -} - -.pagination-controls button { - padding: 0.4rem 0.8rem; - font-size: 1rem; - cursor: pointer; - border-radius: 4px; -} - -.pagination-controls button:disabled { - cursor: not-allowed; - opacity: 0.5; -} - -.job-header { - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - color: #fff; -} - -.job-landing { - width: 100%; - height: 100%; -} - -.job-landing .job-header img { - height: auto; - margin: 0 auto; - flex-direction: column; -} - -.job-navbar { - width: 100%; - display: flex; - justify-content: center; /* keep items together */ - align-items: center; - margin-bottom: 20px; - background-color: #9c0; - padding: 20px; - flex-wrap: wrap; /* allows wrapping on very small screens */ - gap: 12px; /* space between form and dropdown */ -} - -.search-form { - display: flex; - align-items: center; - gap: 8px; -} - -.job-select { - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; - background-color: #fff; - font-size: 14px; - min-width: 200px; /* ensures dropdown has consistent width */ -} - - -.search-form input { - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; -} - -.search-form button { - padding: 6px 10px; - border: none; - border-radius: 2px; - cursor: pointer; - font-size: 14px; -} - -.job-list { - display: flex; - flex-direction: column; - grid-template-columns: 1fr; /* Column layout by default */ - gap: 20px; - width: 100%; - max-width: 600px; - margin: 0 auto; -} - -.job-ad { - background: #fff; - border: 1px solid #ccc; - padding: 30px 25px; /* Increased padding for bigger box */ - border-radius: 12px; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - justify-content: space-between; - flex: 0 1 350px; /* Bigger min width */ - /* Max width to keep uniform size */ - transition: transform 0.3s, box-shadow 0.3s; - margin-bottom: 30px; -} - -.job-ad:hover { - transform: translateY(-8px); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.25); -} - -.job-role-name { - font-size: 2rem; - font-weight: bold; - margin-bottom: 1rem; - text-align: center; - color: #0069d9; -} - -.job-details { - font-size: 1.1rem; - color: #555; - line-height: 1.6; - margin-bottom: 1.5rem; - text-align: center; -} - -.job-requirements h4 { - margin-bottom: 0.5rem; - font-weight: 600; - color: #333; - text-align: left; -} - -.job-requirements ul { - list-style-type: disc; - padding-left: 1.75rem; - margin-bottom: 1.5rem; - font-size: 1rem; - color: #444; -} - -.apply-now-btn { - background-color: #007bff; - color: white; - padding: 0.75rem 1.5rem; - border: none; - cursor: pointer; - font-size: 1.1rem; - border-radius: 6px; - text-align: center; - align-self: center; /* Center button */ - width: fit-content; - transition: background-color 0.3s; -} - -.apply-now-btn:hover { - background-color: #0056b3; -} - -.job-grid { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 30px; /* More gap for bigger cards */ - margin: 0 auto; -} - -.category-icon { - width: 150px; - height: 150px; - object-fit: contain; - display: block; - margin: 0 auto 15px; -} - -@media (max-width: 600px) { - .category-icon { - width: 100px; - height: 100px; - } -} - -@media screen and (min-width: 768px) { - .job-grid { - justify-content: center; - } - .job-ad { - flex: 0 1 400px; - } -} diff --git a/src/components/Collaboration/SuggestedJobsList.jsx b/src/components/Collaboration/SuggestedJobsList.jsx deleted file mode 100644 index 9bca59ade7..0000000000 --- a/src/components/Collaboration/SuggestedJobsList.jsx +++ /dev/null @@ -1,310 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { ApiEndpoint } from '../../utils/URL'; -import { toast } from 'react-toastify'; -import OneCommunityImage from '../../assets/images/logo2.png'; -import './SuggestedJobsList.css'; - -function SuggestedJobsList() { - const [categories, setCategories] = useState([]); - const [query, setQuery] = useState(''); - const [category, setCategory] = useState(''); - const [jobAds, setJobAds] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(0); - const [hasSearched, setHasSearched] = useState(false); - const adsPerPage = 3; - const darkMode = useSelector(state => state.theme.darkMode); - // Fetch categories on mount - useEffect(() => { - const fetchCategories = async () => { - try { - const response = await fetch(`${ApiEndpoint}/jobs/categories`, { method: 'GET' }); - if (!response.ok) throw new Error(`Failed to fetch categories: ${response.statusText}`); - const data = await response.json(); - const sortedCategories = data.categories.sort((a, b) => a.localeCompare(b)); - setCategories(sortedCategories); - } catch (error) { - toast.error('Error fetching categories'); - } - }; - fetchCategories(); - }, []); - - // Fetch job ads whenever query, category or page changes - useEffect(() => { - if (!query && !category) { - setJobAds([]); // Clear jobs if no filters selected - setTotalPages(0); - return; // Skip fetching - } - - const fetchJobAds = async () => { - try { - const url = `${ApiEndpoint}/jobs?page=${currentPage}&limit=${adsPerPage}&search=${encodeURIComponent( - query, - )}&category=${encodeURIComponent(category)}`; - const response = await fetch(url, { method: 'GET' }); - if (!response.ok) throw new Error(`Failed to fetch jobs: ${response.statusText}`); - const data = await response.json(); - setJobAds(data.jobs); - setTotalPages(data.pagination.totalPages); - } catch (error) { - toast.error('Error fetching jobs'); - } - }; - - fetchJobAds(); - }, [query, category, currentPage]); - - // Handle form submission for search - const handleSubmit = e => { - e.preventDefault(); - const inputQuery = e.target.elements.searchInput.value.trim(); - setQuery(inputQuery); - setCurrentPage(1); - setHasSearched(inputQuery !== '' || category !== ''); - }; - - // Handle category change - const handleCategoryChange = e => { - const selectedValue = e.target.value; - setCategory(selectedValue); - setCurrentPage(1); // Reset page to 1 on category change - - // 👇 Reset hasSearched based on input - if (selectedValue === '' && query.trim() === '') { - setHasSearched(false); - } else { - setHasSearched(true); - } - }; - - // Pagination controls - const goToPreviousPage = () => { - if (currentPage > 1) setCurrentPage(currentPage - 1); - }; - - const goToNextPage = () => { - if (currentPage < totalPages) setCurrentPage(currentPage + 1); - }; - const getCategoryIcon = jobType => { - switch (jobType) { - case 'Engineering': - return 'https://img.icons8.com/external-prettycons-flat-prettycons/47/external-job-social-media-prettycons-flat-prettycons.png'; - case 'Marketing': - return 'https://img.icons8.com/external-justicon-lineal-color-justicon/64/external-marketing-marketing-and-growth-justicon-lineal-color-justicon-1.png'; - case 'Design': - return 'https://img.icons8.com/arcade/64/design.png'; - case 'Finance': - return 'https://img.icons8.com/cotton/64/merchant-account--v2.png'; - default: - return 'https://img.icons8.com/cotton/64/working-with-a-laptop--v1.png'; - } - }; - - return ( -
-
- - One Community Logo - -
- - - {/* Job ads listing */} -
- {jobAds.length > 0 && - jobAds.map(ad => ( -
- {`${ad.category} -

- {ad.title} -

- -
- {ad.location?.toLowerCase() !== 'remote' - ? `In-Person | Location: ${ad.location}` - : 'Remote'} -
- -

- {ad.description || 'No detailed description available.'} -

- - {ad.requirements && ad.requirements.length > 0 && ( -
-

Requirements:

-
    - {ad.requirements.map(req => ( -
  • {req}
  • - ))} -
-
- )} - - - - -
- ))} - - {jobAds.length === 0 && hasSearched && ( -
- No results -

No Job Ads Found

-

- We couldn’t find any matches. Try a different keyword or category. -

-
- )} - - {jobAds.length === 0 && !hasSearched && ( -
-

🔍 Begin Your Search

-

- Use the search bar or pick a category to explore available job roles! -

-
- {['Engineering', 'Marketing', 'Design', 'Finance'].map(cat => ( - - ))} -
-
- )} -
- - {/* Pagination */} - {totalPages > 1 && ( -
- - - Page {currentPage} of {totalPages} - - -
- )} -
- ); -} - -export default SuggestedJobsList; diff --git a/src/components/Collaboration/index.jsx b/src/components/Collaboration/index.jsx index 622a2f2b15..ebf0470919 100644 --- a/src/components/Collaboration/index.jsx +++ b/src/components/Collaboration/index.jsx @@ -1,3 +1,3 @@ -import SuggestedJobsList from './SuggestedJobsList'; +import Collaboration from './Collaboration'; -export default SuggestedJobsList; +export default Collaboration; diff --git a/src/components/LBDashboard/LBDashboard.jsx b/src/components/LBDashboard/LBDashboard.jsx index 4743a3cc85..b2f8982902 100644 --- a/src/components/LBDashboard/LBDashboard.jsx +++ b/src/components/LBDashboard/LBDashboard.jsx @@ -1,270 +1,12 @@ -import { useState } from 'react'; -import { - Container, - Row, - Col, - Button, - ButtonGroup, - ButtonDropdown, - DropdownToggle, - DropdownMenu, - DropdownItem, - Card, - CardBody, -} from 'reactstrap'; +import { Container } from 'reactstrap'; import styles from './LBDashboard.module.css'; -const METRIC_OPTIONS = { - DEMAND: [ - { key: 'pageVisits', label: 'Page Visits' }, // default overall - { key: 'numBids', label: 'Number of Bids' }, - { key: 'avgRating', label: 'Average Rating' }, - ], - REVENUE: [ - { key: 'avgBid', label: 'Average Bid' }, - { key: 'finalPrice', label: 'Final Price / Income' }, // default for Revenue - ], - VACANCY: [ - { key: 'occupancyRate', label: 'Occupancy Rate (% days not vacant)' }, // default for Vacancy - { key: 'avgStay', label: 'Average Duration of Stay' }, - ], -}; - -const DEFAULTS = { - DEMAND: 'pageVisits', - REVENUE: 'finalPrice', - VACANCY: 'occupancyRate', -}; - -function GraphCard({ title, metricLabel }) { - return ( - - -
- {title} - {metricLabel} -
-
- Graph area -
-
-
- ); -} - export function LBDashboard() { - const [activeCategory, setActiveCategory] = useState('DEMAND'); - const [selectedMetricKey, setSelectedMetricKey] = useState(DEFAULTS.DEMAND); - - const [openDD, setOpenDD] = useState({ DEMAND: false, REVENUE: false, VACANCY: false }); - - const metricLabel = (() => { - const all = Object.values(METRIC_OPTIONS).flat(); - return (all.find(o => o.key === selectedMetricKey) || {}).label || ''; - })(); - - const handleCategoryClick = category => { - setActiveCategory(category); - setSelectedMetricKey(DEFAULTS[category]); - }; - - const handleMetricPick = (category, key) => { - setActiveCategory(category); - setSelectedMetricKey(key); - }; - - const toggleDD = category => setOpenDD(s => ({ ...s, [category]: !s[category] })); - - const goBack = () => { - window.history.back(); - }; - return ( - - {/* Header */} -
-

Listing and Bidding Platform Dashboard

- + +
+

Biding Dashboard

- - {/* Preset Overview Filter */} -
-
Choose Metric to view
- - - {/* DEMAND */} - - toggleDD('DEMAND')} - className={styles.dd} - > - - - {METRIC_OPTIONS.DEMAND.map(m => ( - handleMetricPick('DEMAND', m.key)} - className={`${styles.dropdownItem} ${ - selectedMetricKey === m.key ? styles.dropdownActive : '' - }`} - > - {m.label} - - ))} - - - - {/* VACANCY */} - - toggleDD('VACANCY')} - className={styles.dd} - > - - - {METRIC_OPTIONS.VACANCY.map(m => ( - handleMetricPick('VACANCY', m.key)} - className={`${styles.dropdownItem} ${ - selectedMetricKey === m.key ? styles.dropdownActive : '' - }`} - > - {m.label} - - ))} - - - - {/* REVENUE */} - - toggleDD('REVENUE')} - className={styles.dd} - > - - - {METRIC_OPTIONS.REVENUE.map(m => ( - handleMetricPick('REVENUE', m.key)} - className={`${styles.dropdownItem} ${ - selectedMetricKey === m.key ? styles.dropdownActive : '' - }`} - > - {m.label} - - ))} - - - - -
- Current metric: {metricLabel} -
-
- - {/* By Village */} -
-
- By Village -
- - - - - - - - - - - -
-
-
- - {/* By Property */} -
-
- By Property -
- - - - - - - - -
-
-
- - {/* Insights from Reviews */} -
-
- Insights from Reviews -
- - - - -
Village Wordcloud
-
Wordcloud area
-
-
- - - - -
Property Wordcloud
-
Wordcloud area
-
-
- -
-
-
-
); } diff --git a/src/components/LBDashboard/LBDashboard.module.css b/src/components/LBDashboard/LBDashboard.module.css index 2a4ad66884..48ab005a59 100644 --- a/src/components/LBDashboard/LBDashboard.module.css +++ b/src/components/LBDashboard/LBDashboard.module.css @@ -5,213 +5,152 @@ body { padding: 0; } - - .dashboardContainer { - padding: 16px 20px 48px; - background: #faf9fb; + padding: 40px 20px; + max-width: 1400px; + margin: 0 auto; + background: #ffffff; + border-radius: 16px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); } .dashboardHeader { display: flex; - align-items: center; justify-content: space-between; - margin-bottom: 10px; -} - -.title { - font-size: 26px; - font-weight: 700; - margin: 0; -} - -.backBtn { - background: #000; - color: #fff; - border: none; - border-radius: 8px; - padding: 6px 12px; -} - -.filterBar { - background: #fff; - border: 1px solid #ece7f4; - border-radius: 14px; - padding: 14px 16px; - margin-bottom: 18px; - display: grid; - grid-template-columns: auto 1fr auto; - gap: 12px 16px; align-items: center; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(120deg, #ffffff, #f8f9fa); + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -.filterLabel { - font-size: 12px; - color: #666; -} - -.categoryGroup { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; -} - -/* Purple buttons */ -.filterBtn { - background: #6a4ff7 !important; - border-color: #6a4ff7 !important; - color: #fff !important; - border-radius: 16px; - padding: 6px 14px; - font-size: 14px; - font-weight: 500; +.dashboardHeader h1 { + font-size: 2.5rem; + font-weight: bold; + color: #2c3e50; } -.filterBtn:hover { - - color: #fff !important; +.dashboardSearchContainer input { + border: 2px solid #2c3e50; + border-radius: 25px; + padding: 12px 20px; + font-size: 1rem; + width: 100%; + max-width: 600px; + transition: all 0.3s ease; } -.filterBtn:focus { +.dashboardSearchContainer input:focus { + border-color: #34495e; + box-shadow: 0 0 8px rgba(52, 73, 94, 0.3); outline: none; - box-shadow: none !important; } -/* Active = black pill */ -.active { - background: #000 !important; - border-color: #000 !important; - color: #fff !important; -} - -/* Dropdown caret button same purple/black treatment */ -.dd :global(.dropdown-toggle) { - background: #6a4ff7 !important; - border-color: #6a4ff7 !important; - color: #fff !important; - border-radius: 16px !important; - padding: 6px 10px; -} - -.dd :global(.dropdown-toggle:hover) { - background: #593dd8 !important; - border-color: #593dd8 !important; - color: #fff !important; -} - - - - - -/* Dropdown menu + items */ -.dropdownMenu { - border-radius: 10px; - padding: 4px 0; -} - -.dropdownItem { - font-size: 14px; - padding: 6px 14px; -} - -.dropdownItem:hover { - background: #eae4fb; - color: #6a4ff7; -} - -.dropdownActive { - background: #000 !important; - color: #fff !important; -} - -.currentMetric { - justify-self: end; - font-size: 14px; -} - -.section { - margin-bottom: 14px; +.dashboardSidebar { + padding: 30px; + background: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -.sectionSummary { - list-style: none; - cursor: pointer; +.filterSection h4 { + font-size: 1.8rem; + color: #2c3e50; + margin-bottom: 20px; font-weight: 600; - font-size: 18px; - padding: 10px 6px; } -/* caret */ -.sectionSummary::before { - content: '▾'; - font-size: 18px; - margin-right: 8px; +.filterItem input, +.filterItem select { + padding: 12px 15px; + margin-top: 10px; + border: 1px solid #ddd; + border-radius: 8px; + width: 100%; + font-size: 1rem; + background: #ffffff; + transition: all 0.3s ease; } -.sectionBody { - background: #fff; - border: 1px solid #ece7f4; - border-radius: 14px; - padding: 14px; +.filterItem input:focus, +.filterItem select:focus { + border-color: #2c3e50; + box-shadow: 0 0 5px rgba(44, 62, 80, 0.4); + outline: none; } -.graphCard { - border: 1px solid #eee; +.dashboardMain { + padding: 30px; + background: #ffffff; border-radius: 12px; - min-height: 220px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -.graphTitle { - display: flex; - align-items: center; - justify-content: space-between; - font-weight: 600; - margin-bottom: 8px; +.sectionTitle { + font-size: 2.2rem; + color: #2c3e50; + margin-bottom: 20px; + font-weight: bold; + text-align: center; } -.metricPill { - font-size: 12px; - border: 1px solid #e2e2e2; - border-radius: 999px; - padding: 2px 8px; - background: #f6f6f8; +.eventCard { + margin-bottom: 20px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; } -.graphPlaceholder { - height: 160px; - border: 1px dashed #c9c9d4; - border-radius: 10px; - display: grid; - place-items: center; +.eventCard:hover { + transform: scale(1.05); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); } -.placeholderText { - font-size: 13px; - color: #999; +.eventCardImgContainer img { + width: 100%; + height: auto; + border-bottom: 3px solid #34495e; } -.wordcloudCard { - border: 1px solid #eee; - border-radius: 12px; - min-height: 240px; +.eventTitle { + text-align: center; + color: #2c3e50; + margin: 10px 0; + font-weight: bold; + font-size: 1.5rem; } -.wordcloudBody { +.eventDate, +.eventLocation, +.eventOrganizer { + font-size: 1rem; + color: #555; display: flex; - flex-direction: column; + align-items: center; gap: 8px; + margin: 5px 0; } -.wordcloudTitle { - font-weight: 600; +.dashboardActions { + text-align: center; + margin-top: 20px; } -.wordcloudPlaceholder { - flex: 1; - border: 1px dashed #c9c9d4; - border-radius: 10px; - display: grid; - place-items: center; - color: #999; +.dashboardActions button { + background-color: #2c3e50; + color: #ffffff; + border: none; + padding: 12px 25px; + font-size: 1rem; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: bold; } + +.dashboardActions button:hover { + background-color: #34495e; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} \ No newline at end of file diff --git a/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx b/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx index 19b43dd126..08259b450c 100644 --- a/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx +++ b/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx @@ -299,5 +299,5 @@ describe('UserPermissionsPopup component', () => { const addButtons = screen.queryAllByRole('button', { name: /add/i }); expect(addButtons.length).toBeGreaterThan(0); }); - }); -}, 10000); + }, 10000); +}); diff --git a/src/components/Projects/Members/FoundUser/FoundUser.jsx b/src/components/Projects/Members/FoundUser/FoundUser.jsx index 945e1c11e1..1f41f2ce39 100644 --- a/src/components/Projects/Members/FoundUser/FoundUser.jsx +++ b/src/components/Projects/Members/FoundUser/FoundUser.jsx @@ -34,7 +34,6 @@ const FoundUser = props => { 'Assign', props.firstName, props.lastName, - props.isActive, ); // Optionally, trigger a refresh or update local state if needed // e.g., props.onAssigned && props.onAssigned(props.uid); diff --git a/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx b/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx index 0f9debdfd7..3cdde7d6e0 100644 --- a/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx +++ b/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx @@ -33,7 +33,6 @@ describe('FoundUser Component', () => { uid: 'user123', firstName: 'John', lastName: 'Smith', - isActive: 'true', email: 'john.smith@example.com', assigned: false, projectId: 'project123', @@ -117,7 +116,6 @@ describe('FoundUser Component', () => { 'Assign', 'John', 'Smith', - 'true', ); }); }); diff --git a/src/components/Projects/Members/Members.jsx b/src/components/Projects/Members/Members.jsx index 63668b26e9..9c27f4d136 100644 --- a/src/components/Projects/Members/Members.jsx +++ b/src/components/Projects/Members/Members.jsx @@ -12,9 +12,8 @@ import { findProjectMembers, getAllUserProfiles, assignProject, - foundUsers -} from '~/actions/projectMembers'; - + foundUsers } from '~/actions/projectMembers'; + import Member from './Member'; import FoundUser from './FoundUser'; import './members.css'; @@ -57,7 +56,7 @@ const Members = props => { // Wait for all members to be assigned await Promise.all( allUsers.map(user => - props.assignProject(projectId, user._id, 'Assign', user.firstName, user.lastName, user.isActive), + props.assignProject(projectId, user._id, 'Assign', user.firstName, user.lastName), ), ); @@ -66,26 +65,18 @@ const Members = props => { useEffect(() => { if (!isLoading) { - setMembersList(props.state.projectMembers.members.filter(user => !showActiveMembersOnly || user.isActive)); + setMembersList(props.state.projectMembers.members); } }, [props.state.projectMembers.members, isLoading]); // ADDED: State for toggling display of active members only const [showActiveMembersOnly, setShowActiveMembersOnly] = useState(false); - useEffect(() => { - setMembersList(props.state.projectMembers.members?.filter(user => !showActiveMembersOnly || user.isActive)) - }, [showActiveMembersOnly]) - - useEffect(() => { - handleFind() - }, [membersList]) - // avoid re-filtering the netire list on every render - // const displayedMembers = useMemo( - // () => (showActiveMembersOnly ? membersList?.filter(member => member.isActive) : [...membersList]), - // [membersList, showActiveMembersOnly] - // ); + const displayedMembers = useMemo( + () => (showActiveMembersOnly ? membersList?.filter(member => member.isActive) : membersList), + [membersList, showActiveMembersOnly] + ); const handleToggle = async () => { setShowActiveMembersOnly(prevState => !prevState); @@ -99,9 +90,10 @@ const Members = props => { const currentValue = event.target.value; setQuery(currentValue); setSearchText(currentValue); - + if (lastTimeoutId !== null) clearTimeout(lastTimeoutId); + const timeoutId = setTimeout(() => { // Only call findUserProfiles if there's actual search text if (currentValue && currentValue.trim() !== '') { @@ -111,11 +103,11 @@ const Members = props => { setShowFindUserList(false); } }, 300); + setLastTimeoutId(timeoutId); }; - const handleFind = () => { - const q = (searchText || '').trim(); + const q = (searchText || '').trim(); if (!q) { setShowFindUserList(false); return; @@ -126,7 +118,7 @@ const Members = props => { return ( -
+
{ > @@ -758,10 +751,7 @@ const TeamMemberTasks = React.memo(props => { {teamList.length === 0 ? ( - + ) : ( teamList .filter(user => filterByUserFeatures(user)) @@ -820,7 +810,7 @@ const TeamMemberTasks = React.memo(props => { timeEntriesList .filter(timeEntry => timeEntry.personId === user.personId) .map(timeEntry => ( - + - - - - @@ -244,7 +243,6 @@ export function TaskDifferenceModal({ diff --git a/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx b/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx index c6f5b5547e..ed3e600e80 100644 --- a/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx +++ b/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, within } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; @@ -85,7 +85,7 @@ describe('TaskDifferenceModal component', () => { }); it('check if modal is open when isOpen is set to true', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('Task Info Changes')).toBeInTheDocument(); + expect(screen.queryByText('Task Info Changes')).toBeInTheDocument(); }); it('check if modal is not open when isOpen is set to false', () => { renderComponent(false, 'abc123', taskNotifications); @@ -93,7 +93,7 @@ describe('TaskDifferenceModal component', () => { }); it('check if modal body does get displayed when task notification user id is same as user id', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('White Bold = No Changes')).toBeInTheDocument(); + expect(screen.queryByText('White Bold = No Changes')).toBeInTheDocument(); }); it('check if modal body does not get displayed when task notification user id is not same as user id', () => { renderComponent(true, 'ghi123', taskNotifications); @@ -106,53 +106,72 @@ describe('TaskDifferenceModal component', () => { }); it('check if task name and Task Name header gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('Task Name')).toBeInTheDocument(); - expect(screen.getByText(`${taskNotifications[0].oldTask.taskName}`)).toBeInTheDocument(); + expect(screen.queryByText('Task Name')).toBeInTheDocument(); + expect(screen.queryByText(`${taskNotifications[0].oldTask.taskName}`)).toBeInTheDocument(); }); it('check if priority and Assigned gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('Priority')).toBeInTheDocument(); - expect(screen.getByText('Primary')).toBeInTheDocument(); - expect(screen.getByText('Assigned')).toBeInTheDocument(); - expect(screen.getByText('Yes')).toBeInTheDocument(); + expect(screen.queryByText('Priority')).toBeInTheDocument(); + expect(screen.queryByText('Primary')).toBeInTheDocument(); + expect(screen.queryByText('Assigned')).toBeInTheDocument(); + expect(screen.queryByText('Yes')).toBeInTheDocument(); }); it('check if status and Hours - Best-case gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('Status')).toBeInTheDocument(); - expect(screen.getByText('Complete')).toBeInTheDocument(); + expect(screen.queryByText('Status')).toBeInTheDocument(); + expect(screen.queryByText('Complete')).toBeInTheDocument(); - expect(screen.getByTestId('hours-best-value')).toHaveTextContent('10'); + const hoursBestLabel = screen.getByText('Hours - Best-case'); + + expect(screen.queryByText('Hours - Best-case')).toBeInTheDocument(); + const hoursBestValueElement = hoursBestLabel.nextElementSibling; + const spanElement = hoursBestValueElement.querySelector('span'); + expect(spanElement.textContent).toBe('10'); }); it('check if Hours - Worst-case and Hours - Most-case gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('Hours - Worst-case')).toBeInTheDocument(); + expect(screen.queryByText('Hours - Worst-case')).toBeInTheDocument(); + + const hoursWorstLabel = screen.getByText('Hours - Worst-case'); + const hoursWorstValueElement = hoursWorstLabel.nextElementSibling; + const spanElement = hoursWorstValueElement.querySelector('span'); + expect(spanElement.textContent).toBe('13'); - expect(screen.getByTestId('hours-worst-value')).toHaveTextContent('13'); - expect(screen.getByText('Hours - Most-case')).toBeInTheDocument(); - expect(screen.getByTestId('hours-most-value')).toHaveTextContent('13'); + expect(screen.queryByText('Hours - Most-case')).toBeInTheDocument(); + const hoursMostLabel = screen.getByText('Hours - Most-case'); + const hoursMostValueElement = hoursMostLabel.nextElementSibling; + const hoursMostSpanElement = hoursMostValueElement.querySelector('span'); + expect(hoursMostSpanElement.textContent).toBe('13'); }); it('check if estimated hours and Classification get displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('Estimated Hours')).toBeInTheDocument(); - expect(screen.getByTestId('estimated-hours-value')).toHaveTextContent('11'); - expect(screen.getByText('Classification')).toBeInTheDocument(); - expect(screen.getByText('Example')).toBeInTheDocument(); + expect(screen.queryByText('Estimated Hours')).toBeInTheDocument(); + const estimatedHoursLabel = screen.getByText('Estimated Hours'); + const estimatedHoursValueElement = estimatedHoursLabel.nextElementSibling; + const estimatedHoursSpanElement = estimatedHoursValueElement.querySelector('span'); + expect(estimatedHoursSpanElement.textContent).toBe('11'); + + expect(screen.queryByText('Classification')).toBeInTheDocument(); + expect(screen.queryByText('Example')).toBeInTheDocument(); }); it('check if Why this Task is Important and Design Intent get displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.getByText('Why this Task is Important')).toBeInTheDocument(); - expect(screen.getByText('some reason')).toBeInTheDocument(); - expect(screen.getByText('Design Intent')).toBeInTheDocument(); - expect(screen.getByText('Food')).toBeInTheDocument(); + expect(screen.queryByText('Why this Task is Important')).toBeInTheDocument(); + expect(screen.queryByText('some reason')).toBeInTheDocument(); + expect(screen.queryByText('Design Intent')).toBeInTheDocument(); + expect(screen.queryByText('Food')).toBeInTheDocument(); }); it('check if Start Date and End Date does not get displayed when Start and End Date is set to null', () => { renderComponent(true, 'abc123', taskNotifications); + const startDateLabel = screen.getByText('Start Date'); + const startDateElement = startDateLabel.nextElementSibling; + const startSpanElement = startDateElement.querySelector('span'); + expect(startSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); - expect(screen.getByText('Start Date')).toBeInTheDocument(); - expect(screen.getByText('End Date')).toBeInTheDocument(); - - expect(screen.queryByTestId('start-date-value')).not.toBeInTheDocument(); - expect(screen.queryByTestId('end-date-value')).not.toBeInTheDocument(); + const endDateLabel = screen.getByText('End Date'); + const endDateElement = endDateLabel.nextElementSibling; + const endSpanElement = endDateElement.querySelector('span'); + expect(endSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); }); it('check if Start Date and End Date get displayed when Start and End Date is not set to null', () => { const newTaskNotifications = [ @@ -177,12 +196,15 @@ describe('TaskDifferenceModal component', () => { }); it('check if links, resources does not get displayed when the array size is 0', () => { renderComponent(true, 'abc123', taskNotifications); + const resourceLabel = screen.getByText('Resources'); + const resourceElement = resourceLabel.nextElementSibling; + const resourceSpanElement = resourceElement.querySelector('span'); + expect(resourceSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); - expect(screen.getByText('Resources')).toBeInTheDocument(); - expect(screen.getByText('Links')).toBeInTheDocument(); - - expect(screen.queryByTestId('resources-value')).not.toBeInTheDocument(); - expect(screen.queryByTestId('links-value')).not.toBeInTheDocument(); + const linksLabel = screen.getByText('Links'); + const linksElement = linksLabel.nextElementSibling; + const linksSpanElement = linksElement.querySelector('span'); + expect(linksSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); }); it('check if links and resources get displayed as expected when links and resources arrays are set to entries more than 0', () => { const newResource = [ diff --git a/src/components/Teams/TeamMembersPopup.jsx b/src/components/Teams/TeamMembersPopup.jsx index 6a7c244a2e..4c4e030bc2 100644 --- a/src/components/Teams/TeamMembersPopup.jsx +++ b/src/components/Teams/TeamMembersPopup.jsx @@ -215,7 +215,6 @@ export const TeamMembersPopup = React.memo(props => { { {checkedStatus} - - + + + + + + + + + + + + + + + + {/* By Property */} +
+
+ By Property +
+ +
+ + + + + + + + + + + {/* Insights from Reviews */} +
+
+ Insights from Reviews +
+ +
+ + +
Village Wordcloud
+
Wordcloud area
+
+
+ + + + +
Property Wordcloud
+
Wordcloud area
+
+
+ + + + + ); } diff --git a/src/components/LBDashboard/LBDashboard.module.css b/src/components/LBDashboard/LBDashboard.module.css index 48ab005a59..2a4ad66884 100644 --- a/src/components/LBDashboard/LBDashboard.module.css +++ b/src/components/LBDashboard/LBDashboard.module.css @@ -5,152 +5,213 @@ body { padding: 0; } + + .dashboardContainer { - padding: 40px 20px; - max-width: 1400px; - margin: 0 auto; - background: #ffffff; - border-radius: 16px; - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); + padding: 16px 20px 48px; + background: #faf9fb; } .dashboardHeader { display: flex; + align-items: center; justify-content: space-between; + margin-bottom: 10px; +} + +.title { + font-size: 26px; + font-weight: 700; + margin: 0; +} + +.backBtn { + background: #000; + color: #fff; + border: none; + border-radius: 8px; + padding: 6px 12px; +} + +.filterBar { + background: #fff; + border: 1px solid #ece7f4; + border-radius: 14px; + padding: 14px 16px; + margin-bottom: 18px; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 12px 16px; align-items: center; - margin-bottom: 30px; - padding: 20px; - background: linear-gradient(120deg, #ffffff, #f8f9fa); - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -.dashboardHeader h1 { - font-size: 2.5rem; - font-weight: bold; - color: #2c3e50; +.filterLabel { + font-size: 12px; + color: #666; } -.dashboardSearchContainer input { - border: 2px solid #2c3e50; - border-radius: 25px; - padding: 12px 20px; - font-size: 1rem; - width: 100%; - max-width: 600px; - transition: all 0.3s ease; +.categoryGroup { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +/* Purple buttons */ +.filterBtn { + background: #6a4ff7 !important; + border-color: #6a4ff7 !important; + color: #fff !important; + border-radius: 16px; + padding: 6px 14px; + font-size: 14px; + font-weight: 500; +} + +.filterBtn:hover { + + color: #fff !important; } -.dashboardSearchContainer input:focus { - border-color: #34495e; - box-shadow: 0 0 8px rgba(52, 73, 94, 0.3); +.filterBtn:focus { outline: none; + box-shadow: none !important; } -.dashboardSidebar { - padding: 30px; - background: #f9f9f9; - border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +/* Active = black pill */ +.active { + background: #000 !important; + border-color: #000 !important; + color: #fff !important; +} + +/* Dropdown caret button same purple/black treatment */ +.dd :global(.dropdown-toggle) { + background: #6a4ff7 !important; + border-color: #6a4ff7 !important; + color: #fff !important; + border-radius: 16px !important; + padding: 6px 10px; } -.filterSection h4 { - font-size: 1.8rem; - color: #2c3e50; - margin-bottom: 20px; +.dd :global(.dropdown-toggle:hover) { + background: #593dd8 !important; + border-color: #593dd8 !important; + color: #fff !important; +} + + + + + +/* Dropdown menu + items */ +.dropdownMenu { + border-radius: 10px; + padding: 4px 0; +} + +.dropdownItem { + font-size: 14px; + padding: 6px 14px; +} + +.dropdownItem:hover { + background: #eae4fb; + color: #6a4ff7; +} + +.dropdownActive { + background: #000 !important; + color: #fff !important; +} + +.currentMetric { + justify-self: end; + font-size: 14px; +} + +.section { + margin-bottom: 14px; +} + +.sectionSummary { + list-style: none; + cursor: pointer; font-weight: 600; + font-size: 18px; + padding: 10px 6px; } -.filterItem input, -.filterItem select { - padding: 12px 15px; - margin-top: 10px; - border: 1px solid #ddd; - border-radius: 8px; - width: 100%; - font-size: 1rem; - background: #ffffff; - transition: all 0.3s ease; +/* caret */ +.sectionSummary::before { + content: '▾'; + font-size: 18px; + margin-right: 8px; } -.filterItem input:focus, -.filterItem select:focus { - border-color: #2c3e50; - box-shadow: 0 0 5px rgba(44, 62, 80, 0.4); - outline: none; +.sectionBody { + background: #fff; + border: 1px solid #ece7f4; + border-radius: 14px; + padding: 14px; } -.dashboardMain { - padding: 30px; - background: #ffffff; +.graphCard { + border: 1px solid #eee; border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-height: 220px; } -.sectionTitle { - font-size: 2.2rem; - color: #2c3e50; - margin-bottom: 20px; - font-weight: bold; - text-align: center; +.graphTitle { + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 600; + margin-bottom: 8px; } -.eventCard { - margin-bottom: 20px; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1); - transition: transform 0.3s ease, box-shadow 0.3s ease; +.metricPill { + font-size: 12px; + border: 1px solid #e2e2e2; + border-radius: 999px; + padding: 2px 8px; + background: #f6f6f8; } -.eventCard:hover { - transform: scale(1.05); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); +.graphPlaceholder { + height: 160px; + border: 1px dashed #c9c9d4; + border-radius: 10px; + display: grid; + place-items: center; } -.eventCardImgContainer img { - width: 100%; - height: auto; - border-bottom: 3px solid #34495e; +.placeholderText { + font-size: 13px; + color: #999; } -.eventTitle { - text-align: center; - color: #2c3e50; - margin: 10px 0; - font-weight: bold; - font-size: 1.5rem; +.wordcloudCard { + border: 1px solid #eee; + border-radius: 12px; + min-height: 240px; } -.eventDate, -.eventLocation, -.eventOrganizer { - font-size: 1rem; - color: #555; +.wordcloudBody { display: flex; - align-items: center; + flex-direction: column; gap: 8px; - margin: 5px 0; } -.dashboardActions { - text-align: center; - margin-top: 20px; +.wordcloudTitle { + font-weight: 600; } -.dashboardActions button { - background-color: #2c3e50; - color: #ffffff; - border: none; - padding: 12px 25px; - font-size: 1rem; - border-radius: 8px; - cursor: pointer; - transition: all 0.3s ease; - font-weight: bold; +.wordcloudPlaceholder { + flex: 1; + border: 1px dashed #c9c9d4; + border-radius: 10px; + display: grid; + place-items: center; + color: #999; } - -.dashboardActions button:hover { - background-color: #34495e; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); -} \ No newline at end of file diff --git a/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx b/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx index 08259b450c..19b43dd126 100644 --- a/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx +++ b/src/components/PermissionsManagement/__tests__/UserPermissionsPopup.test.jsx @@ -299,5 +299,5 @@ describe('UserPermissionsPopup component', () => { const addButtons = screen.queryAllByRole('button', { name: /add/i }); expect(addButtons.length).toBeGreaterThan(0); }); - }, 10000); -}); + }); +}, 10000); diff --git a/src/components/Projects/Members/FoundUser/FoundUser.jsx b/src/components/Projects/Members/FoundUser/FoundUser.jsx index 1f41f2ce39..945e1c11e1 100644 --- a/src/components/Projects/Members/FoundUser/FoundUser.jsx +++ b/src/components/Projects/Members/FoundUser/FoundUser.jsx @@ -34,6 +34,7 @@ const FoundUser = props => { 'Assign', props.firstName, props.lastName, + props.isActive, ); // Optionally, trigger a refresh or update local state if needed // e.g., props.onAssigned && props.onAssigned(props.uid); diff --git a/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx b/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx index 3cdde7d6e0..0f9debdfd7 100644 --- a/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx +++ b/src/components/Projects/Members/FoundUser/__tests__/FoundUser.test.jsx @@ -33,6 +33,7 @@ describe('FoundUser Component', () => { uid: 'user123', firstName: 'John', lastName: 'Smith', + isActive: 'true', email: 'john.smith@example.com', assigned: false, projectId: 'project123', @@ -116,6 +117,7 @@ describe('FoundUser Component', () => { 'Assign', 'John', 'Smith', + 'true', ); }); }); diff --git a/src/components/Projects/Members/Members.jsx b/src/components/Projects/Members/Members.jsx index 9c27f4d136..63668b26e9 100644 --- a/src/components/Projects/Members/Members.jsx +++ b/src/components/Projects/Members/Members.jsx @@ -12,8 +12,9 @@ import { findProjectMembers, getAllUserProfiles, assignProject, - foundUsers } from '~/actions/projectMembers'; - + foundUsers +} from '~/actions/projectMembers'; + import Member from './Member'; import FoundUser from './FoundUser'; import './members.css'; @@ -56,7 +57,7 @@ const Members = props => { // Wait for all members to be assigned await Promise.all( allUsers.map(user => - props.assignProject(projectId, user._id, 'Assign', user.firstName, user.lastName), + props.assignProject(projectId, user._id, 'Assign', user.firstName, user.lastName, user.isActive), ), ); @@ -65,18 +66,26 @@ const Members = props => { useEffect(() => { if (!isLoading) { - setMembersList(props.state.projectMembers.members); + setMembersList(props.state.projectMembers.members.filter(user => !showActiveMembersOnly || user.isActive)); } }, [props.state.projectMembers.members, isLoading]); // ADDED: State for toggling display of active members only const [showActiveMembersOnly, setShowActiveMembersOnly] = useState(false); + useEffect(() => { + setMembersList(props.state.projectMembers.members?.filter(user => !showActiveMembersOnly || user.isActive)) + }, [showActiveMembersOnly]) + + useEffect(() => { + handleFind() + }, [membersList]) + // avoid re-filtering the netire list on every render - const displayedMembers = useMemo( - () => (showActiveMembersOnly ? membersList?.filter(member => member.isActive) : membersList), - [membersList, showActiveMembersOnly] - ); + // const displayedMembers = useMemo( + // () => (showActiveMembersOnly ? membersList?.filter(member => member.isActive) : [...membersList]), + // [membersList, showActiveMembersOnly] + // ); const handleToggle = async () => { setShowActiveMembersOnly(prevState => !prevState); @@ -90,10 +99,9 @@ const Members = props => { const currentValue = event.target.value; setQuery(currentValue); setSearchText(currentValue); - + if (lastTimeoutId !== null) clearTimeout(lastTimeoutId); - const timeoutId = setTimeout(() => { // Only call findUserProfiles if there's actual search text if (currentValue && currentValue.trim() !== '') { @@ -103,11 +111,11 @@ const Members = props => { setShowFindUserList(false); } }, 300); - setLastTimeoutId(timeoutId); }; + const handleFind = () => { - const q = (searchText || '').trim(); + const q = (searchText || '').trim(); if (!q) { setShowFindUserList(false); return; @@ -118,7 +126,7 @@ const Members = props => { return ( -
+
({ darkMode: state.theme.darkMode, }); -TeamMemberTasks.displayName = 'TeamMemberTasks'; - export default connect(mapStateToProps, null)(TeamMemberTasks); diff --git a/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx b/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx index 8f1366d50a..655915467b 100644 --- a/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx +++ b/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { configureStore } from 'redux-mock-store'; import { themeMock } from '../../../__tests__/mockStates'; @@ -29,9 +29,9 @@ describe('DiffedText Component', () => { , ); - expect(screen.getByText('Hello')).toHaveStyle('color: rgb(255, 255, 255)'); - expect(screen.getByText('world')).toHaveStyle('color: rgb(255, 255, 255)'); - expect(screen.getByText('React')).toHaveStyle('color: rgb(0, 128, 0)'); + expect(getByText('Hello')).toHaveStyle('color: rgb(255, 255, 255)'); + expect(getByText('world')).toHaveStyle('color: rgb(255, 255, 255)'); + expect(getByText('React')).toHaveStyle('color: rgb(0, 128, 0)'); }); it('handles text removal correctly', () => { @@ -44,7 +44,7 @@ describe('DiffedText Component', () => { , ); - expect(screen.getByText('world')).toHaveStyle( + expect(getByText('world')).toHaveStyle( 'textDecorationLine: line-through; color: rgb(255, 0, 0)', ); }); diff --git a/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx b/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx index 3ab0c787da..20364314ab 100644 --- a/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx +++ b/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx @@ -1,6 +1,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { Provider, useDispatch } from 'react-redux'; +import { Provider } from 'react-redux'; import { configureStore } from 'redux-mock-store'; +import { useDispatch } from 'react-redux'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import TaskButton from '../TaskButton'; @@ -110,13 +111,10 @@ describe('TaskButton', () => { endstateInfo: task.endstateInfo, classification: task.classification, }); + expect(deleteSelectedTaskSpy).toHaveBeenCalledWith(task._id, task.mother); + expect(getAllUserProfileSpy).toHaveBeenCalled(); + expect(fetchAllTasksSpy).toHaveBeenCalled(); }); - - await waitFor(() => expect(deleteSelectedTaskSpy).toHaveBeenCalledWith(task._id, task.mother)); - - await waitFor(() => expect(getAllUserProfileSpy).toHaveBeenCalled()); - - await waitFor(() => expect(fetchAllTasksSpy).toHaveBeenCalled()); }); test('does not render button when task status is Complete', () => { diff --git a/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx b/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx index 140b6f5ff8..7f3a0e521e 100644 --- a/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx +++ b/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx @@ -221,8 +221,10 @@ describe('TeamMemberTasks component', () => { , ); - - expect(screen.getAllByTestId('team-member-tasks-row')).not.toHaveLength(0); + const skeletonLoadingElement = container.querySelector( + '.skeleton-loading-team-member-tasks-row', + ); + expect(skeletonLoadingElement).toBeInTheDocument(); }); it('check if the skeleton loading html elements are not shown when isLoading is false', () => { axios.get.mockResolvedValue({ @@ -237,7 +239,10 @@ describe('TeamMemberTasks component', () => { , ); - expect(screen.queryByTestId('team-member-tasks-row')).not.toBeInTheDocument(); + const skeletonLoadingElement = container.querySelector( + '.skeleton-loading-team-member-tasks-row', + ); + expect(skeletonLoadingElement).not.toBeInTheDocument(); }); it('check if class names does not include color when dark mode is false', () => { axios.get.mockResolvedValue({ @@ -252,23 +257,32 @@ describe('TeamMemberTasks component', () => { , ); - - const timeOffButton = screen.getByTestId('show-time-off-btn'); - expect(timeOffButton).toBeInTheDocument(); - - expect(screen.getByTestId('team-member-tasks-container')).toBeInTheDocument(); - - fireEvent.click(timeOffButton); - - expect(screen.getByTitle('Timelogs submitted in the past 1 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 2 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 3 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 4 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 7 days')).toBeInTheDocument(); + const darkModeElement = container.querySelector('.container.team-member-tasks'); + const timeOffElement = container.querySelector('.show-time-off-btn'); + const hoursCompletedElement = container.querySelector('.team-member-tasks-subtable'); + const oneDayElement = container.querySelector( + '[title="Timelogs submitted in the past 1 days"]', + ); + const twoDayElement = container.querySelector( + '[title="Timelogs submitted in the past 2 days"]', + ); + const threeDayElement = container.querySelector( + '[title="Timelogs submitted in the past 3 days"]', + ); + const fourDayElement = container.querySelector( + '[title="Timelogs submitted in the past 4 days"]', + ); + const sevenDayElement = container.querySelector( + '[title="Timelogs submitted in the past 7 days"]', + ); + expect(darkModeElement).toBeInTheDocument(); + expect(hoursCompletedElement).toBeInTheDocument(); + expect(timeOffElement).toBeInTheDocument(); + expect(oneDayElement).toBeInTheDocument(); + expect(twoDayElement).toBeInTheDocument(); + expect(threeDayElement).toBeInTheDocument(); + expect(fourDayElement).toBeInTheDocument(); + expect(sevenDayElement).toBeInTheDocument(); }); it('check if class names does include color when dark mode is true', () => { axios.get.mockResolvedValue({ @@ -295,22 +309,32 @@ describe('TeamMemberTasks component', () => { , ); - - expect(screen.getByTestId('team-member-tasks-subtable')).toBeInTheDocument(); - - expect(screen.getByTestId('team-member-tasks-container')).toBeInTheDocument(); - - fireEvent.click(screen.getByTestId('show-time-off-btn')); - - expect(screen.getByTitle('Timelogs submitted in the past 1 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 2 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 3 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 4 days')).toBeInTheDocument(); - - expect(screen.getByTitle('Timelogs submitted in the past 7 days')).toBeInTheDocument(); + const darkModeElement = container.querySelector('.container.team-member-tasks'); + const timeOffElement = container.querySelector('.show-time-off-btn'); + const hoursCompletedElement = container.querySelector('.team-member-tasks-subtable'); + const oneDayElement = container.querySelector( + '[title="Timelogs submitted in the past 1 days"]', + ); + const twoDayElement = container.querySelector( + '[title="Timelogs submitted in the past 2 days"]', + ); + const threeDayElement = container.querySelector( + '[title="Timelogs submitted in the past 3 days"]', + ); + const fourDayElement = container.querySelector( + '[title="Timelogs submitted in the past 4 days"]', + ); + const sevenDayElement = container.querySelector( + '[title="Timelogs submitted in the past 7 days"]', + ); + expect(darkModeElement).toBeInTheDocument(); + expect(hoursCompletedElement).toBeInTheDocument(); + expect(timeOffElement).toBeInTheDocument(); + expect(oneDayElement).toBeInTheDocument(); + expect(twoDayElement).toBeInTheDocument(); + expect(threeDayElement).toBeInTheDocument(); + expect(fourDayElement).toBeInTheDocument(); + expect(sevenDayElement).toBeInTheDocument(); }); it('check if show time off button works as expected', () => { axios.get.mockResolvedValue({ @@ -324,15 +348,14 @@ describe('TeamMemberTasks component', () => { , ); - const buttonElement = screen.getByTestId('show-time-off-btn'); - - expect(screen.getByTestId('time-off-calendar-icon')).toBeInTheDocument(); - expect(screen.getByTestId('show-time-off-icon')).toBeInTheDocument(); - + const buttonElement = container.querySelector('[class="m-1 show-time-off-btn"]'); + expect(container.querySelector('[class="show-time-off-calender-svg"]')).toBeInTheDocument(); + expect(container.querySelector('[class="show-time-off-icon"]')).toBeInTheDocument(); fireEvent.click(buttonElement); - - expect(screen.getByTestId('time-off-calendar-icon')).toBeInTheDocument(); - expect(screen.getByTestId('show-time-off-icon')).toBeInTheDocument(); + const iconElement = container.querySelector('[class="show-time-off-calender-svg"]'); + expect(iconElement).toBeInTheDocument(); + const newIconElement = container.querySelector('[class="show-time-off-icon"]'); + expect(newIconElement).toBeInTheDocument(); }); it('check if days button works as expected', () => { axios.get.mockResolvedValue({ status: 200, data: '' }); @@ -372,6 +395,6 @@ describe('TeamMemberTasks component', () => { , ); - expect(screen.queryByTestId('table-row')).not.toBeInTheDocument(); + expect(container.querySelector('[className="table-row"]')).not.toBeInTheDocument(); }); }); diff --git a/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx b/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx index bd54dfe283..a9a9c4f61a 100644 --- a/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx +++ b/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { boxStyle, boxStyleDark } from '~/styles'; import '../../Header/DarkMode.css'; import { toast } from 'react-toastify'; +import '../../Header/DarkMode.css'; const TaskCompletedModal = React.memo(props => { const { darkMode } = props; @@ -93,6 +94,4 @@ const TaskCompletedModal = React.memo(props => { ); }); -TaskCompletedModal.displayName = 'TaskCompletedModal'; - export default TaskCompletedModal; diff --git a/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx b/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx index bba680d9af..76ecc678af 100644 --- a/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx +++ b/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx @@ -148,7 +148,7 @@ export function TaskDifferenceModal({
Hours - Best-case +
Hours - Worst-case +
Hours - Most-case +
Estimated Hours +
#User Name#User Name Date Added{' '} { ); }); -TeamMembersPopup.displayName = 'TeamMembersPopup'; - export default connect(null, { hasPermission })(TeamMembersPopup); diff --git a/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx b/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx index 3a18535156..6eddd801c9 100644 --- a/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx +++ b/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx @@ -18,24 +18,6 @@ import AssignTeamField from './AssignTeamField'; import AssignTeamCodeField from './AssignTeamCodeField'; import '../../Header/DarkMode.css'; -// ---- helpers --------------------------------------------------------------- - -const normalizeTeam = (value, teams = []) => { - if (!value) return null; - - // already object - if (typeof value === 'object') { - const found = teams.find(t => t?._id === value._id) || null; - return found ? { _id: found._id, teamName: found.teamName || '' } : null; - } - // id string - if (typeof value === 'string') { - const found = teams.find(t => t?._id === value) || null; - return found ? { _id: found._id, teamName: found.teamName || '' } : null; - } - return null; -}; - function AddNewTitleModal({ isOpen, setIsOpen, @@ -46,50 +28,45 @@ function AddNewTitleModal({ setShowMessage, editMode, title, - QSTTeamCodes, + QSTTeamCodes }) { const darkMode = useSelector(state => state.theme.darkMode); const teamCodes = useSelector(state => state.teamCodes?.teamCodes || []); - - // ------------------------- state ----------------------------------------- - + const [titleData, setTitleData] = useState(() => { - if (editMode && title && Object.keys(title).length !== 0) { + if (editMode && Object.keys(title).length !== 0) { return { id: title._id, - titleName: title.titleName || '', - titleCode: title.titleCode || ((title.titleName || '').slice(0, 7)), - mediaFolder: title.mediaFolder || '', - teamCode: title.teamCode || '', - projectAssigned: title.projectAssigned || '', - // safe default even if server had null - teamAssiged: title.teamAssiged || { _id: '', teamName: '' }, + titleName: title.titleName, + titleCode: title.titleCode, + mediaFolder: title.mediaFolder, + teamCode: title.teamCode, + projectAssigned: title.projectAssigned, + teamAssiged: title.teamAssiged == undefined ? { teamName: '', _id: '' } : title.teamAssiged, }; } - // non-edit defaults return { titleName: '', titleCode: '', mediaFolder: '', teamCode: '', projectAssigned: '', - teamAssiged: { _id: '', teamName: '' }, + teamAssiged: {}, }; }); const [isValidTeamCode, setIsValidTeamCode] = useState(true); - // keep titleData in sync when props change useEffect(() => { - if (editMode && title && Object.keys(title).length !== 0) { + if (editMode && Object.keys(title).length !== 0) { setTitleData({ id: title._id, - titleName: title.titleName || '', - titleCode: title.titleCode || ((title.titleName || '').slice(0, 7)), - mediaFolder: title.mediaFolder || '', - teamCode: title.teamCode || '', - projectAssigned: title.projectAssigned || '', - teamAssiged: title.teamAssiged || { _id: '', teamName: '' }, + titleName: title.titleName, + titleCode: title.titleCode || title.titleName.slice(0, 7), + mediaFolder: title.mediaFolder, + teamCode: title.teamCode, + projectAssigned: title.projectAssigned, + teamAssiged: title.teamAssiged, }); } else { setTitleData({ @@ -98,45 +75,36 @@ function AddNewTitleModal({ mediaFolder: '', teamCode: '', projectAssigned: '', - teamAssiged: { _id: '', teamName: '' }, + teamAssiged: {}, }); } }, [editMode, title]); - // auto-fill titleCode from titleName useEffect(() => { - const base = titleData?.titleName || ''; - const code = titleData?.titleCode ? titleData.titleCode : base.slice(0, 7); - setTitleData(prev => ({ ...prev, titleCode: code })); + const titleCode = titleData?.titleCode ? titleData.titleCode : titleData.titleName.slice(0, 7); + setTitleData(prev => ({ + ...prev, + titleCode, + })); }, [titleData.titleName]); - // live teamCode validity (using QSTTeamCodes list) useEffect(() => { setIsValidTeamCode( - titleData.teamCode === '' || - (Array.isArray(QSTTeamCodes) && - QSTTeamCodes.some(code => code?.value === titleData.teamCode)) + titleData.teamCode === '' || QSTTeamCodes.some(code => code.value === titleData.teamCode), ); }, [titleData.teamCode, QSTTeamCodes]); - // ----------------- canonical lists for validation ------------------------ - - // Accept both shapes for teamsData: array OR { allTeams, allTeamCode } - const allTeamsArray = Array.isArray(teamsData) - ? teamsData - : (teamsData && Array.isArray(teamsData.allTeams) ? teamsData.allTeams : []); - - let existTeamCodes = new Set( - (Array.isArray(teamsData?.allTeamCode?.distinctTeamCodes) - ? teamsData.allTeamCode.distinctTeamCodes - : []) - ); - - const existTeamName = new Set( - allTeamsArray.map(t => t?.teamName).filter(Boolean) - ); + let existTeamCodes = new Set(); + let existTeamName = new Set(); - // ------------------- local UI state (selectors) -------------------------- + if (teamsData?.allTeams) { + const codes = teamsData.allTeams.map(team => team.teamCode); + const names = teamsData.allTeams.map(team => team.teamName); + // Use allTeamCode rather than allTeams since team code is not related to records in the Team table. + // It is all distinct team codes from the UserProfile teamCode field. + existTeamCodes = new Set(teamsData?.allTeamCode?.distinctTeamCodes); + existTeamName = new Set(names); + } const [selectedTeam, onSelectTeam] = useState(undefined); const [selectedProject, onSelectProject] = useState(undefined); @@ -144,144 +112,146 @@ function AddNewTitleModal({ const [isValidProject, onValidation] = useState(false); const [searchText, setSearchText] = useState(''); // For addTeamAutoComplete - // ------------------- field handlers -------------------------------------- - const selectProject = project => { onSelectProject(project); - setTitleData(prev => ({ - ...prev, + setTitleData({ + ...titleData, projectAssigned: { projectName: project.projectName, _id: project._id, category: project.category, }, - })); + }); onValidation(true); }; const selectTeamCode = teamCode => { onSelectTeamCode(teamCode); - setTitleData(prev => ({ ...prev, teamCode })); + setTitleData({ + ...titleData, + teamCode, + }); }; const cleanProjectAssign = () => { - setTitleData(prev => ({ ...prev, projectAssigned: '' })); + setTitleData({ + ...titleData, + projectAssigned: '', + }); }; const selectTeam = team => { onSelectTeam(team); setTitleData(prev => ({ ...prev, - teamAssiged: { teamName: team.teamName, _id: team._id }, + teamAssiged: { + teamName: team.teamName, + _id: team._id, + }, })); onValidation(true); }; const cleanTeamCodeAssign = () => { - setTitleData(prev => ({ ...prev, teamCode: '' })); + setTitleData({ + ...titleData, + teamCode: '', + }); }; const cleanTeamAssigned = () => { - const updated = { ...titleData }; - delete updated.teamAssiged; - setTitleData(updated); + // if clean all input field -> no team selected + const updatedTitleData = { ...titleData }; + delete updatedTitleData.teamAssiged; + setTitleData(updatedTitleData); }; const undoTeamAssigned = () => { - setTitleData(prev => ({ - ...prev, - teamAssiged: { teamName: searchText, _id: 'N/A' }, - })); + setTitleData({ + ...titleData, + teamAssiged: { + teamName: searchText, + _id: 'N/A', + }, + }); }; - // ------------------- validations ----------------------------------------- + // confirm and save + const confirmOnClick = () => { + const isValidTeamName = onTeamNameValidation(titleData.teamAssiged); + + if (!isValidTeamName) { + return; + } + + if (editMode) { + editTitle(titleData) + .then(resp => { + if (resp.status !== 200) { + setWarningMessage({ title: 'Error', content: resp.message }); + setShowMessage(true); + } else { + setIsOpen(false); + refreshModalTitles(); + toast.success('Title updated successfully'); + } + }) + .catch(e => { + // eslint-disable-next-line no-console + console.log(e); + }); + } else { + addTitle(titleData) + .then(resp => { + if (resp.status !== 200) { + setWarningMessage({ title: 'Error', content: resp.message }); + setShowMessage(true); + } else { + setIsOpen(false); + refreshModalTitles(); + toast.success('Title added successfully'); + } + }) + .catch(e => { + // eslint-disable-next-line no-console + console.log(e); + }); + + } + }; const onTeamCodeValidation = teamCode => { const format1 = /^[A-Za-z]-[A-Za-z]{3}$/; const format2 = /^[A-Z]{5}$/; + // Check if the input value matches either of the formats const isValidFormat = format1.test(teamCode) || format2.test(teamCode); if (!isValidFormat) { setWarningMessage({ title: 'Error', content: 'Invalid Team Code Format' }); setShowMessage(true); - setTitleData(prev => ({ ...prev, teamCode: '' })); + setTitleData({ ...titleData, teamCode: '' }); return; } if (!existTeamCodes.has(teamCode)) { setWarningMessage({ title: 'Error', content: 'Team Code Not Exists' }); setShowMessage(true); - setTitleData(prev => ({ ...prev, teamCode: '' })); + setTitleData({ ...titleData, teamCode: '' }); return; } setShowMessage(false); }; - // Treat empty selection as OK (make it required here if your business rule requires it) - const onTeamNameValidation = teamObj => { - const name = teamObj && typeof teamObj === 'object' - ? (teamObj.teamName || '').trim() - : ''; - - if (name === '') { - setShowMessage(false); - return true; // optional - } - - if (!existTeamName.has(name)) { - setWarningMessage({ title: 'Error', content: 'Team Name Not Exists' }); - setShowMessage(true); - return false; + const onTeamNameValidation = teamName => { + if (teamName && teamName !== '') { + if (!existTeamName.has(teamName.teamName)) { + setWarningMessage({ title: 'Error', content: 'Team Name Not Exists' }); + setShowMessage(true); + return false; + } } setShowMessage(false); return true; }; - // ------------------- submit ---------------------------------------------- - - const confirmOnClick = () => { - // validate team name (no-op if empty/optional) - if (!onTeamNameValidation(titleData.teamAssiged)) return; - - // normalize team and build payload - const safeTeams = allTeamsArray; - const team = normalizeTeam(titleData.teamAssiged, safeTeams); - - const payload = { - id: titleData.id, - titleName: titleData.titleName?.trim() || '', - titleCode: titleData.titleCode?.trim() || '', - mediaFolder: titleData.mediaFolder?.trim() || '', - teamCode: titleData.teamCode?.trim() || '', - projectAssigned: titleData.projectAssigned || '', - }; - - if (team && team._id) { - payload.teamAssiged = team; // {_id, teamName} - payload.teamName = team.teamName; // some endpoints check this flat prop - } - - const run = editMode ? editTitle : addTitle; - - run(payload) - .then(resp => { - if (resp.status !== 200) { - setWarningMessage({ title: 'Error', content: resp.message }); - setShowMessage(true); - } else { - setIsOpen(false); - refreshModalTitles(); - toast.success(editMode ? 'Title updated successfully' : 'Title added successfully'); - } - }) - .catch(e => { - // eslint-disable-next-line no-console - console.log(e); - setWarningMessage({ title: 'Error', content: 'Unexpected error' }); - setShowMessage(true); - }); - }; - - // ------------------- render ---------------------------------------------- - const fontColor = darkMode ? 'text-light' : ''; return ( @@ -307,30 +277,40 @@ function AddNewTitleModal({ setTitleData(prev => ({ ...prev, titleName: e.target.value }))} + onChange={e => { + e.persist(); + setTitleData({ ...titleData, titleName: e.target.value }); + }} /> - setTitleData(prev => ({ ...prev, titleCode: e.target.value }))} + onChange={e => { + e.persist(); + setTitleData({ ...titleData, titleCode: e.target.value }); + }} maxLength={7} /> - setTitleData(prev => ({ ...prev, mediaFolder: e.target.value }))} + onChange={e => { + const inputValue = e.target.value; + setTitleData({ ...titleData, mediaFolder: inputValue }); + }} placeholder="Enter a valid URL" /> {!/^(https?:\/\/[^\s]+)$/.test(titleData.mediaFolder.trim()) && @@ -339,12 +319,11 @@ function AddNewTitleModal({ Please enter a valid URL that starts with http:// or https:// )} - - @@ -366,26 +344,28 @@ function AddNewTitleModal({ editMode={editMode} value={titleData.projectAssigned} /> - - setTitleData((p) => ({ ...p, teamAssiged: team }))} - placeholder="" -/> - + - - )} - - {(status === 'active' || status === 'invited') && appName === 'slack' && ( -
- - Manual removal required -
- )} - - {status === 'revoked' && ( -
- Revoked: {app?.revokedOn ? new Date(app.revokedOn).toLocaleDateString() : 'N/A'} -
- )} - - {status === 'failed' && ( -
- Failed: {app?.failedReason || 'Unknown error'} -
- )} - - - {status === 'none' && !app?.revokedOn && ( -
-
+ +
+ {status === 'none' && !app?.revokedOn && ( + <> handleCredentialChange(appName, e.target.value)} onBlur={() => setInputTouched(prev => ({ ...prev, [appName]: true }))} required /> - {isDropbox && ( - - )} + + )} + + {status === 'none' && app?.revokedOn && ( +
+ + Access previously revoked
- - {touched && !isCredentialValid && ( -
- {isGithub ? 'GitHub username is required' : 'Email is required'} -
- )} - - {isDropbox && teamFoldersLoading && ( -
- - Loading Dropbox team folders... -
- )} - - {isDropbox && - !teamFoldersLoading && - (teamFolderTouched || (touched && isCredentialValid)) && - !selectedTeamFolder && ( -
- {teamFolders.length === 0 - ? 'No team folders available. Please try refreshing the page.' - : 'Please select a Dropbox team folder'} -
- )} - - {isDropbox && !teamFoldersLoading && selectedTeamFolder && isCredentialValid && ( -
- - User folder name to be created: {userProfile?.firstName}{' '} - {userProfile?.lastName} -
- )} -
- )} - - {status === 'none' && app?.revokedOn && ( -
- - Access previously revoked -
- )} - - {app?.credentials && ( -
- Credentials: {app.credentials} -
- )} + )} + + {status === 'none' && !app?.revokedOn && touched && !isCredentialValid && ( +
+ {isGithub ? 'GitHub username is required' : 'Email is required'} +
+ )} + + {status === 'invited' && ( +
+ Invited: {app?.invitedOn ? new Date(app.invitedOn).toLocaleDateString() : 'N/A'} +
+ )} + + {(status === 'active' || status === 'invited') && appName !== 'slack' && ( + + )} + + {(status === 'active' || status === 'invited') && appName === 'slack' && ( +
+ + Manual removal required +
+ )} + + {status === 'revoked' && ( +
+ Revoked: {app?.revokedOn ? new Date(app.revokedOn).toLocaleDateString() : 'N/A'} +
+ )} + + {status === 'failed' && ( +
+ Failed: {app?.failedReason || 'Unknown error'} +
+ )} +
+ + {app?.credentials && ( +
+ Credentials: {app.credentials} +
+ )} ); }; const renderConfirmationModal = () => { if (!confirmAction) return null; - + const { type, app } = confirmAction; - - // Validate confirmAction structure - if ( - !type || - (type !== 'invite-all' && type !== 'revoke-all' && type !== 'revoke' && type !== 'invite') - ) { - // console.error('Invalid confirmAction structure:', confirmAction); - setConfirmAction(null); - return null; - } - + // Handle bulk actions if (type === 'invite-all') { const invitableApps = getInvitableApps(); return ( - setConfirmAction(null)} - size="md" - className={darkMode ? 'text-light dark-mode' : ''} - > - setConfirmAction(null)} - className={darkMode ? 'bg-space-cadet' : ''} - > + setConfirmAction(null)} size="md" className={darkMode ? 'text-light dark-mode' : ''}> + setConfirmAction(null)} className={darkMode ? 'bg-space-cadet' : ''}> Confirm Invite All Access
- Are you sure you want to invite{' '} - - {userProfile?.firstName} {userProfile?.lastName} - {' '} - to all available applications? + Are you sure you want to invite {userProfile?.firstName} {userProfile?.lastName} to all available applications?
@@ -619,27 +453,14 @@ const AccessManagementModal = ({ isOpen, onClose, userProfile, darkMode = false
    {invitableApps.map(appName => (
  • - {appConfigs[appName].name} -{' '} - {appName === 'github' ? 'Username' : 'Email'}: {credentialsInput[appName]} - {appName === 'dropbox' && selectedTeamFolder && ( -
    - - Team folder:{' '} - {teamFolders.find(f => f.key === selectedTeamFolder)?.name || - selectedTeamFolder} -
    - - User folder: {userProfile?.firstName}{' '} - {userProfile?.lastName} -
    - )} + {appConfigs[appName].name} - {appName === 'github' ? 'Username' : 'Email'}: {credentialsInput[appName]}
  • ))}
- -
); } - + if (type === 'revoke-all') { const revokableApps = getRevokableApps(); return ( - setConfirmAction(null)} - size="md" - className={darkMode ? 'text-light dark-mode' : ''} - > - setConfirmAction(null)} - className={darkMode ? 'bg-space-cadet' : ''} - > + setConfirmAction(null)} size="md" className={darkMode ? 'text-light dark-mode' : ''}> + setConfirmAction(null)} className={darkMode ? 'bg-space-cadet' : ''}> Whoa Tiger!
- - Whoa Tiger! Are you sure you want to do this? This action is not reversible. - + Whoa Tiger! Are you sure you want to do this? This action is not reversible.
Apps to be revoked: @@ -701,7 +508,7 @@ const AccessManagementModal = ({ isOpen, onClose, userProfile, darkMode = false
- -
); } - + // Handle individual app actions const config = appConfigs[app]; - + return ( - setConfirmAction(null)} - size="md" - className={darkMode ? 'text-light dark-mode' : ''} - > - setConfirmAction(null)} - className={darkMode ? 'bg-space-cadet' : ''} - > + setConfirmAction(null)} size="md" className={darkMode ? 'text-light dark-mode' : ''}> + setConfirmAction(null)} className={darkMode ? 'bg-space-cadet' : ''}> {type === 'revoke' ? 'Whoa Tiger!' : 'Confirm Invite Access'} {type === 'revoke' && ( <>
- - Whoa Tiger! Are you sure you want to do this? This action is not reversible. - + Whoa Tiger! Are you sure you want to do this? This action is not reversible.
)}
- - @@ -799,19 +586,9 @@ const AccessManagementModal = ({ isOpen, onClose, userProfile, darkMode = false const invitableApps = !loading && accessData ? getInvitableApps() : []; const revokableApps = !loading && accessData ? getRevokableApps() : []; - // Check if any individual app operations are in progress - const hasInviteLoading = Object.values(inviteLoading).some(loading => loading); - const hasRevokeLoading = Object.values(revokeLoading).some(loading => loading); - const anyAppLoading = hasInviteLoading || hasRevokeLoading; - return ( <> - +
@@ -827,25 +604,23 @@ const AccessManagementModal = ({ isOpen, onClose, userProfile, darkMode = false ) : (
-
- Managing access for:{' '} - - {userProfile?.firstName} {userProfile?.lastName} - -
+
Managing access for: {userProfile?.firstName} {userProfile?.lastName}

Email: {userProfile?.email}

- +
Application Access Status

- {accessData?.found + {accessData?.found ? 'User has access records. Manage their permissions below.' - : 'No access records found. You can invite this user to applications.'} + : 'No access records found. You can invite this user to applications.' + }

- -
{Object.keys(appConfigs).map(renderAppCard)}
+ +
+ {Object.keys(appConfigs).map(renderAppCard)} +
{/* Bulk Action Buttons */}
@@ -854,20 +629,11 @@ const AccessManagementModal = ({ isOpen, onClose, userProfile, darkMode = false color="success" size="sm" onClick={() => setConfirmAction({ type: 'invite-all', app: null })} - disabled={actionInProgress || anyAppLoading} + disabled={actionInProgress} className="mr-3" > - {actionInProgress ? ( - <> - - Inviting All... - - ) : ( - <> - - Invite All ({invitableApps.length}) - - )} + + Invite All ({invitableApps.length}) )} {revokableApps.length > 0 && ( @@ -875,19 +641,10 @@ const AccessManagementModal = ({ isOpen, onClose, userProfile, darkMode = false color="danger" size="sm" onClick={() => setConfirmAction({ type: 'revoke-all', app: null })} - disabled={actionInProgress || anyAppLoading} + disabled={actionInProgress} > - {actionInProgress ? ( - <> - - Revoking All... - - ) : ( - <> - - Revoke All ({revokableApps.length}) - - )} + + Revoke All ({revokableApps.length}) )}
@@ -899,10 +656,10 @@ const AccessManagementModal = ({ isOpen, onClose, userProfile, darkMode = false Close {accessData?.found && ( - + + + + + + {/* Job ads listing */} +
+ {jobAds.length > 0 && + jobAds.map(ad => ( +
+ {`${ad.category} +

+ {ad.title} +

+ +
+ {ad.location?.toLowerCase() !== 'remote' + ? `In-Person | Location: ${ad.location}` + : 'Remote'} +
+ +

+ {ad.description || 'No detailed description available.'} +

+ + {ad.requirements && ad.requirements.length > 0 && ( +
+

Requirements:

+
    + {ad.requirements.map(req => ( +
  • {req}
  • + ))} +
+
+ )} + + + + +
+ ))} + + {jobAds.length === 0 && hasSearched && ( +
+ No results +

No Job Ads Found

+

+ We couldn’t find any matches. Try a different keyword or category. +

+
+ )} + + {jobAds.length === 0 && !hasSearched && ( +
+

🔍 Begin Your Search

+

+ Use the search bar or pick a category to explore available job roles! +

+
+ {['Engineering', 'Marketing', 'Design', 'Finance'].map(cat => ( + + ))} +
+
+ )} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )} +
+ ); +} + +export default SuggestedJobsList; diff --git a/src/components/Collaboration/index.jsx b/src/components/Collaboration/index.jsx index ebf0470919..622a2f2b15 100644 --- a/src/components/Collaboration/index.jsx +++ b/src/components/Collaboration/index.jsx @@ -1,3 +1,3 @@ -import Collaboration from './Collaboration'; +import SuggestedJobsList from './SuggestedJobsList'; -export default Collaboration; +export default SuggestedJobsList; diff --git a/src/components/LBDashboard/LBDashboard.jsx b/src/components/LBDashboard/LBDashboard.jsx index b2f8982902..4743a3cc85 100644 --- a/src/components/LBDashboard/LBDashboard.jsx +++ b/src/components/LBDashboard/LBDashboard.jsx @@ -1,12 +1,270 @@ -import { Container } from 'reactstrap'; +import { useState } from 'react'; +import { + Container, + Row, + Col, + Button, + ButtonGroup, + ButtonDropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, + Card, + CardBody, +} from 'reactstrap'; import styles from './LBDashboard.module.css'; +const METRIC_OPTIONS = { + DEMAND: [ + { key: 'pageVisits', label: 'Page Visits' }, // default overall + { key: 'numBids', label: 'Number of Bids' }, + { key: 'avgRating', label: 'Average Rating' }, + ], + REVENUE: [ + { key: 'avgBid', label: 'Average Bid' }, + { key: 'finalPrice', label: 'Final Price / Income' }, // default for Revenue + ], + VACANCY: [ + { key: 'occupancyRate', label: 'Occupancy Rate (% days not vacant)' }, // default for Vacancy + { key: 'avgStay', label: 'Average Duration of Stay' }, + ], +}; + +const DEFAULTS = { + DEMAND: 'pageVisits', + REVENUE: 'finalPrice', + VACANCY: 'occupancyRate', +}; + +function GraphCard({ title, metricLabel }) { + return ( + + +
+ {title} + {metricLabel} +
+
+ Graph area +
+
+
+ ); +} + export function LBDashboard() { + const [activeCategory, setActiveCategory] = useState('DEMAND'); + const [selectedMetricKey, setSelectedMetricKey] = useState(DEFAULTS.DEMAND); + + const [openDD, setOpenDD] = useState({ DEMAND: false, REVENUE: false, VACANCY: false }); + + const metricLabel = (() => { + const all = Object.values(METRIC_OPTIONS).flat(); + return (all.find(o => o.key === selectedMetricKey) || {}).label || ''; + })(); + + const handleCategoryClick = category => { + setActiveCategory(category); + setSelectedMetricKey(DEFAULTS[category]); + }; + + const handleMetricPick = (category, key) => { + setActiveCategory(category); + setSelectedMetricKey(key); + }; + + const toggleDD = category => setOpenDD(s => ({ ...s, [category]: !s[category] })); + + const goBack = () => { + window.history.back(); + }; + return ( - -
-

Biding Dashboard

+ + {/* Header */} +
+

Listing and Bidding Platform Dashboard

+
+ + {/* Preset Overview Filter */} +
+
Choose Metric to view
+ + + {/* DEMAND */} + + toggleDD('DEMAND')} + className={styles.dd} + > + + + {METRIC_OPTIONS.DEMAND.map(m => ( + handleMetricPick('DEMAND', m.key)} + className={`${styles.dropdownItem} ${ + selectedMetricKey === m.key ? styles.dropdownActive : '' + }`} + > + {m.label} + + ))} + + + + {/* VACANCY */} + + toggleDD('VACANCY')} + className={styles.dd} + > + + + {METRIC_OPTIONS.VACANCY.map(m => ( + handleMetricPick('VACANCY', m.key)} + className={`${styles.dropdownItem} ${ + selectedMetricKey === m.key ? styles.dropdownActive : '' + }`} + > + {m.label} + + ))} + + + + {/* REVENUE */} + + toggleDD('REVENUE')} + className={styles.dd} + > + + + {METRIC_OPTIONS.REVENUE.map(m => ( + handleMetricPick('REVENUE', m.key)} + className={`${styles.dropdownItem} ${ + selectedMetricKey === m.key ? styles.dropdownActive : '' + }`} + > + {m.label} + + ))} + + + + +
+ Current metric: {metricLabel} +
+
+ + {/* By Village */} +
+
+ By Village +
+ +
@@ -251,25 +253,28 @@ const Members = props => { style={darkMode ? {} : boxStyle} > All - + ) : null} - {props.state.projectMembers.foundUsers.map((user, i) => ( - - ))} + {props.state.projectMembers.foundUsers + .filter(user => !showActiveMembersOnly || user.isActive) + .map((user, i) => ( + + ))}
) : null} @@ -295,7 +300,7 @@ const Members = props => { - {displayedMembers.map((member, i) => ( + {membersList?.map((member, i) => ( { {totalEffort >= weeklyCommittedHours && (
-

+

HOURS diff --git a/src/components/TeamMemberTasks/ReviewButton.jsx b/src/components/TeamMemberTasks/ReviewButton.jsx index 26cc892d0d..9609396cd0 100644 --- a/src/components/TeamMemberTasks/ReviewButton.jsx +++ b/src/components/TeamMemberTasks/ReviewButton.jsx @@ -19,8 +19,7 @@ import './reviewButton.css'; import { boxStyle, boxStyleDark } from '~/styles'; import '../Header/DarkMode.css'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCheck } from '@fortawesome/free-solid-svg-icons'; -import { faPencilAlt, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faPencilAlt, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import httpService from '../../services/httpService'; import { ApiEndpoint } from '~/utils/URL'; import hasPermission from '~/utils/permissions'; diff --git a/src/components/TeamMemberTasks/TeamMemberTask.jsx b/src/components/TeamMemberTasks/TeamMemberTask.jsx index 29e1f2c5b3..d8ddedb322 100644 --- a/src/components/TeamMemberTasks/TeamMemberTask.jsx +++ b/src/components/TeamMemberTasks/TeamMemberTask.jsx @@ -540,4 +540,6 @@ const TeamMemberTask = React.memo( }, ); +TeamMemberTask.displayName = 'TeamMemberTask'; + export default TeamMemberTask; diff --git a/src/components/TeamMemberTasks/TeamMemberTaskIconsInfo.jsx b/src/components/TeamMemberTasks/TeamMemberTaskIconsInfo.jsx index f177022de3..5cf7732277 100644 --- a/src/components/TeamMemberTasks/TeamMemberTaskIconsInfo.jsx +++ b/src/components/TeamMemberTasks/TeamMemberTaskIconsInfo.jsx @@ -7,8 +7,6 @@ import { boxStyle, boxStyleDark } from '~/styles'; import './style.css'; import '../Header/DarkMode.css'; import { useSelector } from 'react-redux'; -import './style.css'; -import '../Header/DarkMode.css'; import infoTaskIconContent from './infoTaskIconContent'; const TeamMemberTaskInfo = React.memo(() => { @@ -57,4 +55,6 @@ const TeamMemberTaskInfo = React.memo(() => { ); }); +TeamMemberTaskInfo.displayName = 'TeamMemberTaskInfo'; + export default TeamMemberTaskInfo; diff --git a/src/components/TeamMemberTasks/TeamMemberTasks.jsx b/src/components/TeamMemberTasks/TeamMemberTasks.jsx index 15dad99817..50c8bc7ab1 100644 --- a/src/components/TeamMemberTasks/TeamMemberTasks.jsx +++ b/src/components/TeamMemberTasks/TeamMemberTasks.jsx @@ -1,9 +1,8 @@ -import { Fragment } from 'react'; +import React, { Fragment, useEffect, useState, useCallback } from 'react'; import { faClock } from '@fortawesome/free-solid-svg-icons'; import { Table, Row, Col } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { fetchTeamMembersTask, deleteTaskNotification } from '~/actions/task'; -import React, { useEffect, useState, useCallback } from 'react'; import { useDispatch, useSelector, connect } from 'react-redux'; import { MultiSelect } from 'react-multi-select-component'; import SkeletonLoading from '../common/SkeletonLoading'; @@ -499,6 +498,7 @@ const TeamMemberTasks = React.memo(props => { }; return (
{
@@ -602,7 +605,10 @@ const TeamMemberTasks = React.memo(props => {
) : ( - + )} { > @@ -751,7 +758,10 @@ const TeamMemberTasks = React.memo(props => { {teamList.length === 0 ? ( - + ) : ( teamList .filter(user => filterByUserFeatures(user)) @@ -810,7 +820,7 @@ const TeamMemberTasks = React.memo(props => { timeEntriesList .filter(timeEntry => timeEntry.personId === user.personId) .map(timeEntry => ( - + - - - - @@ -243,6 +244,7 @@ export function TaskDifferenceModal({ diff --git a/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx b/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx index ed3e600e80..c6f5b5547e 100644 --- a/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx +++ b/src/components/TeamMemberTasks/components/__tests__/TaskDifferenceModal.test.jsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, within } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; @@ -85,7 +85,7 @@ describe('TaskDifferenceModal component', () => { }); it('check if modal is open when isOpen is set to true', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('Task Info Changes')).toBeInTheDocument(); + expect(screen.getByText('Task Info Changes')).toBeInTheDocument(); }); it('check if modal is not open when isOpen is set to false', () => { renderComponent(false, 'abc123', taskNotifications); @@ -93,7 +93,7 @@ describe('TaskDifferenceModal component', () => { }); it('check if modal body does get displayed when task notification user id is same as user id', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('White Bold = No Changes')).toBeInTheDocument(); + expect(screen.getByText('White Bold = No Changes')).toBeInTheDocument(); }); it('check if modal body does not get displayed when task notification user id is not same as user id', () => { renderComponent(true, 'ghi123', taskNotifications); @@ -106,72 +106,53 @@ describe('TaskDifferenceModal component', () => { }); it('check if task name and Task Name header gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('Task Name')).toBeInTheDocument(); - expect(screen.queryByText(`${taskNotifications[0].oldTask.taskName}`)).toBeInTheDocument(); + expect(screen.getByText('Task Name')).toBeInTheDocument(); + expect(screen.getByText(`${taskNotifications[0].oldTask.taskName}`)).toBeInTheDocument(); }); it('check if priority and Assigned gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('Priority')).toBeInTheDocument(); - expect(screen.queryByText('Primary')).toBeInTheDocument(); - expect(screen.queryByText('Assigned')).toBeInTheDocument(); - expect(screen.queryByText('Yes')).toBeInTheDocument(); + expect(screen.getByText('Priority')).toBeInTheDocument(); + expect(screen.getByText('Primary')).toBeInTheDocument(); + expect(screen.getByText('Assigned')).toBeInTheDocument(); + expect(screen.getByText('Yes')).toBeInTheDocument(); }); it('check if status and Hours - Best-case gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('Status')).toBeInTheDocument(); - expect(screen.queryByText('Complete')).toBeInTheDocument(); + expect(screen.getByText('Status')).toBeInTheDocument(); + expect(screen.getByText('Complete')).toBeInTheDocument(); - const hoursBestLabel = screen.getByText('Hours - Best-case'); - - expect(screen.queryByText('Hours - Best-case')).toBeInTheDocument(); - const hoursBestValueElement = hoursBestLabel.nextElementSibling; - const spanElement = hoursBestValueElement.querySelector('span'); - expect(spanElement.textContent).toBe('10'); + expect(screen.getByTestId('hours-best-value')).toHaveTextContent('10'); }); it('check if Hours - Worst-case and Hours - Most-case gets displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('Hours - Worst-case')).toBeInTheDocument(); - - const hoursWorstLabel = screen.getByText('Hours - Worst-case'); - const hoursWorstValueElement = hoursWorstLabel.nextElementSibling; - const spanElement = hoursWorstValueElement.querySelector('span'); - expect(spanElement.textContent).toBe('13'); + expect(screen.getByText('Hours - Worst-case')).toBeInTheDocument(); - expect(screen.queryByText('Hours - Most-case')).toBeInTheDocument(); - const hoursMostLabel = screen.getByText('Hours - Most-case'); - const hoursMostValueElement = hoursMostLabel.nextElementSibling; - const hoursMostSpanElement = hoursMostValueElement.querySelector('span'); - expect(hoursMostSpanElement.textContent).toBe('13'); + expect(screen.getByTestId('hours-worst-value')).toHaveTextContent('13'); + expect(screen.getByText('Hours - Most-case')).toBeInTheDocument(); + expect(screen.getByTestId('hours-most-value')).toHaveTextContent('13'); }); it('check if estimated hours and Classification get displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('Estimated Hours')).toBeInTheDocument(); - const estimatedHoursLabel = screen.getByText('Estimated Hours'); - const estimatedHoursValueElement = estimatedHoursLabel.nextElementSibling; - const estimatedHoursSpanElement = estimatedHoursValueElement.querySelector('span'); - expect(estimatedHoursSpanElement.textContent).toBe('11'); - - expect(screen.queryByText('Classification')).toBeInTheDocument(); - expect(screen.queryByText('Example')).toBeInTheDocument(); + expect(screen.getByText('Estimated Hours')).toBeInTheDocument(); + expect(screen.getByTestId('estimated-hours-value')).toHaveTextContent('11'); + expect(screen.getByText('Classification')).toBeInTheDocument(); + expect(screen.getByText('Example')).toBeInTheDocument(); }); it('check if Why this Task is Important and Design Intent get displayed properly', () => { renderComponent(true, 'abc123', taskNotifications); - expect(screen.queryByText('Why this Task is Important')).toBeInTheDocument(); - expect(screen.queryByText('some reason')).toBeInTheDocument(); - expect(screen.queryByText('Design Intent')).toBeInTheDocument(); - expect(screen.queryByText('Food')).toBeInTheDocument(); + expect(screen.getByText('Why this Task is Important')).toBeInTheDocument(); + expect(screen.getByText('some reason')).toBeInTheDocument(); + expect(screen.getByText('Design Intent')).toBeInTheDocument(); + expect(screen.getByText('Food')).toBeInTheDocument(); }); it('check if Start Date and End Date does not get displayed when Start and End Date is set to null', () => { renderComponent(true, 'abc123', taskNotifications); - const startDateLabel = screen.getByText('Start Date'); - const startDateElement = startDateLabel.nextElementSibling; - const startSpanElement = startDateElement.querySelector('span'); - expect(startSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); - const endDateLabel = screen.getByText('End Date'); - const endDateElement = endDateLabel.nextElementSibling; - const endSpanElement = endDateElement.querySelector('span'); - expect(endSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); + expect(screen.getByText('Start Date')).toBeInTheDocument(); + expect(screen.getByText('End Date')).toBeInTheDocument(); + + expect(screen.queryByTestId('start-date-value')).not.toBeInTheDocument(); + expect(screen.queryByTestId('end-date-value')).not.toBeInTheDocument(); }); it('check if Start Date and End Date get displayed when Start and End Date is not set to null', () => { const newTaskNotifications = [ @@ -196,15 +177,12 @@ describe('TaskDifferenceModal component', () => { }); it('check if links, resources does not get displayed when the array size is 0', () => { renderComponent(true, 'abc123', taskNotifications); - const resourceLabel = screen.getByText('Resources'); - const resourceElement = resourceLabel.nextElementSibling; - const resourceSpanElement = resourceElement.querySelector('span'); - expect(resourceSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); - const linksLabel = screen.getByText('Links'); - const linksElement = linksLabel.nextElementSibling; - const linksSpanElement = linksElement.querySelector('span'); - expect(linksSpanElement).toHaveStyle('color: rgb(0, 0, 0); font-weight: bold;'); + expect(screen.getByText('Resources')).toBeInTheDocument(); + expect(screen.getByText('Links')).toBeInTheDocument(); + + expect(screen.queryByTestId('resources-value')).not.toBeInTheDocument(); + expect(screen.queryByTestId('links-value')).not.toBeInTheDocument(); }); it('check if links and resources get displayed as expected when links and resources arrays are set to entries more than 0', () => { const newResource = [ diff --git a/src/components/Teams/TeamMembersPopup.jsx b/src/components/Teams/TeamMembersPopup.jsx index 4c4e030bc2..6a7c244a2e 100644 --- a/src/components/Teams/TeamMembersPopup.jsx +++ b/src/components/Teams/TeamMembersPopup.jsx @@ -215,6 +215,7 @@ export const TeamMembersPopup = React.memo(props => { { {checkedStatus} - - + +
({ darkMode: state.theme.darkMode, }); +TeamMemberTasks.displayName = 'TeamMemberTasks'; + export default connect(mapStateToProps, null)(TeamMemberTasks); diff --git a/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx b/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx index 655915467b..8f1366d50a 100644 --- a/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx +++ b/src/components/TeamMemberTasks/__tests__/DiffedText.test.jsx @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; import { configureStore } from 'redux-mock-store'; import { themeMock } from '../../../__tests__/mockStates'; @@ -29,9 +29,9 @@ describe('DiffedText Component', () => { , ); - expect(getByText('Hello')).toHaveStyle('color: rgb(255, 255, 255)'); - expect(getByText('world')).toHaveStyle('color: rgb(255, 255, 255)'); - expect(getByText('React')).toHaveStyle('color: rgb(0, 128, 0)'); + expect(screen.getByText('Hello')).toHaveStyle('color: rgb(255, 255, 255)'); + expect(screen.getByText('world')).toHaveStyle('color: rgb(255, 255, 255)'); + expect(screen.getByText('React')).toHaveStyle('color: rgb(0, 128, 0)'); }); it('handles text removal correctly', () => { @@ -44,7 +44,7 @@ describe('DiffedText Component', () => { , ); - expect(getByText('world')).toHaveStyle( + expect(screen.getByText('world')).toHaveStyle( 'textDecorationLine: line-through; color: rgb(255, 0, 0)', ); }); diff --git a/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx b/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx index 20364314ab..3ab0c787da 100644 --- a/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx +++ b/src/components/TeamMemberTasks/__tests__/TaskButton.test.jsx @@ -1,7 +1,6 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { Provider } from 'react-redux'; +import { Provider, useDispatch } from 'react-redux'; import { configureStore } from 'redux-mock-store'; -import { useDispatch } from 'react-redux'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import TaskButton from '../TaskButton'; @@ -111,10 +110,13 @@ describe('TaskButton', () => { endstateInfo: task.endstateInfo, classification: task.classification, }); - expect(deleteSelectedTaskSpy).toHaveBeenCalledWith(task._id, task.mother); - expect(getAllUserProfileSpy).toHaveBeenCalled(); - expect(fetchAllTasksSpy).toHaveBeenCalled(); }); + + await waitFor(() => expect(deleteSelectedTaskSpy).toHaveBeenCalledWith(task._id, task.mother)); + + await waitFor(() => expect(getAllUserProfileSpy).toHaveBeenCalled()); + + await waitFor(() => expect(fetchAllTasksSpy).toHaveBeenCalled()); }); test('does not render button when task status is Complete', () => { diff --git a/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx b/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx index 7f3a0e521e..140b6f5ff8 100644 --- a/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx +++ b/src/components/TeamMemberTasks/__tests__/TeamMemberTasks.test.jsx @@ -221,10 +221,8 @@ describe('TeamMemberTasks component', () => { , ); - const skeletonLoadingElement = container.querySelector( - '.skeleton-loading-team-member-tasks-row', - ); - expect(skeletonLoadingElement).toBeInTheDocument(); + + expect(screen.getAllByTestId('team-member-tasks-row')).not.toHaveLength(0); }); it('check if the skeleton loading html elements are not shown when isLoading is false', () => { axios.get.mockResolvedValue({ @@ -239,10 +237,7 @@ describe('TeamMemberTasks component', () => { , ); - const skeletonLoadingElement = container.querySelector( - '.skeleton-loading-team-member-tasks-row', - ); - expect(skeletonLoadingElement).not.toBeInTheDocument(); + expect(screen.queryByTestId('team-member-tasks-row')).not.toBeInTheDocument(); }); it('check if class names does not include color when dark mode is false', () => { axios.get.mockResolvedValue({ @@ -257,32 +252,23 @@ describe('TeamMemberTasks component', () => { , ); - const darkModeElement = container.querySelector('.container.team-member-tasks'); - const timeOffElement = container.querySelector('.show-time-off-btn'); - const hoursCompletedElement = container.querySelector('.team-member-tasks-subtable'); - const oneDayElement = container.querySelector( - '[title="Timelogs submitted in the past 1 days"]', - ); - const twoDayElement = container.querySelector( - '[title="Timelogs submitted in the past 2 days"]', - ); - const threeDayElement = container.querySelector( - '[title="Timelogs submitted in the past 3 days"]', - ); - const fourDayElement = container.querySelector( - '[title="Timelogs submitted in the past 4 days"]', - ); - const sevenDayElement = container.querySelector( - '[title="Timelogs submitted in the past 7 days"]', - ); - expect(darkModeElement).toBeInTheDocument(); - expect(hoursCompletedElement).toBeInTheDocument(); - expect(timeOffElement).toBeInTheDocument(); - expect(oneDayElement).toBeInTheDocument(); - expect(twoDayElement).toBeInTheDocument(); - expect(threeDayElement).toBeInTheDocument(); - expect(fourDayElement).toBeInTheDocument(); - expect(sevenDayElement).toBeInTheDocument(); + + const timeOffButton = screen.getByTestId('show-time-off-btn'); + expect(timeOffButton).toBeInTheDocument(); + + expect(screen.getByTestId('team-member-tasks-container')).toBeInTheDocument(); + + fireEvent.click(timeOffButton); + + expect(screen.getByTitle('Timelogs submitted in the past 1 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 2 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 3 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 4 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 7 days')).toBeInTheDocument(); }); it('check if class names does include color when dark mode is true', () => { axios.get.mockResolvedValue({ @@ -309,32 +295,22 @@ describe('TeamMemberTasks component', () => { , ); - const darkModeElement = container.querySelector('.container.team-member-tasks'); - const timeOffElement = container.querySelector('.show-time-off-btn'); - const hoursCompletedElement = container.querySelector('.team-member-tasks-subtable'); - const oneDayElement = container.querySelector( - '[title="Timelogs submitted in the past 1 days"]', - ); - const twoDayElement = container.querySelector( - '[title="Timelogs submitted in the past 2 days"]', - ); - const threeDayElement = container.querySelector( - '[title="Timelogs submitted in the past 3 days"]', - ); - const fourDayElement = container.querySelector( - '[title="Timelogs submitted in the past 4 days"]', - ); - const sevenDayElement = container.querySelector( - '[title="Timelogs submitted in the past 7 days"]', - ); - expect(darkModeElement).toBeInTheDocument(); - expect(hoursCompletedElement).toBeInTheDocument(); - expect(timeOffElement).toBeInTheDocument(); - expect(oneDayElement).toBeInTheDocument(); - expect(twoDayElement).toBeInTheDocument(); - expect(threeDayElement).toBeInTheDocument(); - expect(fourDayElement).toBeInTheDocument(); - expect(sevenDayElement).toBeInTheDocument(); + + expect(screen.getByTestId('team-member-tasks-subtable')).toBeInTheDocument(); + + expect(screen.getByTestId('team-member-tasks-container')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('show-time-off-btn')); + + expect(screen.getByTitle('Timelogs submitted in the past 1 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 2 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 3 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 4 days')).toBeInTheDocument(); + + expect(screen.getByTitle('Timelogs submitted in the past 7 days')).toBeInTheDocument(); }); it('check if show time off button works as expected', () => { axios.get.mockResolvedValue({ @@ -348,14 +324,15 @@ describe('TeamMemberTasks component', () => { , ); - const buttonElement = container.querySelector('[class="m-1 show-time-off-btn"]'); - expect(container.querySelector('[class="show-time-off-calender-svg"]')).toBeInTheDocument(); - expect(container.querySelector('[class="show-time-off-icon"]')).toBeInTheDocument(); + const buttonElement = screen.getByTestId('show-time-off-btn'); + + expect(screen.getByTestId('time-off-calendar-icon')).toBeInTheDocument(); + expect(screen.getByTestId('show-time-off-icon')).toBeInTheDocument(); + fireEvent.click(buttonElement); - const iconElement = container.querySelector('[class="show-time-off-calender-svg"]'); - expect(iconElement).toBeInTheDocument(); - const newIconElement = container.querySelector('[class="show-time-off-icon"]'); - expect(newIconElement).toBeInTheDocument(); + + expect(screen.getByTestId('time-off-calendar-icon')).toBeInTheDocument(); + expect(screen.getByTestId('show-time-off-icon')).toBeInTheDocument(); }); it('check if days button works as expected', () => { axios.get.mockResolvedValue({ status: 200, data: '' }); @@ -395,6 +372,6 @@ describe('TeamMemberTasks component', () => { , ); - expect(container.querySelector('[className="table-row"]')).not.toBeInTheDocument(); + expect(screen.queryByTestId('table-row')).not.toBeInTheDocument(); }); }); diff --git a/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx b/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx index a9a9c4f61a..bd54dfe283 100644 --- a/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx +++ b/src/components/TeamMemberTasks/components/TaskCompletedModal.jsx @@ -3,7 +3,6 @@ import React, { useState } from 'react'; import { boxStyle, boxStyleDark } from '~/styles'; import '../../Header/DarkMode.css'; import { toast } from 'react-toastify'; -import '../../Header/DarkMode.css'; const TaskCompletedModal = React.memo(props => { const { darkMode } = props; @@ -94,4 +93,6 @@ const TaskCompletedModal = React.memo(props => { ); }); +TaskCompletedModal.displayName = 'TaskCompletedModal'; + export default TaskCompletedModal; diff --git a/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx b/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx index 76ecc678af..bba680d9af 100644 --- a/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx +++ b/src/components/TeamMemberTasks/components/TaskDifferenceModal.jsx @@ -148,7 +148,7 @@ export function TaskDifferenceModal({
Hours - Best-case +
Hours - Worst-case +
Hours - Most-case +
Estimated Hours +
#User Name#User Name Date Added{' '} { ); }); +TeamMembersPopup.displayName = 'TeamMembersPopup'; + export default connect(null, { hasPermission })(TeamMembersPopup); diff --git a/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx b/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx index 6eddd801c9..3a18535156 100644 --- a/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx +++ b/src/components/UserProfile/QuickSetupModal/AddNewTitleModal.jsx @@ -18,6 +18,24 @@ import AssignTeamField from './AssignTeamField'; import AssignTeamCodeField from './AssignTeamCodeField'; import '../../Header/DarkMode.css'; +// ---- helpers --------------------------------------------------------------- + +const normalizeTeam = (value, teams = []) => { + if (!value) return null; + + // already object + if (typeof value === 'object') { + const found = teams.find(t => t?._id === value._id) || null; + return found ? { _id: found._id, teamName: found.teamName || '' } : null; + } + // id string + if (typeof value === 'string') { + const found = teams.find(t => t?._id === value) || null; + return found ? { _id: found._id, teamName: found.teamName || '' } : null; + } + return null; +}; + function AddNewTitleModal({ isOpen, setIsOpen, @@ -28,45 +46,50 @@ function AddNewTitleModal({ setShowMessage, editMode, title, - QSTTeamCodes + QSTTeamCodes, }) { const darkMode = useSelector(state => state.theme.darkMode); const teamCodes = useSelector(state => state.teamCodes?.teamCodes || []); - + + // ------------------------- state ----------------------------------------- + const [titleData, setTitleData] = useState(() => { - if (editMode && Object.keys(title).length !== 0) { + if (editMode && title && Object.keys(title).length !== 0) { return { id: title._id, - titleName: title.titleName, - titleCode: title.titleCode, - mediaFolder: title.mediaFolder, - teamCode: title.teamCode, - projectAssigned: title.projectAssigned, - teamAssiged: title.teamAssiged == undefined ? { teamName: '', _id: '' } : title.teamAssiged, + titleName: title.titleName || '', + titleCode: title.titleCode || ((title.titleName || '').slice(0, 7)), + mediaFolder: title.mediaFolder || '', + teamCode: title.teamCode || '', + projectAssigned: title.projectAssigned || '', + // safe default even if server had null + teamAssiged: title.teamAssiged || { _id: '', teamName: '' }, }; } + // non-edit defaults return { titleName: '', titleCode: '', mediaFolder: '', teamCode: '', projectAssigned: '', - teamAssiged: {}, + teamAssiged: { _id: '', teamName: '' }, }; }); const [isValidTeamCode, setIsValidTeamCode] = useState(true); + // keep titleData in sync when props change useEffect(() => { - if (editMode && Object.keys(title).length !== 0) { + if (editMode && title && Object.keys(title).length !== 0) { setTitleData({ id: title._id, - titleName: title.titleName, - titleCode: title.titleCode || title.titleName.slice(0, 7), - mediaFolder: title.mediaFolder, - teamCode: title.teamCode, - projectAssigned: title.projectAssigned, - teamAssiged: title.teamAssiged, + titleName: title.titleName || '', + titleCode: title.titleCode || ((title.titleName || '').slice(0, 7)), + mediaFolder: title.mediaFolder || '', + teamCode: title.teamCode || '', + projectAssigned: title.projectAssigned || '', + teamAssiged: title.teamAssiged || { _id: '', teamName: '' }, }); } else { setTitleData({ @@ -75,36 +98,45 @@ function AddNewTitleModal({ mediaFolder: '', teamCode: '', projectAssigned: '', - teamAssiged: {}, + teamAssiged: { _id: '', teamName: '' }, }); } }, [editMode, title]); + // auto-fill titleCode from titleName useEffect(() => { - const titleCode = titleData?.titleCode ? titleData.titleCode : titleData.titleName.slice(0, 7); - setTitleData(prev => ({ - ...prev, - titleCode, - })); + const base = titleData?.titleName || ''; + const code = titleData?.titleCode ? titleData.titleCode : base.slice(0, 7); + setTitleData(prev => ({ ...prev, titleCode: code })); }, [titleData.titleName]); + // live teamCode validity (using QSTTeamCodes list) useEffect(() => { setIsValidTeamCode( - titleData.teamCode === '' || QSTTeamCodes.some(code => code.value === titleData.teamCode), + titleData.teamCode === '' || + (Array.isArray(QSTTeamCodes) && + QSTTeamCodes.some(code => code?.value === titleData.teamCode)) ); }, [titleData.teamCode, QSTTeamCodes]); - let existTeamCodes = new Set(); - let existTeamName = new Set(); + // ----------------- canonical lists for validation ------------------------ - if (teamsData?.allTeams) { - const codes = teamsData.allTeams.map(team => team.teamCode); - const names = teamsData.allTeams.map(team => team.teamName); - // Use allTeamCode rather than allTeams since team code is not related to records in the Team table. - // It is all distinct team codes from the UserProfile teamCode field. - existTeamCodes = new Set(teamsData?.allTeamCode?.distinctTeamCodes); - existTeamName = new Set(names); - } + // Accept both shapes for teamsData: array OR { allTeams, allTeamCode } + const allTeamsArray = Array.isArray(teamsData) + ? teamsData + : (teamsData && Array.isArray(teamsData.allTeams) ? teamsData.allTeams : []); + + let existTeamCodes = new Set( + (Array.isArray(teamsData?.allTeamCode?.distinctTeamCodes) + ? teamsData.allTeamCode.distinctTeamCodes + : []) + ); + + const existTeamName = new Set( + allTeamsArray.map(t => t?.teamName).filter(Boolean) + ); + + // ------------------- local UI state (selectors) -------------------------- const [selectedTeam, onSelectTeam] = useState(undefined); const [selectedProject, onSelectProject] = useState(undefined); @@ -112,146 +144,144 @@ function AddNewTitleModal({ const [isValidProject, onValidation] = useState(false); const [searchText, setSearchText] = useState(''); // For addTeamAutoComplete + // ------------------- field handlers -------------------------------------- + const selectProject = project => { onSelectProject(project); - setTitleData({ - ...titleData, + setTitleData(prev => ({ + ...prev, projectAssigned: { projectName: project.projectName, _id: project._id, category: project.category, }, - }); + })); onValidation(true); }; const selectTeamCode = teamCode => { onSelectTeamCode(teamCode); - setTitleData({ - ...titleData, - teamCode, - }); + setTitleData(prev => ({ ...prev, teamCode })); }; const cleanProjectAssign = () => { - setTitleData({ - ...titleData, - projectAssigned: '', - }); + setTitleData(prev => ({ ...prev, projectAssigned: '' })); }; const selectTeam = team => { onSelectTeam(team); setTitleData(prev => ({ ...prev, - teamAssiged: { - teamName: team.teamName, - _id: team._id, - }, + teamAssiged: { teamName: team.teamName, _id: team._id }, })); onValidation(true); }; const cleanTeamCodeAssign = () => { - setTitleData({ - ...titleData, - teamCode: '', - }); + setTitleData(prev => ({ ...prev, teamCode: '' })); }; const cleanTeamAssigned = () => { - // if clean all input field -> no team selected - const updatedTitleData = { ...titleData }; - delete updatedTitleData.teamAssiged; - setTitleData(updatedTitleData); + const updated = { ...titleData }; + delete updated.teamAssiged; + setTitleData(updated); }; const undoTeamAssigned = () => { - setTitleData({ - ...titleData, - teamAssiged: { - teamName: searchText, - _id: 'N/A', - }, - }); + setTitleData(prev => ({ + ...prev, + teamAssiged: { teamName: searchText, _id: 'N/A' }, + })); }; - // confirm and save - const confirmOnClick = () => { - const isValidTeamName = onTeamNameValidation(titleData.teamAssiged); - - if (!isValidTeamName) { - return; - } - - if (editMode) { - editTitle(titleData) - .then(resp => { - if (resp.status !== 200) { - setWarningMessage({ title: 'Error', content: resp.message }); - setShowMessage(true); - } else { - setIsOpen(false); - refreshModalTitles(); - toast.success('Title updated successfully'); - } - }) - .catch(e => { - // eslint-disable-next-line no-console - console.log(e); - }); - } else { - addTitle(titleData) - .then(resp => { - if (resp.status !== 200) { - setWarningMessage({ title: 'Error', content: resp.message }); - setShowMessage(true); - } else { - setIsOpen(false); - refreshModalTitles(); - toast.success('Title added successfully'); - } - }) - .catch(e => { - // eslint-disable-next-line no-console - console.log(e); - }); - - } - }; + // ------------------- validations ----------------------------------------- const onTeamCodeValidation = teamCode => { const format1 = /^[A-Za-z]-[A-Za-z]{3}$/; const format2 = /^[A-Z]{5}$/; - // Check if the input value matches either of the formats const isValidFormat = format1.test(teamCode) || format2.test(teamCode); if (!isValidFormat) { setWarningMessage({ title: 'Error', content: 'Invalid Team Code Format' }); setShowMessage(true); - setTitleData({ ...titleData, teamCode: '' }); + setTitleData(prev => ({ ...prev, teamCode: '' })); return; } if (!existTeamCodes.has(teamCode)) { setWarningMessage({ title: 'Error', content: 'Team Code Not Exists' }); setShowMessage(true); - setTitleData({ ...titleData, teamCode: '' }); + setTitleData(prev => ({ ...prev, teamCode: '' })); return; } setShowMessage(false); }; - const onTeamNameValidation = teamName => { - if (teamName && teamName !== '') { - if (!existTeamName.has(teamName.teamName)) { - setWarningMessage({ title: 'Error', content: 'Team Name Not Exists' }); - setShowMessage(true); - return false; - } + // Treat empty selection as OK (make it required here if your business rule requires it) + const onTeamNameValidation = teamObj => { + const name = teamObj && typeof teamObj === 'object' + ? (teamObj.teamName || '').trim() + : ''; + + if (name === '') { + setShowMessage(false); + return true; // optional + } + + if (!existTeamName.has(name)) { + setWarningMessage({ title: 'Error', content: 'Team Name Not Exists' }); + setShowMessage(true); + return false; } setShowMessage(false); return true; }; + // ------------------- submit ---------------------------------------------- + + const confirmOnClick = () => { + // validate team name (no-op if empty/optional) + if (!onTeamNameValidation(titleData.teamAssiged)) return; + + // normalize team and build payload + const safeTeams = allTeamsArray; + const team = normalizeTeam(titleData.teamAssiged, safeTeams); + + const payload = { + id: titleData.id, + titleName: titleData.titleName?.trim() || '', + titleCode: titleData.titleCode?.trim() || '', + mediaFolder: titleData.mediaFolder?.trim() || '', + teamCode: titleData.teamCode?.trim() || '', + projectAssigned: titleData.projectAssigned || '', + }; + + if (team && team._id) { + payload.teamAssiged = team; // {_id, teamName} + payload.teamName = team.teamName; // some endpoints check this flat prop + } + + const run = editMode ? editTitle : addTitle; + + run(payload) + .then(resp => { + if (resp.status !== 200) { + setWarningMessage({ title: 'Error', content: resp.message }); + setShowMessage(true); + } else { + setIsOpen(false); + refreshModalTitles(); + toast.success(editMode ? 'Title updated successfully' : 'Title added successfully'); + } + }) + .catch(e => { + // eslint-disable-next-line no-console + console.log(e); + setWarningMessage({ title: 'Error', content: 'Unexpected error' }); + setShowMessage(true); + }); + }; + + // ------------------- render ---------------------------------------------- + const fontColor = darkMode ? 'text-light' : ''; return ( @@ -277,40 +307,30 @@ function AddNewTitleModal({ { - e.persist(); - setTitleData({ ...titleData, titleName: e.target.value }); - }} + onChange={e => setTitleData(prev => ({ ...prev, titleName: e.target.value }))} /> + { - e.persist(); - setTitleData({ ...titleData, titleCode: e.target.value }); - }} + onChange={e => setTitleData(prev => ({ ...prev, titleCode: e.target.value }))} maxLength={7} /> + { - const inputValue = e.target.value; - setTitleData({ ...titleData, mediaFolder: inputValue }); - }} + onChange={e => setTitleData(prev => ({ ...prev, mediaFolder: e.target.value }))} placeholder="Enter a valid URL" /> {!/^(https?:\/\/[^\s]+)$/.test(titleData.mediaFolder.trim()) && @@ -319,11 +339,12 @@ function AddNewTitleModal({ Please enter a valid URL that starts with http:// or https:// )} + + @@ -344,28 +366,26 @@ function AddNewTitleModal({ editMode={editMode} value={titleData.projectAssigned} /> + - + setTitleData((p) => ({ ...p, teamAssiged: team }))} + placeholder="" +/> + + + )} + + {(status === 'active' || status === 'invited') && appName === 'slack' && ( +
+ + Manual removal required +
+ )} + + {status === 'revoked' && ( +
+ Revoked: {app?.revokedOn ? new Date(app.revokedOn).toLocaleDateString() : 'N/A'} +
+ )} + + {status === 'failed' && ( +
+ Failed: {app?.failedReason || 'Unknown error'} +
+ )} + - -
- {status === 'none' && !app?.revokedOn && ( - <> + + {status === 'none' && !app?.revokedOn && ( +
+
handleCredentialChange(appName, e.target.value)} onBlur={() => setInputTouched(prev => ({ ...prev, [appName]: true }))} required /> + {isDropbox && ( + + )} - - )} - - {status === 'none' && app?.revokedOn && ( -
- - Access previously revoked -
- )} - - {status === 'none' && !app?.revokedOn && touched && !isCredentialValid && ( -
- {isGithub ? 'GitHub username is required' : 'Email is required'} -
- )} - - {status === 'invited' && ( -
- Invited: {app?.invitedOn ? new Date(app.invitedOn).toLocaleDateString() : 'N/A'} -
- )} - - {(status === 'active' || status === 'invited') && appName !== 'slack' && ( - - )} - - {(status === 'active' || status === 'invited') && appName === 'slack' && ( -
- - Manual removal required -
- )} - - {status === 'revoked' && ( -
- Revoked: {app?.revokedOn ? new Date(app.revokedOn).toLocaleDateString() : 'N/A'}
- )} - - {status === 'failed' && ( -
- Failed: {app?.failedReason || 'Unknown error'} -
- )} -
+ + {touched && !isCredentialValid && ( +
+ {isGithub ? 'GitHub username is required' : 'Email is required'} +
+ )} + + {isDropbox && teamFoldersLoading && ( +
+ + Loading Dropbox team folders... +
+ )} + + {isDropbox && + !teamFoldersLoading && + (teamFolderTouched || (touched && isCredentialValid)) && + !selectedTeamFolder && ( +
+ {teamFolders.length === 0 + ? 'No team folders available. Please try refreshing the page.' + : 'Please select a Dropbox team folder'} +
+ )} + + {isDropbox && !teamFoldersLoading && selectedTeamFolder && isCredentialValid && ( +
+ + User folder name to be created: {userProfile?.firstName}{' '} + {userProfile?.lastName} +
+ )} +
+ )} + + {status === 'none' && app?.revokedOn && ( +
+ + Access previously revoked +
+ )} + + {app?.credentials && ( +
+ Credentials: {app.credentials} +
+ )}
- - {app?.credentials && ( -
- Credentials: {app.credentials} -
- )} ); }; const renderConfirmationModal = () => { if (!confirmAction) return null; - + const { type, app } = confirmAction; - + + // Validate confirmAction structure + if ( + !type || + (type !== 'invite-all' && type !== 'revoke-all' && type !== 'revoke' && type !== 'invite') + ) { + // console.error('Invalid confirmAction structure:', confirmAction); + setConfirmAction(null); + return null; + } + // Handle bulk actions if (type === 'invite-all') { const invitableApps = getInvitableApps(); return ( - setConfirmAction(null)} size="md" className={darkMode ? 'text-light dark-mode' : ''}> - setConfirmAction(null)} className={darkMode ? 'bg-space-cadet' : ''}> + setConfirmAction(null)} + size="md" + className={darkMode ? 'text-light dark-mode' : ''} + > + setConfirmAction(null)} + className={darkMode ? 'bg-space-cadet' : ''} + > Confirm Invite All Access
- Are you sure you want to invite {userProfile?.firstName} {userProfile?.lastName} to all available applications? + Are you sure you want to invite{' '} + + {userProfile?.firstName} {userProfile?.lastName} + {' '} + to all available applications?
@@ -453,14 +619,27 @@ const AccessManagementModal = ({
    {invitableApps.map(appName => (
  • - {appConfigs[appName].name} - {appName === 'github' ? 'Username' : 'Email'}: {credentialsInput[appName]} + {appConfigs[appName].name} -{' '} + {appName === 'github' ? 'Username' : 'Email'}: {credentialsInput[appName]} + {appName === 'dropbox' && selectedTeamFolder && ( +
    + + Team folder:{' '} + {teamFolders.find(f => f.key === selectedTeamFolder)?.name || + selectedTeamFolder} +
    + + User folder: {userProfile?.firstName}{' '} + {userProfile?.lastName} +
    + )}
  • ))}
- -
); } - + if (type === 'revoke-all') { const revokableApps = getRevokableApps(); return ( - setConfirmAction(null)} size="md" className={darkMode ? 'text-light dark-mode' : ''}> - setConfirmAction(null)} className={darkMode ? 'bg-space-cadet' : ''}> + setConfirmAction(null)} + size="md" + className={darkMode ? 'text-light dark-mode' : ''} + > + setConfirmAction(null)} + className={darkMode ? 'bg-space-cadet' : ''} + > Whoa Tiger!
- Whoa Tiger! Are you sure you want to do this? This action is not reversible. + + Whoa Tiger! Are you sure you want to do this? This action is not reversible. +
Apps to be revoked: @@ -508,7 +701,7 @@ const AccessManagementModal = ({
- -
); } - + // Handle individual app actions const config = appConfigs[app]; - + return ( - setConfirmAction(null)} size="md" className={darkMode ? 'text-light dark-mode' : ''}> - setConfirmAction(null)} className={darkMode ? 'bg-space-cadet' : ''}> + setConfirmAction(null)} + size="md" + className={darkMode ? 'text-light dark-mode' : ''} + > + setConfirmAction(null)} + className={darkMode ? 'bg-space-cadet' : ''} + > {type === 'revoke' ? 'Whoa Tiger!' : 'Confirm Invite Access'} {type === 'revoke' && ( <>
- Whoa Tiger! Are you sure you want to do this? This action is not reversible. + + Whoa Tiger! Are you sure you want to do this? This action is not reversible. +
)}
- - @@ -586,9 +799,19 @@ const AccessManagementModal = ({ const invitableApps = !loading && accessData ? getInvitableApps() : []; const revokableApps = !loading && accessData ? getRevokableApps() : []; + // Check if any individual app operations are in progress + const hasInviteLoading = Object.values(inviteLoading).some(loading => loading); + const hasRevokeLoading = Object.values(revokeLoading).some(loading => loading); + const anyAppLoading = hasInviteLoading || hasRevokeLoading; + return ( <> - +
@@ -604,23 +827,25 @@ const AccessManagementModal = ({ ) : (
-
Managing access for: {userProfile?.firstName} {userProfile?.lastName}
+
+ Managing access for:{' '} + + {userProfile?.firstName} {userProfile?.lastName} + +

Email: {userProfile?.email}

- +
Application Access Status

- {accessData?.found + {accessData?.found ? 'User has access records. Manage their permissions below.' - : 'No access records found. You can invite this user to applications.' - } + : 'No access records found. You can invite this user to applications.'}

- -
- {Object.keys(appConfigs).map(renderAppCard)} -
+ +
{Object.keys(appConfigs).map(renderAppCard)}
{/* Bulk Action Buttons */}
@@ -629,11 +854,20 @@ const AccessManagementModal = ({ color="success" size="sm" onClick={() => setConfirmAction({ type: 'invite-all', app: null })} - disabled={actionInProgress} + disabled={actionInProgress || anyAppLoading} className="mr-3" > - - Invite All ({invitableApps.length}) + {actionInProgress ? ( + <> + + Inviting All... + + ) : ( + <> + + Invite All ({invitableApps.length}) + + )} )} {revokableApps.length > 0 && ( @@ -641,10 +875,19 @@ const AccessManagementModal = ({ color="danger" size="sm" onClick={() => setConfirmAction({ type: 'revoke-all', app: null })} - disabled={actionInProgress} + disabled={actionInProgress || anyAppLoading} > - - Revoke All ({revokableApps.length}) + {actionInProgress ? ( + <> + + Revoking All... + + ) : ( + <> + + Revoke All ({revokableApps.length}) + + )} )}
@@ -656,10 +899,10 @@ const AccessManagementModal = ({ Close {accessData?.found && ( - -
-
- -
+
+ One Community Logo +
+
+ +
- {console.log(role)} -

FORM CREATION

- {role === 'Owner' || role === 'Administrator' ? ( -
-

- Fill the form with questions about a specific position you want to create an ad for. - The default questions will automatically appear and are alredy selected. You can pick - and choose them with the checkbox. -

- -
- {formFields.map((field, index) => ( -
- changeVisiblity(event, field)} - /> -
- -
- {field.questionType === 'textbox' && ( - - )} - {field.questionType === 'date' && ( - - )} - {field.questionType === 'textarea' && ( -