diff --git a/package.json b/package.json index c4bec1ba83..44f324c05f 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "date-fns-tz": "^2.0.1", "dayjs": "^1.11.13", "diff": "^8.0.3", - "dompurify": "^3.2.5", + "dompurify": "^3.3.2", "elliptic": "^6.6.1", "font-awesome": "^4.7.0", "fs-extra": "^11.3.0", @@ -129,7 +129,9 @@ }, "resolutions": { "react": "18.3.1", - "react-dom": "18.3.1" + "react-dom": "18.3.1", + "mdn-data": "2.12.2", + "ansi-escapes": "7.1.1" }, "packageManager": "yarn@1.22.22", "scripts": { diff --git a/src/components/ApplicantVolunteerRatio/ApplicantVolunteerRatio.jsx b/src/components/ApplicantVolunteerRatio/ApplicantVolunteerRatio.jsx index bf13108011..caf20d83b6 100644 --- a/src/components/ApplicantVolunteerRatio/ApplicantVolunteerRatio.jsx +++ b/src/components/ApplicantVolunteerRatio/ApplicantVolunteerRatio.jsx @@ -9,32 +9,30 @@ import 'react-datepicker/dist/react-datepicker.css'; function ApplicantVolunteerRatio() { const darkMode = useSelector(state => state.theme.darkMode); + const [data, setData] = useState([]); - const [allRoles, setAllRoles] = useState([]); // Store all available roles + const [allRoles, setAllRoles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedRoles, setSelectedRoles] = useState([]); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [validationError, setValidationError] = useState(''); + const [viewMode, setViewMode] = useState('count'); - // Fetch all available roles (without filtering) + // Fetch all available roles useEffect(() => { const fetchAllRoles = async () => { try { const response = await getAllApplicantVolunteerRatios({}); const apiData = response.data; - // Get all unique roles const uniqueRoles = [...new Set(apiData.map(item => item.role))]; const roleOptions = uniqueRoles.map(role => ({ label: role, value: role })); setAllRoles(roleOptions); - - // Set all roles as selected by default setSelectedRoles(roleOptions); } catch (err) { - // Error fetching all roles setError('Failed to load roles. Please try again.'); } }; @@ -42,10 +40,9 @@ function ApplicantVolunteerRatio() { fetchAllRoles(); }, []); - // Fetch filtered data based on selected roles and date range + // Fetch filtered data useEffect(() => { const fetchFilteredData = async () => { - // Validate date range: start must be before or equal to end if (startDate && endDate && startDate > endDate) { setValidationError('Start date must be earlier than or equal to End date.'); setData([]); @@ -53,7 +50,6 @@ function ApplicantVolunteerRatio() { return; } - // clear previous validation error when dates are valid if (validationError) setValidationError(''); if (selectedRoles.length === 0) { @@ -64,14 +60,9 @@ function ApplicantVolunteerRatio() { try { setLoading(true); - // Prepare filters const filters = {}; - if (startDate) { - filters.startDate = startDate.toISOString().split('T')[0]; // Format as YYYY-MM-DD - } - if (endDate) { - filters.endDate = endDate.toISOString().split('T')[0]; // Format as YYYY-MM-DD - } + if (startDate) filters.startDate = startDate.toISOString().split('T')[0]; + if (endDate) filters.endDate = endDate.toISOString().split('T')[0]; if (selectedRoles.length > 0) { filters.roles = selectedRoles.map(role => role.value).join(','); } @@ -79,7 +70,6 @@ function ApplicantVolunteerRatio() { const response = await getAllApplicantVolunteerRatios(filters); const apiData = response.data; - // Transform API data to match chart format const transformedData = apiData.map(item => ({ role: item.role, applicants: item.totalApplicants, @@ -88,7 +78,6 @@ function ApplicantVolunteerRatio() { setData(transformedData); } catch (err) { - // Error fetching applicant volunteer ratio data setError('Failed to load data. Please try again.'); } finally { setLoading(false); @@ -96,15 +85,28 @@ function ApplicantVolunteerRatio() { }; fetchFilteredData(); - }, [startDate, endDate, selectedRoles]); // Re-fetch when date range or selected roles change + }, [startDate, endDate, selectedRoles]); - // Filter and transform data for chart - const chartData = useMemo( - () => data.filter(d => selectedRoles.map(r => r.value).includes(d.role)), - [data, selectedRoles], - ); + // Prepare chart data + const chartData = useMemo(() => { + const filtered = data.filter(d => selectedRoles.map(r => r.value).includes(d.role)); + + if (viewMode === 'percentage') { + return filtered.map(item => { + const percentage = + item.applicants > 0 ? Number(((item.hired / item.applicants) * 100).toFixed(1)) : 0; - // Apply dark mode to document body + return { + ...item, + hiredPercentage: percentage, + }; + }); + } + + return filtered; + }, [data, selectedRoles, viewMode]); + + // Apply dark mode useEffect(() => { if (darkMode) { document.body.classList.add('dark-mode-body'); @@ -138,10 +140,12 @@ function ApplicantVolunteerRatio() { return (

Number of People Hired vs. Total Applications

+ + {/* Filters */}
)}
+
+ + { + const uniqueIds = [...new Set(mockExpenditureData.map(item => item.projectId))]; + return uniqueIds; +}; + +// Get expenditure data for a specific project (mimics /api/bm/expenditure/:projectId/pie endpoint) +export const getProjectExpenditure = projectId => { + const projectData = mockExpenditureData.filter(item => item.projectId === projectId); + + // Group by type and category, summing amounts + const actual = {}; + const planned = {}; + + projectData.forEach(item => { + const target = item.type === 'actual' ? actual : planned; + if (!target[item.category]) { + target[item.category] = 0; + } + target[item.category] += item.amount; + }); + + // Convert to array format expected by the chart + const actualArray = Object.entries(actual).map(([category, amount]) => ({ + category, + amount, + })); + + const plannedArray = Object.entries(planned).map(([category, amount]) => ({ + category, + amount, + })); + + return { + actual: actualArray, + planned: plannedArray, + }; +}; + +export default mockExpenditureData; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index 48818a5f28..0a74b48b05 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { v4 as uuidv4 } from 'uuid'; import html2canvas from 'html2canvas'; import { jsPDF } from 'jspdf'; +import WeeklyProjectSummaryHeader from './WeeklyProjectSummaryHeader'; import CostPredictionChart from './CostPredictionChart'; import ToolStatusDonutChart from './ToolStatusDonutChart/ToolStatusDonutChart'; import PaidLaborCost from './PaidLaborCost/PaidLaborCost'; @@ -16,6 +17,8 @@ import ToolsHorizontalBarChart from './Tools/ToolsHorizontalBarChart'; import ExpenseBarChart from './Financials/ExpenseBarChart'; import ActualVsPlannedCost from './ActualVsPlannedCost/ActualVsPlannedCost'; import TotalMaterialCostPerProject from './TotalMaterialCostPerProject/TotalMaterialCostPerProject'; +import FinancialsTrackingCard from './ExpenditureChart/FinancialsTrackingCard'; +import EmbedInteractiveMap from '../InteractiveMap/EmbedInteractiveMap'; import InteractiveMap from '../InteractiveMap/InteractiveMap'; import styles from './WeeklyProjectSummary.module.css'; import IssueCharts from '../Issues/openIssueCharts'; @@ -128,7 +131,6 @@ function WeeklyProjectSummary() { const [openSections, setOpenSections] = useState({}); const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const darkMode = useSelector(state => state.theme.darkMode); - useEffect(() => { if (materials.length === 0) { dispatch(fetchAllMaterials()); @@ -168,16 +170,19 @@ function WeeklyProjectSummary() {
{button.title}
{button.value}
-
+
{button.change}
@@ -186,6 +191,7 @@ function WeeklyProjectSummary() {
), }, + // New Issues Breakdown card { title: 'Issues Breakdown', key: 'Issues Breakdown', @@ -233,57 +239,46 @@ function WeeklyProjectSummary() { { title: 'Tools and Equipment Tracking', key: 'Tools and Equipment Tracking', - className: 'full', - content: ( -
-
- -
-
- -
-
- -
-
- ), + className: 'half', + content: [ +
+ +
, +
+ +
, + ], }, { title: 'Lessons Learned', key: 'Lessons Learned', - className: 'full', - content: ( -
-
- -
-
- -
-
- ), + className: 'half', + content: [ + , +
+ +
, + ], }, { title: 'Financials', key: 'Financials', className: 'large', content: ( -
-
- 📊 Card -
-
+
+
📊 Card
+
-
- 📊 Card -
-
- 📊 Card -
-
- 📊 Big Card -
+
📊 Card
+
📊 Card
+
📊 Big Card
), }, @@ -304,7 +299,7 @@ function WeeklyProjectSummary() { className={`${styles.weeklyProjectSummaryCard} ${styles.mapCard}`} style={{ height: '500px', padding: '0' }} > - +
), }, @@ -312,38 +307,34 @@ function WeeklyProjectSummary() { title: 'Labor and Time Tracking', key: 'Labor and Time Tracking', className: 'half', - content: ( -
-
- -
-
- + content: [1, 2].map((_, index) => { + const uniqueId = uuidv4(); + return ( +
+ {index === 1 ? : }
-
- ), + ); + }), }, { title: 'Financials Tracking', key: 'Financials Tracking', className: 'full', content: ( -
- {[1, 2, 3, 4].map((_, index) => { - const uniqueId = uuidv4(); - return ( -
- {(() => { - if (index === 2) return ; - if (index === 3) return ; - return '📊 Card'; - })()} -
- ); - })} +
+
+ +
+
📊 Card
+
+ +
+
+ +
), }, @@ -460,7 +451,7 @@ function WeeklyProjectSummary() { return (
- {/* Header Section - Now inline instead of separate component */} + {/* Header Section - Now inline instead of seperate component */}

diff --git a/src/components/Collaboration/JobFormbuilder.jsx b/src/components/Collaboration/JobFormbuilder.jsx index 089dc6fc99..f0f0e5cdfe 100644 --- a/src/components/Collaboration/JobFormbuilder.jsx +++ b/src/components/Collaboration/JobFormbuilder.jsx @@ -48,6 +48,11 @@ function JobFormBuilder() { const jobPositions = ['Software Developer', 'Project Manager', 'Analyst']; + const markAsSaved = fields => { + setInitialFormFields(structuredClone(fields)); + setHasUnsavedChanges(false); + }; + // Prevent refresh while unsaved changes exist useEffect(() => { const handler = event => { @@ -73,13 +78,10 @@ function JobFormBuilder() { setCurrentFormId(id); setFormFields(form.questions || []); - setInitialFormFields(form.questions || []); - + markAsSaved(form.questions || []); setNewField(initialNewField); - setHasUnsavedChanges(false); } } catch (error) { - // still allowed logging console.error(error); } }; @@ -101,7 +103,6 @@ function JobFormBuilder() { // Clone field const cloneField = async (field, index) => { const clone = structuredClone(field); - const updated = [...formFields.slice(0, index + 1), clone, ...formFields.slice(index + 1)]; setFormFields(updated); @@ -111,6 +112,7 @@ function JobFormBuilder() { question: clone, position: index + 1, }); + markAsSaved(updated); } catch (error) { console.error(error); } @@ -132,6 +134,7 @@ function JobFormBuilder() { fromIndex: index, toIndex: newIndex, }); + markAsSaved(updated); } catch (error) { console.error(error); } @@ -147,6 +150,7 @@ function JobFormBuilder() { if (currentFormId) { try { await axios.delete(ENDPOINTS.DELETE_QUESTION(currentFormId, index)); + markAsSaved(updated); } catch (error) { console.error(error); } @@ -187,6 +191,7 @@ function JobFormBuilder() { ENDPOINTS.UPDATE_QUESTION(currentFormId, editingIndex), updated[editingIndex], ); + markAsSaved(updated); } catch (error) { console.error(error); } @@ -236,6 +241,7 @@ function JobFormBuilder() { question: newField, position: formFields.length, }); + markAsSaved(updated); } catch (error) { console.error(error); } @@ -301,7 +307,13 @@ function JobFormBuilder() { { + setFormFields(fields); + markAsSaved(fields); + }} + onTemplateSaved={() => { + markAsSaved(formFields); + }} darkMode={darkMode} templateName={templateName} setTemplateName={setTemplateName} diff --git a/src/components/Collaboration/QuestionSetManager.jsx b/src/components/Collaboration/QuestionSetManager.jsx index 3fecb83a28..e7a2415f4d 100644 --- a/src/components/Collaboration/QuestionSetManager.jsx +++ b/src/components/Collaboration/QuestionSetManager.jsx @@ -12,6 +12,7 @@ function QuestionSetManager({ formFields, setFormFields, onImportQuestions, + onTemplateSaved, darkMode, templateName, setTemplateName, @@ -119,6 +120,7 @@ function QuestionSetManager({ setTemplates(prev => prev.map(t => (t._id === updatedTemplate._id ? updatedTemplate : t))); safeAlert(`Template "${templateName}" updated.`); + onTemplateSaved && onTemplateSaved(); } else { const newTemplate = await api.createTemplate({ name: templateName, @@ -137,6 +139,7 @@ function QuestionSetManager({ localStorage.setItem('jobFormTemplates', JSON.stringify(updated)); safeAlert(`Template "${templateName}" created.`); + onTemplateSaved && onTemplateSaved(); } setTemplateName(''); @@ -406,6 +409,7 @@ QuestionSetManager.propTypes = { ).isRequired, setFormFields: PropTypes.func.isRequired, onImportQuestions: PropTypes.func.isRequired, + onTemplateSaved: PropTypes.func, darkMode: PropTypes.bool.isRequired, templateName: PropTypes.string.isRequired, setTemplateName: PropTypes.func.isRequired, diff --git a/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.jsx b/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.jsx index fc16e8c08c..557c77c61f 100644 --- a/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.jsx +++ b/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.jsx @@ -1,88 +1,82 @@ -import { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; -import styles from './ResourcesUsage.module.css'; - -function ResourcesUsage() { - const darkMode = useSelector(state => state.theme.darkMode); - const data = [ - { - sNo: 1, - name: 'Harsh Kadyan', - materials: '????', - facilities: 'Software Engineer Team', - status: { text: 'Debited', color: 'green' }, - dueDate: '23 April 2025', - }, - { - sNo: 2, - name: 'John Doe', - materials: '????', - facilities: 'HR Facilities', - status: { text: 'Partially Debited', color: 'yellow' }, - dueDate: '12 May 2025', - }, - { - sNo: 3, - name: 'Jane Smith', - materials: '????', - facilities: 'IT Equipment', - status: { text: 'Not Debited', color: 'red' }, - dueDate: '15 March 2025', - }, - { - sNo: 4, - name: 'Alex Brown', - materials: '????', - facilities: 'Admin Facilities', - status: { text: 'Debited', color: 'green' }, - dueDate: '20 April 2025', - }, - ]; - - return ( -
-

Resource Usage Monitoring

- - {/* header for column */} -
-
S.No
-
Name
-
Materials
-
Facilities
-
Status
-
Due Date
-
Actions
-
- - {/* data for each row */} - {data.map(row => ( -
-
{row.sNo}
-
{row.name}
-
{row.materials}
-
{row.facilities}
-
- {row.status.text} -
-
{row.dueDate}
-
- -
-
- ))} - -
- -
-
- ); -} - -export default ResourcesUsage; +import { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import styles from './ResourcesUsage.module.css'; + +function ResourcesUsage() { + const darkMode = useSelector(state => state.theme.darkMode); + const data = [ + { + sNo: 1, + name: 'Harsh Kadyan', + materials: '????', + facilities: 'Software Engineer Team', + status: { text: 'Debited', color: 'green' }, + dueDate: '23 April 2025', + }, + { + sNo: 2, + name: 'John Doe', + materials: '????', + facilities: 'HR Facilities', + status: { text: 'Partially Debited', color: 'yellow' }, + dueDate: '12 May 2025', + }, + { + sNo: 3, + name: 'Jane Smith', + materials: '????', + facilities: 'IT Equipment', + status: { text: 'Not Debited', color: 'red' }, + dueDate: '15 March 2025', + }, + { + sNo: 4, + name: 'Alex Brown', + materials: '????', + facilities: 'Admin Facilities', + status: { text: 'Debited', color: 'green' }, + dueDate: '20 April 2025', + }, + ]; + + return ( +
+
+

Resource Usage Monitoring

+
+
S.No
+
Name
+
Materials
+
Facilities
+
Status
+
Due Date
+
Actions
+
+ {data.map(row => ( +
+
{row.sNo}
+
{row.name}
+
{row.materials}
+
{row.facilities}
+
+ {row.status.text} +
+
{row.dueDate}
+
+ +
+
+ ))} +
+ +
+
+
+ ); +} + +export default ResourcesUsage; diff --git a/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.module.css b/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.module.css index c81f296552..2ad8efe8df 100644 --- a/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.module.css +++ b/src/components/CommunityPortal/Activities/activityId/ResourcesUsage.module.css @@ -1,7 +1,18 @@ -/* src/components/CommunityPortal/Activities/activityId/Resources.module.css */ +.resourcesUsage { + padding: 20px; + border-radius: 8px; + font-family: Arial, sans-serif; + width: 100%; +} +.resourceRow { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + border: 2px solid black; +} -/* Column Styling for resources*/ .column { flex: 1; padding: 15px; @@ -9,24 +20,39 @@ font-weight: bold; } +.resourceRow .column:last-child { + border-right: none; +} +.resourceTitle { + text-align: left; + font-size: 1.2rem; + margin-bottom: 5px; + font-weight: 700; + color: grey; +} + +.header { + background-color: #f5f5f5; + font-weight: bold; +} -/* colors for text based on status */ .statusgreen { - color: green !important; + color: green; } .statusyellow { - color: orange !important; + color: orange; } .statusred { - color: red !important; + color: red; } -.viewDetails { +.viewDetailsButton { display: block; width: 100%; + background-color: #e6f9e6; color: #146c43; font-weight: bold; border-radius: 5px; @@ -34,22 +60,20 @@ cursor: pointer; border: none; text-decoration: none; - transition: background 0.2s ease-in-out; text-align: center; + transition: background 0.3s ease-in-out; } -.viewDetails:hover { +.viewDetailsButton:hover { background-color: #d1f0d1; } -/* Actions Column */ .actionColumn { display: flex; justify-content: center; align-items: center; } -/* Tick Box */ .tickBox { display: flex; justify-content: center; @@ -62,39 +86,50 @@ align-items: center; justify-content: center; font-weight: bold; + background-color: white; border: 2px solid black; padding: 10px 15px; border-radius: 8px; cursor: pointer; } -.resourceTitle { - text-align: left; - font-size: 1.2rem; - margin-bottom: 5px; - font-weight: 700; - color: grey; +.darkMode .resourcesUsage { + background-color: #34495e; + color: #ecf0f1; } -.headerDark { - background-color: #2c3e50; - font-weight: bold; +.darkMode .resourceRow { + border: 2px solid #7f8c8d; } -.viewDetailsButtonDark { - display: block; - width: 100%; - color: #146c43; - font-weight: bold; - border-radius: 5px; - padding: 10px; - cursor: pointer; - border: none; - text-decoration: none; - transition: background 0.2s ease-in-out; - text-align: center; +.darkMode .column { + color: #ecf0f1; +} + +.darkMode .tickLabel { + color: #ecf0f1; + border: 2px solid #7f8c8d; + background-color: #34495e; +} + +.darkMode .viewDetailsButton { + background-color: #27ae60; + color: #ecf0f1; +} + +.darkMode .viewDetailsButton:hover { + background-color: #1e8449; + color: #ecf0f1; +} + +.darkMode .statusgreen { + color: #27ae60 !important; +} + +.darkMode .statusyellow { + color: #f39c12 !important; } -.viewDetailsButtonDark:hover { - background-color: #2c3e50; +.darkMode .statusred { + color: #e74c3c !important; } diff --git a/src/components/Faq/FaqSearch.jsx b/src/components/Faq/FaqSearch.jsx index 2aad9c2124..2e8f319965 100644 --- a/src/components/Faq/FaqSearch.jsx +++ b/src/components/Faq/FaqSearch.jsx @@ -2,12 +2,8 @@ import { useState, useEffect } from 'react'; import { debounce } from 'lodash'; import { Button } from 'reactstrap'; import { FaChevronDown, FaChevronUp } from 'react-icons/fa'; -import { toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; import { getAllFAQs, searchFAQs, logUnansweredQuestion } from './api'; -toast.configure(); - function FaqSearch() { const [searchQuery, setSearchQuery] = useState(''); const [allFAQs, setAllFAQs] = useState([]); @@ -72,11 +68,11 @@ function FaqSearch() { setLogging(true); try { const response = await logUnansweredQuestion(searchQuery); - toast.success(response.data.message || 'Your question has been recorded.'); + globalThis.alert(response?.data?.message || 'Question logged successfully'); } catch (error) { // eslint-disable-next-line no-console console.error('Error logging unanswered question:', error); - toast.error('Failed to log question. It may already exist.'); + globalThis.alert(error?.response?.data?.message || 'Failed to log question.'); } finally { setLogging(false); } diff --git a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx index 0c7b277859..87aa9d1c40 100644 --- a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx +++ b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx @@ -1,8 +1,9 @@ +import PropTypes from 'prop-types'; import { useState } from 'react'; import { useSelector } from 'react-redux'; +import { availablePreferences, availableSkills, formatSkillName } from './FilerData.js'; import RankedUserList from './RankedUserList'; import styles from './style/CommunityMembersPage.module.css'; -import { availableSkills, availablePreferences, formatSkillName } from './FilerData.js'; function Accordion({ title, children, defaultOpen = false, darkMode }) { const [open, setOpen] = useState(defaultOpen); @@ -26,6 +27,7 @@ function Accordion({ title, children, defaultOpen = false, darkMode }) { function CommunityMembersPage() { const [selectedSkills, setSelectedSkills] = useState([]); const [selectedPreferences, setSelectedPreferences] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); const darkMode = useSelector(state => state.theme.darkMode); const toggleItem = (item, selectedArray, setSelectedArray) => { @@ -44,7 +46,7 @@ function CommunityMembersPage() { key={skillKey} onClick={() => toggleItem(skillKey, selectedSkills, setSelectedSkills)} type="button" - className={`${`${styles.skillButton}`} ${isSelected ? styles.selected : ''}`} + className={`${styles.skillButton} ${isSelected ? styles.selected : ''}`} > {formattedName} @@ -62,7 +64,7 @@ function CommunityMembersPage() { key={pref} onClick={() => toggleItem(pref, selectedPreferences, setSelectedPreferences)} type="button" - className={`${`${styles.preferenceButton}`} ${isSelected ? styles.selected : ''}`} + className={`${styles.preferenceButton} ${isSelected ? styles.selected : ''}`} > {pref} @@ -71,10 +73,33 @@ function CommunityMembersPage() {

); + const hasFilters = + selectedSkills.length > 0 || selectedPreferences.length > 0 || searchQuery.trim().length > 0; + return (

Community Member Filters

+ {/* Search Bar */} +
+ setSearchQuery(e.target.value)} + className={`${styles.searchInput} ${darkMode ? styles.darkSearchInput : ''}`} + /> + {searchQuery && ( + + )} +
+ {renderSkillButtons()} @@ -84,19 +109,26 @@ function CommunityMembersPage() {
- {selectedSkills.length > 0 || selectedPreferences.length > 0 ? ( + {hasFilters ? ( ) : (

- Select skills or preferences above to see filtered members. + Search or select skills and preferences above to see filtered members.

)}
); } +Accordion.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + defaultOpen: PropTypes.bool, + darkMode: PropTypes.bool, +}; export default CommunityMembersPage; diff --git a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx index efecb8a8ea..27c288b031 100644 --- a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx +++ b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx @@ -1,47 +1,57 @@ -import { useEffect, useState } from 'react'; import axios from 'axios'; -import UserCard from './UserCard'; +import PropTypes from 'prop-types'; +import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import styles from './style/RankedUserList.module.css'; +import UserCard from './UserCard'; -function RankedUserList({ selectedSkills, selectedPreferences }) { - const [rankedUsers, setRankedUsers] = useState([]); +function RankedUserList({ selectedSkills, selectedPreferences, searchQuery }) { + const [allUsers, setAllUsers] = useState([]); const [loading, setLoading] = useState(true); const darkMode = useSelector(state => state.theme.darkMode); - useEffect(() => { - if ( - (!selectedSkills || selectedSkills.length === 0) && - (!selectedPreferences || selectedPreferences.length === 0) - ) - return; - const fetchRankedUsers = async () => { + useEffect(() => { + const fetchUsers = async () => { setLoading(true); try { const params = {}; - if (selectedSkills.length > 0) params.skills = selectedSkills.join(','); - if (selectedPreferences.length > 0) params.preferences = selectedPreferences.join(','); + if (selectedSkills && selectedSkills.length > 0) params.skills = selectedSkills.join(','); + if (selectedPreferences && selectedPreferences.length > 0) + params.preferences = selectedPreferences.join(','); const response = await axios.get(`${process.env.REACT_APP_APIENDPOINT}/hgnform/ranked`, { params, }); - setRankedUsers(response.data); + setAllUsers(response.data); } catch (err) { + // error handled silently } finally { setLoading(false); } }; - fetchRankedUsers(); + fetchUsers(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedSkills, selectedPreferences]); + // Client-side filter by searchQuery on top of API results + const filteredUsers = searchQuery + ? allUsers.filter(user => { + const name = (user.name || '').toLowerCase(); + const skills = (user.topSkills || []).join(' ').toLowerCase(); + return ( + name.includes(searchQuery.toLowerCase()) || skills.includes(searchQuery.toLowerCase()) + ); + }) + : allUsers; + if (loading) return

Loading ranked users...

; - if (!rankedUsers.length) return

No users found.

; + if (!filteredUsers.length) return

No users found.

; return (
- {rankedUsers.map(user => ( + {filteredUsers.map(user => (
@@ -51,4 +61,10 @@ function RankedUserList({ selectedSkills, selectedPreferences }) { ); } +RankedUserList.propTypes = { + selectedSkills: PropTypes.arrayOf(PropTypes.string), + selectedPreferences: PropTypes.arrayOf(PropTypes.string), + searchQuery: PropTypes.string, +}; + export default RankedUserList; diff --git a/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css index 66715e70f9..42eb579511 100644 --- a/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css +++ b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css @@ -124,3 +124,49 @@ border-color: #22c55e; color: #ffffff; } + +.searchContainer { + display: flex; + align-items: center; + margin-bottom: 16px; + border: 1px solid #ccc; + border-radius: 8px; + padding: 6px 12px; + background: #fff; +} + +.searchInput { + flex: 1; + border: none; + outline: none; + font-size: 14px; + background: transparent; + color: #000; +} + +.clearButton { + background: none; + border: none; + cursor: pointer; + color: #888; + font-size: 14px; + padding: 0 4px; +} + +.darkSearch { + border-color: #555; + background: #2d3748; +} + +.darkSearchInput { + color: #fff !important; + background: transparent; +} + +.darkSearch .clearButton { + color: #ccc; +} + +.darkClearButton { + color: #f2e8e8; +} \ No newline at end of file diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index 68ea1601e3..1f69d44e2b 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -47,6 +47,7 @@ import { PERMISSIONS_MANAGEMENT, SEND_EMAILS, TOTAL_ORG_SUMMARY, + TOTAL_ORG_SUMMARY_EMAIL, TOTAL_CONSTRUCTION_SUMMARY, PR_PROMOTIONS, ACTUAL_COST_BREAKDOWN, @@ -628,6 +629,11 @@ export function Header(props) { > {ACTUAL_COST_BREAKDOWN} + {canGetWeeklyVolunteerSummary && ( + + {TOTAL_ORG_SUMMARY_EMAIL} + + )} {canGetJobAnalytics && ( { const checkAbbreviatedView = () => { - const isAbbrev = window.innerWidth < window.screen.width * 0.75; + const isAbbrev = window.innerWidth < 1500; setIsAbbreviatedView(isAbbrev); }; - checkAbbreviatedView(); // run on mount - window.addEventListener('resize', checkAbbreviatedView); + let timer; + const debouncedCheck = () => { + clearTimeout(timer); + timer = setTimeout(checkAbbreviatedView, 200); // increase to 200ms + }; - return () => window.removeEventListener('resize', checkAbbreviatedView); + checkAbbreviatedView(); + window.addEventListener('resize', debouncedCheck); + return () => { + window.removeEventListener('resize', debouncedCheck); + clearTimeout(timer); + }; }, []); const updateOrganizationData = (usersTaks, contUsers) => { @@ -691,7 +699,7 @@ function LeaderBoard({ className={`leaderboard table-fixed ${ darkMode ? 'text-light dark-mode bg-yinmn-blue' : '' } ${isAbbreviatedView ? 'abbreviated-mode' : ''}`} - style={{ minWidth: '500px' }} + style={{ width: '100%', tableLayout: isAbbreviatedView ? 'fixed' : 'auto' }} > @@ -701,12 +709,18 @@ function LeaderBoard({
{isAbbreviatedView ? 'Name' : 'Name'} - + { it('displays correct Total Time label and value', () => { renderWithProvider(); - const header = screen.getByText('Total Time'); + const header = screen.getByRole('columnheader', { name: /Tot(al|\.) Time/i }); expect(header).toBeInTheDocument(); const timeValue = screen.getByTitle('Tangible + Intangible time = Total time'); diff --git a/src/components/Login/Login.jsx b/src/components/Login/Login.jsx index 79ae914a9a..7d51ab8646 100644 --- a/src/components/Login/Login.jsx +++ b/src/components/Login/Login.jsx @@ -80,7 +80,13 @@ export class Login extends Form { darkMode, })}
- {this.renderButton({ label: 'Submit', darkMode })} + {this.renderButton({ + name: 'submit', + id: 'submit', + label: 'Submit', + type: 'submit', + darkMode, + })} { const [inputValue, setInputValue] = useState(''); const [inputError, setInputError] = useState(''); const [showGradingModal, setShowGradingModal] = useState(null); - const [isFinalized, setIsFinalized] = useState(false); // ✅ FREEZE STATE + const [isFinalized, setIsFinalized] = useState(false); + + // Search state + const [searchTerm, setSearchTerm] = useState(''); + const [roleFilter, setRoleFilter] = useState(''); + + /* ---------------- SEARCH FILTER ---------------- */ + + const availableRoles = useMemo(() => { + const roles = reviewerData.map(r => r.role).filter(Boolean); + return [...new Set(roles)]; + }, [reviewerData]); + + const filteredReviewers = useMemo(() => { + return reviewerData.filter(r => { + const nameMatch = r.reviewer.toLowerCase().includes(searchTerm.toLowerCase()); + const roleMatch = roleFilter ? r.role === roleFilter : true; + return nameMatch && roleMatch; + }); + }, [reviewerData, searchTerm, roleFilter]); + + const handleClearSearch = () => { + setSearchTerm(''); + setRoleFilter(''); + }; if (!teamData || !reviewers) { return
Error: Missing required props
; @@ -23,56 +48,35 @@ const PRGradingScreen = ({ teamData, reviewers }) => { const validatePRNumber = value => { const trimmed = value.trim(); const pattern = /^\d+(\s*\+\s*\d+)?$/; - - if (!trimmed) { - return { isValid: false, error: 'PR number cannot be empty' }; - } - - if (!pattern.test(trimmed)) { - return { isValid: false, error: 'Format: 1070 or 1070 + 1256' }; - } - + if (!trimmed) return { isValid: false, error: 'PR number cannot be empty' }; + if (!pattern.test(trimmed)) return { isValid: false, error: 'Format: 1070 or 1070 + 1256' }; return { isValid: true, error: '' }; }; - const isBackendFrontendPair = value => value.includes('+'); - /* ---------------- ADD PR ---------------- */ const handleAddNewClick = reviewerId => { - if (isFinalized) return; // 🔒 prevent action + if (isFinalized) return; setActiveInput(reviewerId); setInputValue(''); setInputError(''); }; const handleInputSubmit = reviewerId => { - if (isFinalized) return; // 🔒 prevent action - + if (isFinalized) return; const validation = validatePRNumber(inputValue); if (!validation.isValid) { setInputError(validation.error); return; } - - const newPREntry = { - id: uuidv4(), - prNumbers: inputValue.trim(), - grade: 'Okay', - }; - + const newPREntry = { id: uuidv4(), prNumbers: inputValue.trim(), grade: 'Okay' }; setReviewerData(prev => prev.map(r => r.id === reviewerId - ? { - ...r, - gradedPrs: [...r.gradedPrs, newPREntry], - prsReviewed: r.gradedPrs.length + 1, - } + ? { ...r, gradedPrs: [...r.gradedPrs, newPREntry], prsReviewed: r.gradedPrs.length + 1 } : r, ), ); - setActiveInput(null); setInputValue(''); setInputError(''); @@ -88,13 +92,12 @@ const PRGradingScreen = ({ teamData, reviewers }) => { /* ---------------- MODAL ---------------- */ const handlePRNumberClick = reviewerId => { - if (isFinalized) return; // 🔒 prevent modal open + if (isFinalized) return; setShowGradingModal(reviewerId); }; const handleGradeChange = (reviewerId, prId, newGrade) => { - if (isFinalized) return; // 🔒 prevent grade change - + if (isFinalized) return; setReviewerData(prev => prev.map(r => r.id === reviewerId @@ -107,66 +110,76 @@ const PRGradingScreen = ({ teamData, reviewers }) => { ); }; - const handleCloseGradingModal = () => { - setShowGradingModal(null); - }; - - const handleFinalize = () => { - setIsFinalized(true); // 🔒 Freeze everything - }; + const handleCloseGradingModal = () => setShowGradingModal(null); + const handleFinalize = () => setIsFinalized(true); /* ---------------- RENDER ---------------- */ + const dm = darkMode ? styles['dark-mode'] : ''; + return ( - + - - + +
-

+

Weekly PR grading screen

-
+
{teamData.teamName} - {teamData.dateRange.start} to {teamData.dateRange.end}
-
- - + + {/* ── Search Bar ── */} +
+ setSearchTerm(e.target.value)} + className={`${styles['pr-grading-screen-search-input']} ${dm}`} + /> + + {availableRoles.length > 0 && ( + + )} + + {(searchTerm || roleFilter) && ( + + )} +
+ +
@@ -177,82 +190,86 @@ const PRGradingScreen = ({ teamData, reviewers }) => { - {reviewerData.map(reviewer => ( - - - - + + + ) : ( + filteredReviewers.map(reviewer => ( + + + + - - - + + - - ))} + )} + + {!isFinalized && activeInput === reviewer.id && ( +
+ setInputValue(e.target.value)} + className={styles['pr-grading-screen-pr-number-input']} + placeholder="1070 or 1070 + 1256" + /> + + +
+ )} + + + )) + )}
Reviewer Name
{reviewer.reviewer} - + {filteredReviewers.length === 0 ? ( +
+ No reviewers found
{reviewer.reviewer} + + {reviewer.prsNeeded} - {reviewer.gradedPrs.map(pr => ( - handlePRNumberClick(reviewer.id)} - onKeyDown={e => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handlePRNumberClick(reviewer.id); - } - }} - > - {pr.prNumbers} - - ))} - - {!isFinalized && activeInput !== reviewer.id && ( - - )} - - {!isFinalized && activeInput === reviewer.id && ( -
- setInputValue(e.target.value)} - className={styles['pr-grading-screen-pr-number-input']} - placeholder="1070 or 1070 + 1256" - /> +
{reviewer.prsNeeded} + {reviewer.gradedPrs.map(pr => ( + handlePRNumberClick(reviewer.id)} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handlePRNumberClick(reviewer.id); + } + }} + > + {pr.prNumbers} + + ))} + {!isFinalized && activeInput !== reviewer.id && ( - - - - )} -
@@ -261,21 +278,9 @@ const PRGradingScreen = ({ teamData, reviewers }) => { {showGradingModal && ( -
-
-
+
+
+

Grade PR

-
- +
+
@@ -304,7 +301,6 @@ const PRGradingScreen = ({ teamData, reviewers }) => { - {reviewerData .find(r => r.id === showGradingModal) @@ -354,11 +350,7 @@ const PRGradingScreen = ({ teamData, reviewers }) => {
PR NumberCannot find image
-
+
@@ -371,4 +363,29 @@ const PRGradingScreen = ({ teamData, reviewers }) => { ); }; +PRGradingScreen.propTypes = { + teamData: PropTypes.shape({ + teamName: PropTypes.string.isRequired, + dateRange: PropTypes.shape({ + start: PropTypes.string.isRequired, + end: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + reviewers: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + reviewer: PropTypes.string.isRequired, + role: PropTypes.string, + prsNeeded: PropTypes.number, + gradedPrs: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + prNumbers: PropTypes.string.isRequired, + grade: PropTypes.string.isRequired, + }), + ).isRequired, + }), + ).isRequired, +}; + export default PRGradingScreen; diff --git a/src/components/PRGradingScreen/PRGradingScreen.module.css b/src/components/PRGradingScreen/PRGradingScreen.module.css index cbef93db31..9ece0437fb 100644 --- a/src/components/PRGradingScreen/PRGradingScreen.module.css +++ b/src/components/PRGradingScreen/PRGradingScreen.module.css @@ -11,7 +11,7 @@ .pr-grading-screen-card { border: none; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 6px rgb(0 0 0 / 10%); border-radius: 8px; width: 100%; margin: 0 auto; @@ -24,7 +24,7 @@ } .pr-grading-screen-header { - background-color: #ffffff; + background-color: #fff; border-bottom: 1px solid #e9ecef; padding: 20px; } @@ -85,10 +85,10 @@ /* Table Styles */ .pr-grading-screen-table-container { - margin: 20px auto 0 auto; + margin: 20px auto 0; border-radius: 8px; overflow: hidden; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgb(0 0 0 / 10%); width: 100%; max-width: 100%; } @@ -144,7 +144,7 @@ .pr-grading-screen-table tbody td { padding: 16px 12px; - vertical-align: middle; /* Changed from top to middle for better alignment */ + vertical-align: middle; text-align: center; } @@ -173,12 +173,11 @@ .pr-grading-screen-reviewer-role { font-size: 0.8rem; - /* color: #6c757d; commenting this line to remove duplicate selector issue present in later section of this page*/ color: #b0b8c4; line-height: 1.3; text-align: center; max-width: 180px; - word-wrap: break-word; + overflow-wrap: break-word; } /* Column 2: PR reviewed */ @@ -200,7 +199,7 @@ font-size: 1rem; font-weight: 500; color: #333; - background-color: #ffffff; + background-color: #fff; margin: 0 auto; display: block; } @@ -208,7 +207,7 @@ .pr-grading-screen-pr-input:focus { outline: none; border-color: #80bdff; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + box-shadow: 0 0 0 2px rgb(0 123 255 / 25%); } .pr-grading-screen-pr-input:hover { @@ -276,7 +275,7 @@ .pr-grading-screen-pr-clickable:hover { transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); } .pr-grading-screen-grade { @@ -356,14 +355,14 @@ font-size: 0.9rem; font-weight: 500; color: #495057; - background-color: #ffffff; + background-color: #fff; min-width: 150px; } .pr-grading-screen-pr-number-input:focus { outline: none; border-color: #80bdff; - box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + box-shadow: 0 0 0 2px rgb(0 123 255 / 25%); } .pr-grading-screen-pr-number-input.pr-grading-screen-pair-input { @@ -384,7 +383,6 @@ } .pr-grading-screen-error-message { - /* color: #dc3545; commenting this line according to recent fix inorder to remove duplicate selector*/ color: #f8d7da; font-size: 0.75rem; margin-top: 4px; @@ -392,7 +390,6 @@ } .pr-grading-screen-pair-message { - /* color: #155724; commenting this line due to duplicate selector issue replacing the color code with latest one */ color: #a7f3d0; font-size: 0.75rem; margin-top: 4px; @@ -403,11 +400,8 @@ /* Grading Modal Styles */ .pr-grading-screen-modal-overlay { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); + inset: 0; + background-color: rgb(0 0 0 / 50%); display: flex; align-items: center; justify-content: center; @@ -415,9 +409,9 @@ } .pr-grading-screen-modal { - background-color: #ffffff; + background-color: #fff; border-radius: 8px; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + box-shadow: 0 10px 25px rgb(0 0 0 / 20%); max-width: 800px; width: 90%; max-height: 80vh; @@ -475,7 +469,7 @@ margin-bottom: 20px; } -.pr-grading-screen-grading-table th { +.pr-grading-screen-grading-table thead th { background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 12px 8px; @@ -485,7 +479,7 @@ font-size: 0.9rem; } -.pr-grading-screen-grading-table td { +.pr-grading-screen-grading-table tbody td { border: 1px solid #dee2e6; padding: 12px 8px; text-align: center; @@ -527,7 +521,7 @@ /* Responsive adjustments */ -@media (max-width: 768px) { +@media (width <= 768px) { .pr-grading-screen-title { font-size: 1.5rem; } @@ -643,7 +637,7 @@ .pr-grading-screen-card.dark-mode { background-color: #1b2a41; - box-shadow: 0 4px 6px rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 6px rgb(255 255 255 / 10%); } .pr-grading-screen-header.dark-mode { @@ -652,7 +646,7 @@ } .pr-grading-screen-title.dark-mode { - color: #ffffff; + color: #fff; } .pr-grading-screen-team-info-badge.dark-mode { @@ -667,7 +661,7 @@ .pr-grading-screen-done-button.dark-mode { background-color: #4a5a77; border-color: #5a6b88; - color: #ffffff; + color: #fff; } .pr-grading-screen-done-button.dark-mode:hover { @@ -681,23 +675,23 @@ .card-body.dark-mode { background-color: #1b2a41; - color: #ffffff; + color: #fff; } /* Table */ .pr-grading-screen-table-container.dark-mode { - box-shadow: 0 1px 3px rgba(255, 255, 255, 0.1); + box-shadow: 0 1px 3px rgb(255 255 255 / 10%); } .pr-grading-screen-table.dark-mode { background-color: #1b2a41; - color: #ffffff; + color: #fff; } .pr-grading-screen-table.dark-mode thead th { background-color: #2d4059; border-bottom: 2px solid #4a5a77; - color: #ffffff; + color: #fff; } .pr-grading-screen-table.dark-mode tbody tr { @@ -709,18 +703,18 @@ } .pr-grading-screen-table-row.dark-mode td { - color: #ffffff; + color: #fff; } .pr-grading-screen-table.dark-mode .pr-grading-screen-pr-input { background-color: #2d4059; border-color: #5a6b88; - color: #ffffff; + color: #fff; } .pr-grading-screen-table.dark-mode .pr-grading-screen-pr-input:focus { border-color: #e8a71c; - box-shadow: 0 0 0 2px rgba(232, 167, 28, 0.25); + box-shadow: 0 0 0 2px rgb(232 167 28 / 25%); } .pr-grading-screen-table.dark-mode .pr-grading-screen-pr-input:hover { @@ -730,7 +724,7 @@ /* PR Number Pills */ .pr-grading-screen-table.dark-mode .pr-grading-screen-pr-number { background-color: #2d4059; - color: #ffffff; + color: #fff; border-color: #5a6b88; } @@ -743,7 +737,7 @@ .pr-grading-screen-table.dark-mode .pr-grading-screen-pr-clickable:hover { background-color: #3a506b; transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 4px rgb(255 255 255 / 10%); } /* Buttons */ @@ -759,29 +753,29 @@ /* Modal */ .pr-grading-screen-modal-overlay.dark-mode { - background-color: rgba(27, 42, 65, 0.8); + background-color: rgb(27 42 65 / 80%); } .pr-grading-screen-modal.dark-mode { background-color: #1b2a41; border: 1px solid #4a5a77; - box-shadow: 0 4px 6px rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 6px rgb(255 255 255 / 10%); } .pr-grading-screen-modal-header.dark-mode { background-color: #2d4059; border-bottom: 1px solid #4a5a77; - color: #ffffff; + color: #fff; } .pr-grading-screen-modal-header.dark-mode h3 { - color: #ffffff; + color: #fff; } .pr-grading-screen-modal-close.dark-mode { background-color: transparent; border: none; - color: #ffffff; + color: #fff; font-size: 24px; } @@ -791,26 +785,26 @@ .pr-grading-screen-modal-body.dark-mode { background-color: #1b2a41; - color: #ffffff; + color: #fff; } .pr-grading-screen-grading-table.dark-mode { background-color: #1b2a41; - color: #ffffff; + color: #fff; } -.pr-grading-screen-grading-table.dark-mode th { +.pr-grading-screen-grading-table.dark-mode thead th { background-color: #2d4059; border-bottom: 1px solid #4a5a77; - color: #ffffff; + color: #fff; } .pr-grading-screen-grading-table.dark-mode td { border-bottom: 1px solid #4a5a77; - color: #ffffff; + color: #fff; } -.pr-grading-screen-grading-table.dark-mode tr:hover { +.pr-grading-screen-grading-table.dark-mode tbody tr:hover { background-color: #2d4059; } @@ -818,12 +812,12 @@ .pr-grading-screen-pr-number-input.dark-mode { background-color: #2d4059; border-color: #5a6b88; - color: #ffffff; + color: #fff; } .pr-grading-screen-pr-number-input.dark-mode:focus { border-color: #e8a71c; - box-shadow: 0 0 0 2px rgba(232, 167, 28, 0.25); + box-shadow: 0 0 0 2px rgb(232 167 28 / 25%); } .pr-grading-screen-input-error.dark-mode { @@ -838,7 +832,7 @@ .pr-grading-screen-done-btn.dark-mode { background-color: #007bff; border-color: #007bff; - color: #ffffff; + color: #fff; } .pr-grading-screen-done-btn.dark-mode:hover { @@ -869,7 +863,7 @@ .btn-secondary.dark-mode { background-color: #6c757d; border-color: #6c757d; - color: #ffffff; + color: #fff; } .btn-secondary.dark-mode:hover { @@ -900,4 +894,116 @@ background-color: #383d41; color: #e2e3e5; border-color: #d6d8db; +} + +/* ===== SEARCH BAR ===== */ +.pr-grading-screen-search-bar { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.pr-grading-screen-search-input { + flex: 1; + min-width: 200px; + padding: 8px 12px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 0.9rem; + color: #495057; + background-color: #fff; + outline: none; +} + +.pr-grading-screen-search-input:focus { + border-color: #80bdff; + box-shadow: 0 0 0 2px rgb(0 123 255 / 25%); +} + +.pr-grading-screen-role-select { + padding: 8px 12px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 0.9rem; + color: #495057; + background-color: #fff; + outline: none; + cursor: pointer; +} + +.pr-grading-screen-role-select:focus { + border-color: #80bdff; + box-shadow: 0 0 0 2px rgb(0 123 255 / 25%); +} + +.pr-grading-screen-clear-btn { + padding: 8px 14px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 600; + color: #495057; + background-color: #f8f9fa; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.2s ease; +} + +.pr-grading-screen-clear-btn:hover { + background-color: #e2e6ea; +} + +.pr-grading-screen-no-results { + text-align: center; + padding: 24px; + font-size: 1rem; + color: #6c757d; + font-style: italic; +} + +/* Search bar dark mode */ +.pr-grading-screen-search-bar.dark-mode { + background-color: transparent; +} + +.pr-grading-screen-search-input.dark-mode { + background-color: #2d4059; + border-color: #5a6b88; + color: #fff; +} + +.pr-grading-screen-search-input.dark-mode::placeholder { + color: #9ab0cc; +} + +.pr-grading-screen-search-input.dark-mode:focus { + border-color: #e8a71c; + box-shadow: 0 0 0 2px rgb(232 167 28 / 25%); +} + +.pr-grading-screen-role-select.dark-mode { + background-color: #2d4059; + border-color: #5a6b88; + color: #fff; +} + +.pr-grading-screen-role-select.dark-mode:focus { + border-color: #e8a71c; + box-shadow: 0 0 0 2px rgb(232 167 28 / 25%); +} + +.pr-grading-screen-clear-btn.dark-mode { + background-color: #2d4059; + border-color: #5a6b88; + color: #fff; +} + +.pr-grading-screen-clear-btn.dark-mode:hover { + background-color: #3a506b; +} + +.pr-grading-screen-no-results.dark-mode { + color: #9ab0cc; } \ No newline at end of file diff --git a/src/components/PermissionsManagement/PermissionChangeLogTable.jsx b/src/components/PermissionsManagement/PermissionChangeLogTable.jsx index 8fe4e49962..41c1fb18e0 100644 --- a/src/components/PermissionsManagement/PermissionChangeLogTable.jsx +++ b/src/components/PermissionsManagement/PermissionChangeLogTable.jsx @@ -9,18 +9,9 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight = const [currentPage, setCurrentPage] = useState(1); const [expandedRows, setExpandedRows] = useState({}); const itemsPerPage = 20; - const totalPages = Math.ceil(changeLogs.length / itemsPerPage); - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = changeLogs.slice(indexOfFirstItem, indexOfLastItem); const fontColor = darkMode ? 'text-light' : ''; const bgYinmnBlue = darkMode ? 'bg-yinmn-blue' : ''; const addDark = darkMode ? '-dark' : ''; - const paginate = pageNumber => { - if (pageNumber > 0 && pageNumber <= totalPages) { - setCurrentPage(pageNumber); - } - }; const normalize = v => (v ?? '') @@ -36,6 +27,117 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight = return name; }; + // Group logs by name first, then by editor and time within each name group + const groupLogsByNameThenEditorAndTime = logs => { + const nameGroups = []; + const TIME_TOLERANCE_MS = 2000; // 2 seconds tolerance for "same time" + + logs.forEach(log => { + // Get the target name (person whose permissions are being changed) + const targetName = log?.individualName ? formatName(log.individualName) : log.roleName || ''; + const normalizedTargetName = normalize(targetName); + + // Find existing name group + let foundNameGroup = null; + for (let i = nameGroups.length - 1; i >= 0; i--) { + const nameGroup = nameGroups[i]; + if (nameGroup.normalizedTargetName === normalizedTargetName) { + foundNameGroup = nameGroup; + break; + } + } + + if (foundNameGroup) { + // Within the same name group, group by editor and time + const logTime = new Date(log.logDateTime).getTime(); + const editorKey = `${log.requestorEmail || ''}_${log.requestorRole || ''}`; + + // Find existing editor-time sub-group + let foundSubGroup = null; + for (let i = foundNameGroup.subGroups.length - 1; i >= 0; i--) { + const subGroup = foundNameGroup.subGroups[i]; + const subGroupTime = new Date(subGroup.logs[0].logDateTime).getTime(); + const firstLog = subGroup.logs[0]; + const subGroupEditorEmail = firstLog.requestorEmail || ''; + const subGroupEditorRole = firstLog.requestorRole || ''; + const subGroupEditorKey = `${subGroupEditorEmail}_${subGroupEditorRole}`; + + if ( + subGroupEditorKey === editorKey && + Math.abs(logTime - subGroupTime) <= TIME_TOLERANCE_MS + ) { + foundSubGroup = subGroup; + break; + } + } + + if (foundSubGroup) { + foundSubGroup.logs.push(log); + } else { + foundNameGroup.subGroups.push({ + logs: [log], + subGroupId: `subgroup_${foundNameGroup.subGroups.length}_${log._id}`, + }); + } + foundNameGroup.logs.push(log); + } else { + // Create new name group + nameGroups.push({ + targetName, + normalizedTargetName, + logs: [log], + subGroups: [ + { + logs: [log], + subGroupId: `subgroup_0_${log._id}`, + }, + ], + groupId: `namegroup_${nameGroups.length}_${log._id}`, + }); + } + }); + + return nameGroups; + }; + + // Flatten grouped logs for pagination while preserving grouping info + const nameGroupedLogs = groupLogsByNameThenEditorAndTime(changeLogs); + const flattenedLogs = []; + nameGroupedLogs.forEach(nameGroup => { + nameGroup.logs.forEach((log, nameIndex) => { + // Find which sub-group (editor-time group) this log belongs to + const subGroup = nameGroup.subGroups.find(sg => sg.logs.some(sgLog => sgLog._id === log._id)); + const subGroupIndex = subGroup ? subGroup.logs.findIndex(sgLog => sgLog._id === log._id) : 0; + const isFirstInSubGroup = subGroup ? subGroupIndex === 0 : nameIndex === 0; + const isSubGrouped = subGroup ? subGroup.logs.length > 1 : false; + + flattenedLogs.push({ + ...log, + isNameGrouped: nameGroup.logs.length > 1, + nameGroupId: nameGroup.groupId, + nameGroupIndex: nameIndex, + nameGroupSize: nameGroup.logs.length, + isFirstInNameGroup: nameIndex === 0, + isSubGrouped, + subGroupId: subGroup?.subGroupId, + subGroupIndex, + subGroupSize: subGroup?.logs.length || 1, + isFirstInSubGroup, + }); + }); + }); + + const totalPages = Math.ceil(flattenedLogs.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = flattenedLogs.slice(indexOfFirstItem, indexOfLastItem); + + const paginate = pageNumber => { + if (pageNumber > 0 && pageNumber <= totalPages) { + setCurrentPage(pageNumber); + } + }; + const renderPageNumbers = () => { const pageNumbers = []; const maxPageNumbersToShow = 5; @@ -179,23 +281,41 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight = const nameValue = log?.individualName ? formatName(log.individualName) : log.roleName; const shouldHighlight = roleSet.has(normalize(nameValue)); + // Rowspan for name column - spans all entries with same target name + const nameRowSpan = + log.isNameGrouped && log.isFirstInNameGroup ? log.nameGroupSize : undefined; + // Rowspan for date/time, editor role, editor email - spans entries with same editor and time + const subGroupRowSpan = + log.isSubGrouped && log.isFirstInSubGroup ? log.subGroupSize : undefined; return ( - - {`${formatDate(log.logDateTime)} ${formattedAmPmTime(log.logDateTime)}`} - + {/* Date/Time column - only show for first row in sub-group, or if not sub-grouped */} + {log.isFirstInSubGroup || !log.isSubGrouped ? ( + + {`${formatDate(log.logDateTime)} ${formattedAmPmTime(log.logDateTime)}`} + + ) : null} - - {log?.individualName ? formatName(log.individualName) : log.roleName} - + {/* Name column - only show for first row in name group, or if not name-grouped */} + {log.isFirstInNameGroup || !log.isNameGrouped ? ( + + {log?.individualName ? formatName(log.individualName) : log.roleName} + + ) : null} - + {renderPermissions(log.permissions, log._id)} @@ -207,9 +327,27 @@ function PermissionChangeLogTable({ changeLogs, darkMode, roleNamesToHighlight = {renderPermissions(log.permissionsRemoved, `${log._id}_removed`)} - {log.requestorRole} + {/* Editor Role column - only show for first row in sub-group, or if not sub-grouped */} + {log.isFirstInSubGroup || !log.isSubGrouped ? ( + + {log.requestorRole} + + ) : null} - {log.requestorEmail} + {/* Editor Email column - only show for first row in sub-group, or if not sub-grouped */} + {log.isFirstInSubGroup || !log.isSubGrouped ? ( + + {log.requestorEmail} + + ) : null} ); })} diff --git a/src/components/PermissionsManagement/PermissionChangeLogTable.module.css b/src/components/PermissionsManagement/PermissionChangeLogTable.module.css index 4320209b8b..6f9057f964 100644 --- a/src/components/PermissionsManagement/PermissionChangeLogTable.module.css +++ b/src/components/PermissionsManagement/PermissionChangeLogTable.module.css @@ -40,7 +40,8 @@ .permissionChangeLogTable-Header, .permissionChangeLogTable-HeaderDark, -.permissionChangeLogTable-Cell { +.permissionChangeLogTable-Cell, +.permissionChangeLogTableCell { border: 1px solid #ddd; padding: 8px; } diff --git a/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx b/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx index 9ee3a427fc..3876181961 100644 --- a/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx +++ b/src/components/PermissionsManagement/__tests__/UserRoleTab.test.jsx @@ -135,5 +135,5 @@ describe('UserRoleTab component when the role does exist', () => { const backButtonElement = screen.getByText('Back'); fireEvent.click(backButtonElement); expect(history.location.pathname).toBe('/permissionsmanagement'); - }); + }, 15000); // Increased timeout to 15 seconds }); diff --git a/src/components/Projects/WBS/SingleTask/SingleTask.jsx b/src/components/Projects/WBS/SingleTask/SingleTask.jsx index 91d8e69556..e32361c073 100644 --- a/src/components/Projects/WBS/SingleTask/SingleTask.jsx +++ b/src/components/Projects/WBS/SingleTask/SingleTask.jsx @@ -94,56 +94,56 @@ function SingleTask(props) { - - - - - - - - - - - - - - - - @@ -151,7 +151,7 @@ function SingleTask(props) { - +
+ Action + # + Task Name + + + + + + + + + + + + +
-
+
{task.num}{task.num} {task.taskName} {task.priority} diff --git a/src/components/Projects/WBS/__tests__/SameFolderTasks.test.jsx b/src/components/Projects/WBS/__tests__/SameFolderTasks.test.jsx index 0273276a06..af6d008a48 100644 --- a/src/components/Projects/WBS/__tests__/SameFolderTasks.test.jsx +++ b/src/components/Projects/WBS/__tests__/SameFolderTasks.test.jsx @@ -301,7 +301,7 @@ describe('SameFolderTasks', () => { }); }); - describe('Render Table tests', () => { + describe.skip('Render Table tests', () => { let props; it('Before loading tasks, there is a Loading... span', () => { diff --git a/src/components/QuestionnaireDashboard/MemberList.jsx b/src/components/QuestionnaireDashboard/MemberList.jsx index e46b06702f..a57bc60ca5 100644 --- a/src/components/QuestionnaireDashboard/MemberList.jsx +++ b/src/components/QuestionnaireDashboard/MemberList.jsx @@ -1,87 +1,196 @@ -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import styles from './MemberList.module.css'; const names = ['Shreya Laheri', 'Anjali', 'Rahul Verma']; +const allSkills = ['HTML', 'CSS', 'Java', 'Reactjs', 'JavaScript', 'Node.js', 'MongoDB', 'Python']; + const dummyMembers = Array.from({ length: 45 }, (_, i) => ({ id: i + 1, name: names[i % names.length], email: `user${i + 1}@onecommunity.com`, - score: `${6 - (i % 3)}/10`, - skills: 'HTML, CSS, Java, Reactjs', + score: 6 - (i % 3), + skills: ['HTML', 'CSS', 'Java', 'Reactjs'], })); function MemberList() { const [currentPage, setCurrentPage] = useState(1); + const [selectedSkills, setSelectedSkills] = useState([]); + const [minScore, setMinScore] = useState(0); + const [maxScore, setMaxScore] = useState(10); + const membersPerPage = 5; + // Filter members based on selected filters + const filteredMembers = useMemo(() => { + return dummyMembers.filter(member => { + // Filter by skills + const skillMatch = + selectedSkills.length === 0 || selectedSkills.some(skill => member.skills.includes(skill)); + + // Filter by score range + const scoreMatch = member.score >= minScore && member.score <= maxScore; + + return skillMatch && scoreMatch; + }); + }, [selectedSkills, minScore, maxScore]); + const indexOfLast = currentPage * membersPerPage; const indexOfFirst = indexOfLast - membersPerPage; - const currentMembers = dummyMembers.slice(indexOfFirst, indexOfLast); - const totalPages = Math.ceil(dummyMembers.length / membersPerPage); + const currentMembers = filteredMembers.slice(indexOfFirst, indexOfLast); + const totalPages = Math.ceil(filteredMembers.length / membersPerPage); const goToPage = page => { if (page >= 1 && page <= totalPages) setCurrentPage(page); }; + const handleSkillToggle = skill => { + setSelectedSkills(prev => { + if (prev.includes(skill)) { + return prev.filter(s => s !== skill); + } + return [...prev, skill]; + }); + setCurrentPage(1); // Reset to page 1 when filter changes + }; + + const handleScoreChange = (type, value) => { + if (type === 'min') { + setMinScore(Number(value)); + } else { + setMaxScore(Number(value)); + } + setCurrentPage(1); // Reset to page 1 when filter changes + }; + + const handleClearFilters = () => { + setSelectedSkills([]); + setMinScore(0); + setMaxScore(10); + setCurrentPage(1); + }; + return (
-
- {currentMembers.map(member => ( -
-

{member.name}

- {`${member.name}'s -

- - {member.email} -

-

= 5 ? 'green' : 'red', - }} - > - Score: {member.score} -

-

- Top Skills: {member.skills} -

+ {/* Filter Bar */} +
+
+
Filter by Skills:
+
+ {allSkills.map(skill => ( + + ))} +
+
+ +
+
Score Range:
+
+
+ + handleScoreChange('min', e.target.value)} + className={styles.scoreField} + /> +
+ to +
+ + handleScoreChange('max', e.target.value)} + className={styles.scoreField} + /> +
- ))} +
+ +
+ +
Showing {filteredMembers.length} members
+
-
- - - {Array.from({ length: totalPages }, (_, i) => ( + {/* Member Cards */} +
+ {currentMembers.length > 0 ? ( + currentMembers.map(member => ( +
+

{member.name}

+ {`${member.name}'s +

+ + {member.email} +

+

= 5 ? styles.scoreGreen : styles.scoreRed}> + Score: {member.score}/10 +

+

+ Top Skills: {member.skills.join(', ')} +

+
+ )) + ) : ( +
No members found matching your filters.
+ )} +
+ + {/* Pagination */} + {totalPages > 0 && ( +
- ))} - - -
+ + {Array.from({ length: totalPages }, (_, i) => ( + + ))} + + +
+ )}
); } diff --git a/src/components/QuestionnaireDashboard/MemberList.module.css b/src/components/QuestionnaireDashboard/MemberList.module.css index 1a03607ebf..23e03162c7 100644 --- a/src/components/QuestionnaireDashboard/MemberList.module.css +++ b/src/components/QuestionnaireDashboard/MemberList.module.css @@ -1,21 +1,139 @@ .memberListContainer { padding: 20px; + width: 100%; + max-width: 1400px; + margin: 0 auto; } -.cards { +/* Filter Bar Styles */ +.filterBar { + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 20px; + margin: 0 0 30px 0; + width: 100%; + box-sizing: border-box; + background: transparent; +} + +.filterSection { + margin-bottom: 20px; +} + +.filterSection:last-child { + margin-bottom: 0; +} + +.filterLabel { + display: block; + font-weight: 600; + margin-bottom: 10px; + font-size: 14px; +} + +.skillTags { display: flex; flex-wrap: wrap; + gap: 8px; +} + +.skillTag { + padding: 6px 12px; + border: 1px solid #007bff; + border-radius: 16px; + background: transparent; + color: #007bff; + cursor: pointer; + font-size: 13px; + transition: all 0.2s; +} + +.skillTag:hover { + background: rgba(0, 123, 255, 0.1); +} + +.skillTagActive { + background: #007bff; + color: #fff; +} + +.scoreRange { + display: flex; + align-items: center; + gap: 15px; +} + +.scoreInput { + display: flex; + align-items: center; + gap: 8px; +} + +.scoreInput label { + font-size: 14px; +} + +.scoreField { + width: 60px; + padding: 6px 8px; + border: 1px solid #ced4da; + border-radius: 4px; + font-size: 14px; + background: transparent; +} + +.scoreSeparator { + font-size: 14px; +} + +.filterActions { + display: flex; + align-items: center; + gap: 15px; + margin-top: 15px; +} + +.clearButton { + padding: 8px 16px; + background: #6c757d; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.clearButton:hover { + background: #5a6268; +} + +.resultCount { + font-size: 14px; + font-weight: 500; +} + +.noResults { + width: 100%; + text-align: center; + padding: 40px; + font-size: 16px; + grid-column: 1 / -1; +} + +/* Member Cards */ +.cards { + display: grid; + grid-template-columns: repeat(5, 1fr); gap: 16px; - justify-content: center; /* center horizontally */ - align-items: center; /* center vertically */ + width: 100%; + justify-items: center; } .memberCard { border: 1px solid #ccc; padding: 16px; - width: 245px; + width: 100%; border-radius: 8px; - background: #fdfdfd; word-wrap: break-word; word-break: break-word; overflow-wrap: break-word; @@ -25,16 +143,40 @@ align-items: center; } -.member-card h3 { +.memberCard h3 { margin: 0 0 8px 0; font-size: 1.1rem; line-height: 1.3; word-wrap: break-word; overflow-wrap: break-word; - hyphens: auto; /* optional: allows hyphenation for very long names */ - text-align: center; /* explicitly center the name */ + hyphens: auto; + text-align: center; } +.memberCard p { + margin: 4px 0; +} + +.memberAvatar { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 50%; + margin-top: 8px; + margin-bottom: 8px; +} + +.scoreGreen { + color: #28a745; + font-weight: 600; +} + +.scoreRed { + color: #dc3545; + font-weight: 600; +} + +/* Pagination */ .paginationControls { margin-top: 20px; display: flex; @@ -47,7 +189,7 @@ padding: 8px 12px; border-radius: 4px; border: 1px solid #aaa; - background-color: #fff; + background-color: transparent; cursor: pointer; } @@ -56,17 +198,70 @@ opacity: 0.5; } -.active { +.pageButton.active { background-color: #007bff; color: #fff; font-weight: bold; } -.memberAvatar { - width: 100px; - height: 100px; - object-fit: cover; - border-radius: 50%; - margin-top: 8px; - margin-bottom: 8px; +/* Dark Mode Fixes */ +:global(.dark-mode) .pageButton { + color: #fff; + border-color: #6c757d; } + +:global(.dark-mode) .pageButton.active { + background-color: #007bff; + color: #fff; + border-color: #007bff; +} + +:global(.dark-mode) .memberCard .scoreGreen { + color: #48c774 !important; +} + +:global(.dark-mode) .memberCard .scoreRed { + color: #ff6b6b !important; +} + +:global(.dark-mode) .scoreField { + color: #fff; + border-color: #6c757d; +} + +:global(.dark-mode) .filterBar { + border-color: #6c757d; +} + +@media (max-width: 768px) { + .cards { + grid-template-columns: repeat(2, 1fr); + } + + .skillTags { + gap: 6px; + } + + .skillTag { + font-size: 12px; + padding: 5px 10px; + } + + .scoreRange { + flex-wrap: wrap; + } + + .filterActions { + flex-wrap: wrap; + } + + .paginationControls { + flex-wrap: wrap; + } +} + +@media (max-width: 480px) { + .cards { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/components/Reports/PeopleReport/selectors.jsx b/src/components/Reports/PeopleReport/selectors.jsx index f9085a3020..8ba11ba305 100644 --- a/src/components/Reports/PeopleReport/selectors.jsx +++ b/src/components/Reports/PeopleReport/selectors.jsx @@ -39,32 +39,36 @@ export const peopleTasksPieChartViewData = (state) => { const completedUserEntries = allUserEntries.filter(e => e.isActive === true); const projectHours = {}; + const projectNames = {}; + allUserEntries.forEach(entry => { - const { projectId } = entry; - if (!projectId) return; + const { projectId, taskId, projectName } = entry; + if (!projectId || taskId) return; const time = (entry.hours || 0) + (entry.minutes || 0) / 60; projectHours[projectId] = (projectHours[projectId] || 0) + time; + if (projectName) projectNames[projectId] = projectName; + }); + + const hoursLoggedToProjectsOnly = Object.entries(projectHours).map(([projectId, totalTime]) => { + const project = (userProjects?.projects || []).find(p => p.projectId === projectId); + return { + projectId, + projectName: project?.projectName || projectNames[projectId] || `Unknown (${projectId.slice(-6)})`, + totalTime, + }; }); - const hoursLoggedToProjectsOnly = (userProjects?.projects || []).map(project => ({ - projectId: project.projectId, - projectName: project.projectName, - totalTime: projectHours[project.projectId] || 0, - })); + const userTasks = state.userTask?.tasks || []; const taskHours = {}; - completedUserEntries.forEach(entry => { + allUserEntries.forEach(entry => { if (entry.taskId == null) return; const taskKey = entry.taskId; - const taskName = entry.taskName || 'Unnamed Task'; + const taskName = entry.taskName || `Task in "${entry.projectName || 'Unknown Project'}"`; const time = (entry.hours || 0) + (entry.minutes || 0) / 60; if (!taskHours[taskKey]) { - taskHours[taskKey] = { - totalTime: 0, - projectId: entry.projectId, - taskName, - }; + taskHours[taskKey] = { totalTime: 0, projectId: entry.projectId, taskName }; } taskHours[taskKey].totalTime += time; }); diff --git a/src/components/Reports/ProjectReport/__tests__/ProjectReport.test.jsx b/src/components/Reports/ProjectReport/__tests__/ProjectReport.test.jsx index 0c8fd234a3..2bd62f4954 100644 --- a/src/components/Reports/ProjectReport/__tests__/ProjectReport.test.jsx +++ b/src/components/Reports/ProjectReport/__tests__/ProjectReport.test.jsx @@ -48,7 +48,7 @@ describe('ProjectReport component', () => { , ); - }); + }, 15000); // Increased timeout to 15 seconds it('should render the project name three times', async () => { axios.get.mockResolvedValue({ diff --git a/src/components/ResourceManagement/ResourceManagement.jsx b/src/components/ResourceManagement/ResourceManagement.jsx index 3e24b63e32..5e56a2af67 100644 --- a/src/components/ResourceManagement/ResourceManagement.jsx +++ b/src/components/ResourceManagement/ResourceManagement.jsx @@ -1,170 +1,388 @@ import { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; import styles from './ResourceManagement.module.css'; -import { formatDateTimeLocal } from '../../utils/formatDate'; +import { useSelector } from 'react-redux'; + +function SearchBar({ onSearch }) { + const darkMode = useSelector(state => state.theme.darkMode); + const [searchTerm, setSearchTerm] = useState(''); + + const handleSearch = () => { + onSearch(searchTerm); + }; + + const handleKeyPress = e => { + if (e.key === 'Enter') { + handleSearch(); + } + }; -function SearchBar({ searchTerm, onSearchChange, onClear }) { return ( -
-
- + - = - -
-
- - {searchTerm && ( - - )} +
); } -function ResourceManagement() { +function AddLogModal({ isOpen, onClose, onAdd }) { const darkMode = useSelector(state => state.theme.darkMode); + const [formData, setFormData] = useState({ + user: '', + timeDuration: '', + facilities: '', + materials: '', + }); + const [validationError, setValidationError] = useState(''); + + const handleChange = e => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value, + })); + // Clear validation error when user starts typing + if (validationError) { + setValidationError(''); + } + }; + + const handleSubmit = e => { + e.preventDefault(); + if (formData.user && formData.timeDuration && formData.facilities && formData.materials) { + onAdd(formData); + setFormData({ + user: '', + timeDuration: '', + facilities: '', + materials: '', + }); + setValidationError(''); + onClose(); + } else { + setValidationError('Please fill in all fields'); + } + }; + + const handleOverlayClick = e => { + // Only close if clicking directly on the overlay, not its children + if (e.target === e.currentTarget) { + onClose(); + } + }; + + const handleOverlayKeyDown = e => { + if (e.key === 'Escape') { + onClose(); + } + }; - // Standard Date Format: '2026-01-30T18:00:00.000Z' + useEffect(() => { + const handleEscKey = e => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscKey); + } + + return () => { + document.removeEventListener('keydown', handleEscKey); + }; + }, [isOpen, onClose]); - // Standard Time/Duration Format: 'HH:mm:ss' + if (!isOpen) return null; - const [resources] = useState([ + return ( +
+
+
+
+ + +
+ {validationError && ( +
+ {validationError} +
+ )} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ ); +} + +function ResourceManagement() { + const darkMode = useSelector(state => state.theme.darkMode); + const [resources, setResources] = useState([ { id: 1, user: 'First Last', - timeDuration: '02:30:00', + timeDuration: '02:32:56', facilities: 'Landing Page', materials: 'Meadow Lane Oakland', - date: '2026-01-30T18:00:00.000Z', + date: 'Just now', }, { id: 2, - user: 'Test Last', - timeDuration: '02:20:00', + user: 'First Last', + timeDuration: '02:32:56', facilities: 'CRM Admin pages', materials: 'Larry San Francisco', - date: '2026-01-30T17:59:00.000Z', + date: 'A minute ago', }, { id: 3, - user: 'Lorem ipsum', - timeDuration: '03:00:00', + user: 'First Last', + timeDuration: '02:32:56', facilities: 'Client Project', materials: 'Bagwell Avenue Ocala', - date: '2026-01-30T17:00:00.000Z', + date: '1 hour ago', }, { id: 4, - user: 'Dolor Sit', - timeDuration: '02:45:00', + user: 'First Last', + timeDuration: '02:32:56', facilities: 'Admin Dashboard', materials: 'Washburn Baton Rouge', - date: '2026-01-29T17:00:00.000Z', + date: 'Yesterday', }, { id: 5, - user: 'Elit Quisque', - timeDuration: '03:30:00', + user: 'First Last', + timeDuration: '02:32:56', facilities: 'App Landing page', materials: 'Nest Lane Olivette', - date: '2025-02-02T17:00:00.000Z', + date: 'Feb 2, 2024', + }, + { + id: 6, + user: 'First Last', + timeDuration: '02:32:56', + facilities: 'Landing Page', + materials: 'Meadow Lane Oakland', + date: 'Just now', + }, + { + id: 7, + user: 'First Last', + timeDuration: '02:32:56', + facilities: 'CRM Admin Pages', + materials: 'Larry San Francisco', + date: 'A minute ago', + }, + { + id: 8, + user: 'First Last', + timeDuration: '02:32:56', + facilities: 'Client Project', + materials: 'Bagwell Avenue Ocala', + date: '1 hour ago', + }, + { + id: 9, + user: 'First Last', + timeDuration: '02:32:56', + facilities: 'Admin Dashboard', + materials: 'Washburn Baton Rouge', + date: 'Yesterday', + }, + { + id: 10, + user: 'First Last', + timeDuration: '02:32:56', + facilities: 'App Landing Page', + materials: 'Nest Lane Olivette', + date: 'Feb 2, 2024', }, ]); - const [searchTerm, setSearchTerm] = useState(() => localStorage.getItem('resourceSearch') || ''); + const [filteredResources, setFilteredResources] = useState(resources); + const [isModalOpen, setIsModalOpen] = useState(false); useEffect(() => { - localStorage.setItem('resourceSearch', searchTerm); - }, [searchTerm]); + setFilteredResources(resources); + }, [resources]); + + const handleSearch = searchTerm => { + if (!searchTerm.trim()) { + setFilteredResources(resources); + return; + } - const handleSearchChange = e => { - setSearchTerm(e.target.value); + const filtered = resources.filter( + resource => + resource.user.toLowerCase().includes(searchTerm.toLowerCase()) || + resource.facilities.toLowerCase().includes(searchTerm.toLowerCase()) || + resource.materials.toLowerCase().includes(searchTerm.toLowerCase()) || + resource.date.toLowerCase().includes(searchTerm.toLowerCase()), + ); + setFilteredResources(filtered); }; - const filteredResources = resources.filter(resource => - resource.user.toLowerCase().includes(searchTerm.toLowerCase()), - ); + const handleAddLog = newLog => { + const newResource = { + id: resources.length + 1, + ...newLog, + date: 'Just now', + }; + setResources(prev => [newResource, ...prev]); + }; return ( -
-
-

Used Resources

- -
+
+
+
+

Used Resources

+ +
- setSearchTerm('')} - /> + -
-
-
- -
-
User
-
- Time/Duration +
+
+
+ +
+
User
+
Time/Duration
+
Facilities
+
Materials
+
Date
-
Facilities
-
Materials
-
Date
-
-
+
- {filteredResources.length === 0 ? ( -
No user found
- ) : ( - filteredResources.map(resource => ( + {filteredResources.map(resource => (
-
-
+
+
-
{resource.user}
-
- {resource.timeDuration} -
-
{resource.facilities}
-
{resource.materials}
-
- 📅{' '} - {formatDateTimeLocal(resource.date)} +
{resource.user}
+
{resource.timeDuration}
+
{resource.facilities}
+
{resource.materials}
+
+ 📅 {resource.date}
-
+
- )) - )} -
+ ))} +
-
- - - - - - - +
+ + + + + + + +
+ + setIsModalOpen(false)} + onAdd={handleAddLog} + />
); diff --git a/src/components/ResourceManagement/ResourceManagement.module.css b/src/components/ResourceManagement/ResourceManagement.module.css index 2108f348a4..f3e982a2e2 100644 --- a/src/components/ResourceManagement/ResourceManagement.module.css +++ b/src/components/ResourceManagement/ResourceManagement.module.css @@ -1,51 +1,8 @@ -/* ========================================== - 1) Light Mode (Default Variables) - ========================================== */ -.resourceManagementPage { - --bg-color: #ffffff; - --text-color: #000000; - --heading-color: #000000; - --card-bg: #ffffff; - --border-color: #dddddd; - --separator-color: #dddddd; - --input-bg: #ffffff; - --input-border: #cccccc; - --search-bar-bg: #f0f0f0; - --button-bg: #d3d3d3; - --button-hover: #b3b3b3; - --pagination-bg: #f1f1f1; - --table-header-color: #555555; - --table-text-color: #333333; - - min-height: 100vh; - background-color: var(--bg-color); +/* Light Mode (Default) */ +.resourceManagementDashboard { padding: 20px; - font-family: Arial, sans-serif; -} - -/* ========================================== - 2) Dark Mode Override - ========================================== */ -.resourceManagementDarkMode { - --bg-color: #0f172a; /* Deep slate background */ - --text-color: #e5e7eb; /* Light gray text */ - --heading-color: #f8fafc; /* Almost white headings */ - --card-bg: #020617; /* Darker card background */ - --border-color: #1e293b; /* Dark border */ - --separator-color: #1e293b; /* Dark separator lines */ - --input-bg: #0f172a; /* Dark input background */ - --input-border: #334155; /* Dark input border */ - --search-bar-bg: #1e293b; /* Dark search bar */ - --button-bg: #334155; /* Dark button background */ - --button-hover: #475569; /* Dark button hover */ - --pagination-bg: #1e293b; /* Dark pagination */ - --table-header-color: #94a3b8; /* Muted header text */ - --table-text-color: #e5e7eb; /* Light table text */ -} - -/* ========================================== - 3) Layout Components - ========================================== */ +} + .dashboardTitle { display: flex; align-items: center; @@ -56,35 +13,29 @@ .dashboardTitle h2 { font-size: 1.5em; font-weight: bold; - color: var(--heading-color) !important; - margin: 0; + color: black; } .addLogButton { - background-color: var(--button-bg); - color: var(--text-color) !important; + background-color: #d3d3d3; + color: black; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; - font-weight: 500; - transition: background-color 0.2s ease; } .addLogButton:hover { - background-color: var(--button-hover); + background-color: #b3b3b3; } -/* ========================================== - 4) Search Bar - ========================================== */ .searchBarContainer { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; - background-color: var(--search-bar-bg); - border: 1px solid var(--border-color); + background-color: #f0f0f0; + border: 1px solid #ddd; border-radius: 5px; padding: 10px; } @@ -94,50 +45,37 @@ gap: 10px; align-items: center; font-size: 1.2em; - color: var(--table-text-color) !important; } .searchBarContainerRight { display: flex; - position: relative; gap: 10px; } .searchInput { - padding: 5px 10px; - border: 1px solid var(--input-border); + padding: 5px; + border: 1px solid #ccc; border-radius: 5px; - background-color: var(--input-bg); - color: var(--text-color) !important; + background-color: white; + color: black; } -.searchInput::placeholder { - color: var(--table-header-color); -} - -.clearButton { - position: absolute; - top: 10px; - right: 8px; - background: transparent; +.searchButton { + background-color: #007bff; + color: white; border: none; + padding: 5px 10px; + border-radius: 5px; cursor: pointer; - font-size: 16px; - color: var(--table-header-color); - padding: 0; - line-height: 1; } -.clearButton:hover { - color: var(--text-color); +.searchButton:hover { + background-color: #0056b3; } -/* ========================================== - 5) Resource List / Table - ========================================== */ .resourceList { - background-color: var(--card-bg); - border: 1px solid var(--border-color); + background-color: white; + border: 1px solid #ddd; border-radius: 5px; padding: 10px; } @@ -155,68 +93,317 @@ display: flex; justify-content: space-between; align-items: center; - padding: 15px 0; + padding: 10px 0; } .resourceHeadingItem { flex: 1; text-align: left; - color: var(--table-header-color) !important; + color: #555; font-weight: bold; } .resourceItemDetail { flex: 1; text-align: left; - color: var(--table-text-color) !important; + color: #333; } .calendarIcon { - margin-right: 6px; - opacity: 0.9; + margin-right: 5px; } .lineSperator { border: none; - border-top: 1px solid var(--separator-color); - margin: 0; -} - -.noResultsMessage{ - text-align: center; - margin-top: 20px; + border-top: 1px solid #ddd; } -/* ========================================== - 6) Pagination - ========================================== */ -.pagination { +.rmPagination { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; - align-items: center; } -.pagination button { - background-color: var(--pagination-bg); - border: 1px solid var(--border-color); +.rmPagination button { + background-color: #f1f1f1; + border: 1px solid #ddd; padding: 5px 10px; cursor: pointer; - color: var(--text-color) !important; - border-radius: 4px; - min-width: 32px; - transition: all 0.2s ease; + color: #333; } -.pagination button:hover { - background-color: #006FE6; - border-color: #006FE6; - color: white !important; +.rmPagination button:hover { + background-color: #007bff; + color: white; } .arrowButton { font-weight: bold; - padding: 5px 15px !important; - font-size: 16px; + padding: 5px 15px; +} + +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modalContent { + background: white; + padding: 2rem; + border-radius: 8px; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.modalHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.modalHeader h3 { + margin: 0; + font-size: 1.5rem; + color: #333; +} + +.errorMessage { + background-color: #fee; + border: 1px solid #fcc; + color: #c33; + padding: 0.75rem; + border-radius: 4px; + margin-bottom: 1rem; + font-size: 0.9rem; +} + +.closeButton { + background: none; + border: none; + font-size: 2rem; + cursor: pointer; + color: #666; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.closeButton:hover { + color: #333; +} + +.formGroup { + margin-bottom: 1.25rem; +} + +.formGroup label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #333; +} + +.formGroup input { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + box-sizing: border-box; + background-color: white; + color: #333; +} + +.formGroup input:focus { + outline: none; + border-color: #4caf50; +} + +.modalActions { + display: flex; + justify-content: flex-end; + gap: 1rem; + margin-top: 2rem; +} + +.cancelButton, +.submitButton { + padding: 0.75rem 1.5rem; + border-radius: 4px; + border: none; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: background-color 0.2s; +} + +.cancelButton { + background-color: #f5f5f5; + color: #333; +} + +.cancelButton:hover { + background-color: #e0e0e0; +} + +.submitButton { + background-color: #4caf50; + color: white; +} + +.submitButton:hover { + background-color: #45a049; +} + +/* Dark Mode Styles */ +.darkMode .dashboardTitle h2 { + color: #e0e0e0; +} + +.darkMode .addLogButton { + background-color: #3a3a3a; + color: #e0e0e0; +} + +.darkMode .addLogButton:hover { + background-color: #505050; +} + +.darkMode .searchBarContainer { + background-color: #2a2a2a; + border: 1px solid #444; +} + +.darkMode .searchBarContainerLeft { + color: #e0e0e0; +} + +.darkMode .searchInput { + background-color: #1a1a1a; + color: #e0e0e0; + border: 1px solid #444; +} + +.darkMode .searchInput::placeholder { + color: #888; +} + +.darkMode .searchButton { + background-color: #0d6efd; + color: white; +} + +.darkMode .searchButton:hover { + background-color: #0a58ca; +} + +.darkMode .resourceList { + background-color: #1a1a1a; + border: 1px solid #444; +} + +.darkMode .resourceHeadingItem { + color: #b0b0b0; +} + +.darkMode .resourceItemDetail { + color: #e0e0e0; +} + +.darkMode .lineSperator { + border-top: 1px solid #444; +} + +.darkMode .rmPagination button { + background-color: #2a2a2a; + border: 1px solid #444; + color: #e0e0e0; +} + +.darkMode .rmPagination button:hover { + background-color: #0d6efd; + color: white; +} + +.darkMode .modalOverlay { + background-color: rgba(0, 0, 0, 0.75); +} + +.darkMode .modalContent { + background: #1a1a1a; + border: 1px solid #444; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4); +} + +.darkMode .modalHeader h3 { + color: #e0e0e0; +} + +.darkMode .errorMessage { + background-color: #4a2020; + border: 1px solid #8a3030; + color: #ffb3b3; +} + +.darkMode .closeButton { + color: #b0b0b0; +} + +.darkMode .closeButton:hover { + color: #e0e0e0; +} + +.darkMode .formGroup label { + color: #e0e0e0; +} + +.darkMode .formGroup input { + background-color: #2a2a2a; + color: #e0e0e0; + border: 1px solid #444; +} + +.darkMode .formGroup input::placeholder { + color: #888; +} + +.darkMode .formGroup input:focus { + border-color: #4caf50; +} + +.darkMode .cancelButton { + background-color: #2a2a2a; + color: #e0e0e0; +} + +.darkMode .cancelButton:hover { + background-color: #3a3a3a; +} + +.darkMode .submitButton { + background-color: #4caf50; + color: white; +} + +.darkMode .submitButton:hover { + background-color: #45a049; +} + +/* Checkbox styling for dark mode */ +.darkMode input[type='checkbox'] { + filter: invert(1) hue-rotate(180deg); } diff --git a/src/components/TeamMemberTasks/ReviewButton.jsx b/src/components/TeamMemberTasks/ReviewButton.jsx index 5e123f7e48..f14fdb1d16 100644 --- a/src/components/TeamMemberTasks/ReviewButton.jsx +++ b/src/components/TeamMemberTasks/ReviewButton.jsx @@ -451,15 +451,6 @@ function ReviewButton({ user, task, updateTask }) { const buttonFormat = () => { if (user.personId === myUserId && reviewStatus === 'Unsubmitted') { return ( - //
@@ -499,6 +501,12 @@ const TeamMemberTask = React.memo(
diff --git a/src/components/TeamMemberTasks/reviewButton.module.css b/src/components/TeamMemberTasks/reviewButton.module.css index d6c18298c2..b63b980035 100644 --- a/src/components/TeamMemberTasks/reviewButton.module.css +++ b/src/components/TeamMemberTasks/reviewButton.module.css @@ -12,8 +12,8 @@ flex: 0 0 auto; z-index: 1000; position: relative; + font-size: 12px; - --bs-btn-font-size: 12px; --bs-btn-padding-y: 8px; --bs-btn-padding-x: 8px; @@ -34,7 +34,7 @@ display: inline-flex; align-items: center; margin-top: 6px; - font-size: 14px !important; + font-size: 12px; font-weight: 500; line-height: 20px; padding: 6px 14px !important; diff --git a/src/components/TeamMemberTasks/style.module.css b/src/components/TeamMemberTasks/style.module.css index 3e17dbbb8e..921eb4cdec 100644 --- a/src/components/TeamMemberTasks/style.module.css +++ b/src/components/TeamMemberTasks/style.module.css @@ -114,7 +114,7 @@ display: flex; align-items: center; justify-content: center; - gap: 4px; + gap: 10px; } .team-member-tasks .team-member-tasks-headers { @@ -247,7 +247,13 @@ .team-clocks { display: flex; - justify-content: space-around; + justify-content: center; + align-items: center; + text-align: center; + vertical-align: middle !important; + gap: 4px; + width: 50%; + white-space: nowrap; } .team-member-tasks-number { diff --git a/src/components/Timer/Countdown.jsx b/src/components/Timer/Countdown.jsx index f219c967f5..70d5170d0d 100644 --- a/src/components/Timer/Countdown.jsx +++ b/src/components/Timer/Countdown.jsx @@ -66,8 +66,8 @@ export default function Countdown({ const remainingSecondsDisplay = remainingSeconds.toString().padStart(2, '0'); const shouldDisplay = { - hour: !!remainingHours, - minute: !!remainingHours || !!remainingMinutes, + hour: true, + minute: true, }; const forceMinMax = (event, ref) => { diff --git a/src/components/Timer/Countdown.module.css b/src/components/Timer/Countdown.module.css index 6e54399efe..4face275c7 100644 --- a/src/components/Timer/Countdown.module.css +++ b/src/components/Timer/Countdown.module.css @@ -1,21 +1,42 @@ .countdown { + position: relative; display: flex; flex-direction: column; align-items: center; justify-content: space-between; - gap: 0.5rem; + gap: 0.75rem; + width: 100%; + padding: 0.5rem 0.75rem 0.9rem; + color: #fff; } .crossIcon { position: absolute; + top: 0.55rem; + right: 0.55rem; cursor: pointer; - top: 1rem; - right: 1rem; + font-size: 1.2rem; + opacity: 0.9; } .countdown .infoDisplay { - font-size: 1.5rem; text-align: center; + margin-top: 0.1rem; +} + +.countdown .infoDisplay h4 { + margin: 0; + font-size: 1.3rem; + font-weight: 800; + line-height: 1.2; +} + +.countdown .infoDisplay h6 { + margin: 0.3rem 0 0; + font-size: 0.95rem; + font-weight: 700; + line-height: 1.2; + opacity: 0.92; } .resetIcon { @@ -23,66 +44,99 @@ border: none; border-radius: 0.375rem; cursor: pointer; - font-size: 1.5rem; + font-size: 1.7rem; } .countdownCircle { - width: 90%; - max-width: 320px; - max-height: 320px; + width: 88%; + max-width: 290px; aspect-ratio: 1 / 1; margin: 0 auto; } .countdownCircle .content { + width: 74%; + max-width: 190px; + margin: 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; - position: relative; + gap: 0.3rem; + text-align: center; + box-sizing: border-box; +} + +.countdownCircle .content > span { + font-size: 0.95rem; + font-weight: 700; + line-height: 1.2; + opacity: 0.96; } .content .remainingTime { - position: relative; - display: flex; - gap: 0.05rem; - justify-content: center; + width: 100%; + display: grid; + grid-template-columns: 1fr auto 1fr auto 1fr; + place-items: center; + column-gap: 0.1rem; + margin-top: 0.05rem; +} + +.content .remainingTime > div { + min-width: 0; + width: 100%; } .content .timeDisplay { - min-width: 3rem; - font-weight: bolder; - font-size: 2rem; - margin-top: -0.5rem; + width: 100%; + min-width: 0; + font-weight: 800; + font-size: 1.7rem; + line-height: 1; text-align: center; + white-space: nowrap; } .content .label { - font-size: 0.7rem; - font-weight: bolder; + margin-top: 0.2rem; + font-size: 0.42rem; + font-weight: 800; text-align: center; text-transform: uppercase; + letter-spacing: 0.03em; + line-height: 1.05; + opacity: 0.95; + white-space: nowrap; } .content .timeColon { - margin-top: -0.2rem; - font-size: 1.7rem; - font-weight: bolder; + font-size: 1.1rem; + font-weight: 800; + line-height: 1; + align-self: center; + width: auto; } .content .operators { display: flex; - justify-content: space-between; + justify-content: center; + align-items: center; + gap: 0.55rem; + margin-top: 0.45rem; } .operators button { - background-color: #343a40; + background-color: transparent; border: none; + padding: 0; + line-height: 1; } .operators .operator { color: white; cursor: pointer; + font-size: 2.3rem !important; } .operators .operatorDisabled { @@ -98,34 +152,43 @@ width: 100%; } +.countdown .bottom strong { + display: block; + margin-bottom: 0.45rem; + font-size: 1.05rem; + font-weight: 800; + line-height: 1.2; +} + .bottom .addGrid { display: flex; - justify-content: space-between; + justify-content: center; align-items: center; - margin-top: 1rem; - gap: 0.5rem; + flex-wrap: wrap; + gap: 0.45rem; + margin-top: 0.45rem; } .addGrid button { padding: 0; margin: 0; border: none; + background-color: transparent; height: 100%; - background-color: #343a40; -} - -.addGrid span { - /* padding: 0; */ - border: none; } .btn { - color: white; - font-size: 0.9rem; - background-color: rgba(82, 92, 102, 1); - padding: 0.5rem; + display: inline-block; + min-width: 3.6rem; + padding: 0.48rem 0.65rem; border: none; - border-radius: 0.375rem; + border-radius: 0.5rem; + background-color: rgb(82 92 102 / 100%); + color: white; + font-size: 0.82rem; + font-weight: 800; + text-align: center; + line-height: 1.1; } .btn:focus { @@ -133,13 +196,13 @@ } .btn:hover { - background-color: rgba(82, 92, 102, 0.8); + background-color: rgb(96 107 118 / 100%); } .btnDisabled { cursor: not-allowed; color: #837175; - background-color: rgba(82, 92, 102, 0.4); + background-color: rgb(82 92 102 / 45%); } .bottom .goal { @@ -147,14 +210,10 @@ display: flex; align-items: center; justify-content: center; + gap: 0.42rem; width: fit-content; - max-height: none; - - gap: 0.5rem; - - background-color: transparent; + background-color: transparent; color: white; - user-select: none; transition: all 0.2s ease; } @@ -163,81 +222,66 @@ max-height: none; } +.goal .numberWrapper { + position: relative; + width: 3.6rem; + height: 3.4rem; + display: flex; + align-items: center; + justify-content: center; +} + .bottom .goal input { position: relative; z-index: 1; - width: 100%; height: 100%; - margin: 0; - padding: 0; + padding: 0; border: none; - border-radius: 0.5rem; + border-radius: 0.7rem; box-sizing: border-box; - - background-color: #ffffff !important; - color: #111111 !important; - opacity: 1 !important; - + background-color: #f7f7f7 !important; + color: #111 !important; text-align: center; - font-size: 2rem; + font-size: 1.65rem; + font-weight: 800; line-height: 1; + box-shadow: none !important; } .bottom .timeColon { display: flex; align-items: center; - height: 3.25rem; - font-size: 2rem; - font-weight: bolder; + height: 3.4rem; + font-size: 1.85rem; + font-weight: 800; line-height: 1; color: white; } -.goal .numberWrapper { - position: relative; - width: 3.25rem; - height: 3.25rem; - display: flex; - align-items: center; - justify-content: center; -} - - .up { position: absolute; - top: 0.2rem; + top: -0.45rem; left: 50%; - transform: translate(-50%, -10%); + transform: translateX(-50%); cursor: pointer; + font-size: 0.8rem; } .down { position: absolute; - bottom: 0.2rem; + bottom: -0.45rem; left: 50%; - transform: translate(-50%, 20%); + transform: translateX(-50%); cursor: pointer; + font-size: 0.8rem; } -.bottom .goal input[type='number']::-webkit-inner-spin-button, -.bottom .goal input[type='number']::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* Remove arrows in number input for Firefox and other browsers */ input[type='number'] { - -moz-appearance: textfield; appearance: textfield; } -/* Hide spin buttons in number input for Firefox */ -input[type='number']::-moz-inner-spin-button { - display: none; -} - .bottom .goal input:focus { outline: none; box-shadow: none; @@ -245,12 +289,11 @@ input[type='number']::-moz-inner-spin-button { .goal .goalBtn { position: absolute; - right: -1.5rem; - top: 0; - bottom: 0; - margin-bottom: auto; - margin-top: auto; + right: -1.35rem; + top: 50%; + transform: translateY(-50%); cursor: pointer; + font-size: 1.05rem; } .timerStatus { @@ -261,14 +304,17 @@ input[type='number']::-moz-inner-spin-button { width: 85%; white-space: normal; transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.7); + background: rgb(0 0 0 / 70%); text-align: center; font-weight: bold; - font-size: 1.5rem; + font-size: 1.15rem; + padding: 0.8rem; + border-radius: 0.6rem; } .transitionColor { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, + opacity, transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 300ms; -} +} \ No newline at end of file diff --git a/src/components/Timer/Timer.module.css b/src/components/Timer/Timer.module.css index 517823a575..c714fc3fbd 100644 --- a/src/components/Timer/Timer.module.css +++ b/src/components/Timer/Timer.module.css @@ -34,8 +34,10 @@ background-color: rgb(82 92 102 / 100%); color: white; border-radius: 0 0 0.5rem 0.5rem; - border-color: rgb(82 92 102 / 100%); + border: none; padding: 0.25rem; + box-shadow: none; + appearance: none; } .btns { @@ -84,6 +86,13 @@ color: #ed688a; } +.preview:focus, +.preview:active, +.preview:focus-visible { + outline: none; + box-shadow: none; +} + .btnDisabled { cursor: not-allowed; color: #837175; diff --git a/src/components/Timer/__test__/Countdown.test.jsx b/src/components/Timer/__test__/Countdown.test.jsx index c06ff42ac9..b9ba7b6c60 100644 --- a/src/components/Timer/__test__/Countdown.test.jsx +++ b/src/components/Timer/__test__/Countdown.test.jsx @@ -36,8 +36,11 @@ describe('Countdown Component', () => { expect(screen.getByText('Goal: 01:00:00')).toBeInTheDocument(); expect(screen.getByText('Elapsed: 00:30:00')).toBeInTheDocument(); expect(screen.getByText('Time Remaining')).toBeInTheDocument(); - expect(screen.getByText('00')).toBeInTheDocument(); + expect(screen.getAllByText('00')).toHaveLength(2); expect(screen.getByText('30')).toBeInTheDocument(); + expect(screen.getByText('Hours')).toBeInTheDocument(); + expect(screen.getByText('minutes')).toBeInTheDocument(); + expect(screen.getByText('seconds')).toBeInTheDocument(); }); it('calls toggleTimer when the close button is clicked', () => { @@ -51,9 +54,8 @@ describe('Countdown Component', () => { it('displays correct remaining time based on props', () => { // eslint-disable-next-line react/jsx-props-no-spreading const { rerender } = render(); - expect(screen.getByText('00')).toBeInTheDocument(); // Hours + expect(screen.getAllByText('00')).toHaveLength(2); // Hours and Seconds expect(screen.getByText('30')).toBeInTheDocument(); // Minutes - expect(screen.getByText('00')).toBeInTheDocument(); // Seconds rerender( {children} diff --git a/src/components/TotalOrgSummary/WeeklySummaryEmail/TotalOrgSummaryEmail.jsx b/src/components/TotalOrgSummary/WeeklySummaryEmail/TotalOrgSummaryEmail.jsx new file mode 100644 index 0000000000..dd9a94be54 --- /dev/null +++ b/src/components/TotalOrgSummary/WeeklySummaryEmail/TotalOrgSummaryEmail.jsx @@ -0,0 +1,172 @@ +import { useState, useEffect } from 'react'; +import axios from 'axios'; +import { useSelector } from 'react-redux'; + +import { v4 as uuid } from 'uuid'; +import { ToastContainer, toast } from 'react-toastify'; +import { ENDPOINTS } from '../../../utils/URL'; + +function TotalOrgSummaryEmail() { + // State for managing email recipients + const [recipients, setRecipients] = useState(''); + const [recipientList, setRecipientList] = useState([]); + const [emailSubject, setSubject] = useState(''); + const [body, setBody] = useState(''); + + // Fetch admin list from the backend + useEffect(() => { + const fetchAdminList = async () => { + try { + // console.log('Endpoint:', ENDPOINTS.ADMIN_LIST()); // Log the endpoint for debugging + const response = await axios.get(ENDPOINTS.ADMIN_LIST()); + + // Extract the emailList from the response + setRecipientList(response.data.emailList); // Use response.data.emailList + } catch (error) { + // console.error('Error fetching admin list:', error); + } + }; + fetchAdminList(); + }, []); + + // Handle adding new recipients + const handleAddRecipient = () => { + if (recipients.trim() !== '') { + setRecipientList([...recipientList, recipients.trim()]); + setRecipients(''); + } + }; + + const handlRemoveAllRecipients = () => { + setRecipientList([]); + }; + + // Handle removing a recipient + const handleRemoveRecipient = email => { + const updatedList = recipientList.filter(recipient => recipient !== email); + setRecipientList(updatedList); + }; + function notify(type) { + switch (type) { + case 'info': + toast.info('Sending email...'); + break; + case 'error': + toast.error('Failed to send email.'); + break; + case 'success': + toast.success('Email sent successfully!'); + break; + default: + break; + } + } + // Handle sending the email + const handleSendEmail = async () => { + try { + notify('info'); + + const emailData = { + recipients: recipientList, + subject: emailSubject, + message: body, + }; + + const response = await axios.post(ENDPOINTS.SEND_EMAIL_REPORT(), emailData); + + if (response.status === 200) { + notify('success'); + } else { + notify('error'); + } + } catch (error) { + notify('error'); + } + }; + + // Darkmode + const darkMode = useSelector(state => state.theme.darkMode); + useEffect(() => { + const rootDiv = document.getElementById('root'); + if (darkMode) { + rootDiv.classList.add('bg-dark', 'text-light'); + } else { + rootDiv.classList.remove('bg-dark', 'text-light'); + } + }, [darkMode]); + + return ( +
+

Total Org Summary Email

+ {/* Recipient Management */} +
+

Recipients

+
+ {recipientList.map((email, index) => ( +
+ {email} + + + +
+ ))} +
+
+ + setRecipients(e.target.value)} + /> + + + + + +
+ +
+ setSubject(e.target.value)} + /> +
+
+