diff --git a/src/actions/badgeManagement.js b/src/actions/badgeManagement.js index a11e3889b9..292d4c4431 100644 --- a/src/actions/badgeManagement.js +++ b/src/actions/badgeManagement.js @@ -47,38 +47,6 @@ export const fetchAllBadges = (forceRefresh = false) => { } }; }; - -// Return updated badgeCollection -export const returnUpdatedBadgesCollection = (badgeCollection, selectedBadgesId) => { - const personalMaxBadge = '666b78265bca0bcb94080605'; // backend id for Personal Max badge - const badgeMap = new Map(badgeCollection?.map(badge => [badge.badge, badge])); - - const currentTs = Date.now(); - const currentDate = formatDate(); - selectedBadgesId.forEach(originalBadgeId => { - const badgeId = originalBadgeId.replace('assign-badge-', ''); - if (badgeMap.has(badgeId)) { - // Update the existing badge record - if (badgeId !== personalMaxBadge) { - const badge = badgeMap.get(badgeId); - badge.count = (badge.count || 0) + 1; - badge.lastModified = currentTs; - badge.earnedDate.push(currentDate); - } - } else { - // Add the new badge record - badgeMap.set(badgeId, { - badge: badgeId, - count: 1, - lastModified: currentTs, - earnedDate: [currentDate], - }); - } - }); - - return Array.from(badgeMap.values()); -}; - export const gotCloseAlert = () => ({ type: CLOSE_ALERT }); const getBadgeCountSuccess = badgeCount => ({ @@ -167,7 +135,7 @@ export const validateBadges = (firstName, lastName) => { if (!firstName || !lastName) { dispatch( getMessage( - 'The Name Find function does not work without entering a name. Nice try though.', + 'The Name Find function does not work without entering first and last name. Nice try though.', 'danger', ), ); @@ -180,7 +148,105 @@ export const validateBadges = (firstName, lastName) => { }; }; -export const assignBadges = (firstName, lastName, selectedBadges) => { + + +export const returnUpdatedBadgesCollection = (badgeCollection, selectedBadgesId) => { + let newBadgeCollection = Array.from(badgeCollection); + + // Object to track updated or newly added badges to prevent duplicates + const updatedOrAddedBadges = {}; + + selectedBadgesId.forEach(originalBadgeId => { + let badgeId = originalBadgeId; + + // Remove "assign-badge-" from badgeId + if (badgeId.includes('assign-badge-')) badgeId = badgeId.replace('assign-badge-', ''); + + if (!updatedOrAddedBadges[badgeId]) { + // Flag to check if the badge is already in the collection + let included = false; + const currentTs = Date.now(); + const currentDate = formatDate(); + + for (let i = 0; i < newBadgeCollection.length; i+=1) { + const badgeObj = newBadgeCollection[i]; + if (badgeId === badgeObj.badge) { + // If the badge is found, increment the count and mark it as included + badgeObj.count = badgeObj.count ? badgeObj.count + 1 : 1; + badgeObj.lastModified = currentTs; + badgeObj.earnedDate.push(currentDate); + included = true; + // Mark this badge ID as updated so it's not added again + updatedOrAddedBadges[badgeId] = true; + break; // Exit loop after finding and updating the badge + } + } + + // If the badge was not already in the collection, add it as a new entry + if (!included) { + newBadgeCollection.push({ + badge: badgeId, + count: 1, + lastModified: currentTs, + earnedDate: [currentDate], + }); + // Mark this badge ID as added + updatedOrAddedBadges[badgeId] = true; + } + } + }); + + return newBadgeCollection; +}; + +export const returnUpdatedBadgesCollectionSingleUser = (badgeCollection, selectedBadgesId) => { + let newBadgeCollection = Array.from(badgeCollection); + + const updatedOrAddedBadges = {}; + + selectedBadgesId.forEach(originalBadgeId => { + let badgeId = originalBadgeId; + if (badgeId.includes('assign-badge-')) badgeId = badgeId.replace('assign-badge-', ''); + + if (!updatedOrAddedBadges[badgeId]) { + let included = false; + const currentTs = Date.now(); + const currentDate = formatDate(); + + newBadgeCollection = newBadgeCollection.map(badgeObj => { + if (badgeId === badgeObj.badge) { + if (!included) { + included = true; + updatedOrAddedBadges[badgeId] = true; + return { + ...badgeObj, + count: badgeObj.count ? badgeObj.count + 1 : 1, + lastModified: currentTs, + earnedDate: [...badgeObj.earnedDate, currentDate] + }; + } + updatedOrAddedBadges[badgeId] = true; + } + return badgeObj; + }); + + if (!included) { + newBadgeCollection.push({ + badge: badgeId, + count: 1, + lastModified: currentTs, + earnedDate: [currentDate], + }); + updatedOrAddedBadges[badgeId] = true; + } + } + }); + + return newBadgeCollection; +}; + + +export const assignBadgesByUserID = (userId, selectedBadges) => { return async dispatch => { if (selectedBadges.length === 0) { dispatch( @@ -189,17 +255,13 @@ export const assignBadges = (firstName, lastName, selectedBadges) => { 'danger', ), ); - if (ALERT_DELAY === 0) { + setTimeout(() => { dispatch(closeAlert()); - } else { - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY); - } + }, 6000); return; } - const userAssigned = `${firstName} ${lastName}`; - - const res = await axios.get(ENDPOINTS.USER_PROFILE_BY_NAME(userAssigned)); + const res = await axios.get(ENDPOINTS.USER_PROFILE(userId)); if (res.data.length === 0) { dispatch( @@ -208,18 +270,22 @@ export const assignBadges = (firstName, lastName, selectedBadges) => { 'danger', ), ); - if (ALERT_DELAY === 0) { + setTimeout(() => { dispatch(closeAlert()); - } else { - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY); - } + }, 6000); return; } + const { badgeCollection } = res.data; + for (let i = 0; i < badgeCollection.length; i+=1) { + badgeCollection[i].badge = badgeCollection[i].badge._id; + } - const { badgeCollection } = res.data[0]; - const userToBeAssignedBadge = res.data[0]._id; - const newBadgeCollection = returnUpdatedBadgesCollection(badgeCollection, selectedBadges); - + const userToBeAssignedBadge = res.data._id; + const newBadgeCollection = returnUpdatedBadgesCollectionSingleUser( + badgeCollection, + selectedBadges, + ); + // send updated badgeCollection to backend const url = ENDPOINTS.BADGE_ASSIGN(userToBeAssignedBadge); try { await axios.put(url, { @@ -232,23 +298,19 @@ export const assignBadges = (firstName, lastName, selectedBadges) => { 'success', ), ); - if (ALERT_DELAY === 0) { + setTimeout(() => { dispatch(closeAlert()); - } else { - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY); - } + }, 6000); } catch (e) { dispatch(getMessage('Oops, something is wrong!', 'danger')); - if (ALERT_DELAY === 0) { + setTimeout(() => { dispatch(closeAlert()); - } else { - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY); - } + }, 6000); } }; }; -export const assignBadgesByUserID = (userId, selectedBadges) => { +export const assignBadgesToMultipleUserID = (userIds, selectedBadges) => { return async dispatch => { if (selectedBadges.length === 0) { dispatch( @@ -257,45 +319,80 @@ export const assignBadgesByUserID = (userId, selectedBadges) => { 'danger', ), ); - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY || 0); + setTimeout(() => { + dispatch(closeAlert()); + }, 6000); return; } try { - const res = await axios.get(ENDPOINTS.USER_PROFILE(userId)); - const userData = Array.isArray(res.data) ? res.data[0] : res.data; + const response = await axios.post(ENDPOINTS.BADGE_ASSIGN_MULTIPLE, { + userIds, + selectedBadges, + }); - if (!userData || !userData._id) { - dispatch(getMessage('User data is incomplete. Cannot assign badges.', 'danger')); - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY || 0); - return; + if (response.status === 200) { + dispatch( + getMessage( + "Awesomesauce! You've increased badges and proportionally increased life happiness for multiple users!", + 'success', + ), + ); + } else { + throw new Error('Failed to assign badges'); } + } catch (error) { + dispatch(getMessage('Oops, something went wrong while assigning badges!', 'danger')); + } finally { + setTimeout(() => { + dispatch(closeAlert()); + }, 6000); + } + }; +}; - const badgeCollectionToSend = selectedBadges.map(badgeId => ({ - badge: badgeId, - count: 1, - lastModified: Date.now(), - featured: false, - earnedDate: [] - })); - - await axios.put(ENDPOINTS.BADGE_ASSIGN(userData._id), { - badgeCollection: badgeCollectionToSend, - newBadges: selectedBadges.length - }); - +export const assignBadges = (firstName, lastName, selectedBadges) => { + return async dispatch => { + if (selectedBadges.length === 0) { dispatch( getMessage( - "Awesomesauce! Not only have you increased a person's badges, you've also proportionally increased their life happiness!", - 'success', + "Um no, that didn't work. Badge Select Function must include actual selection of badges to work. Better luck next time!", + 'danger', ), ); + setTimeout(() => { + dispatch(closeAlert()); + }, 6000); + return; + } - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY || 0); - } catch (e) { - toast.error('Badge assignment error:', e); - dispatch(getMessage('Oops, something is wrong!', 'danger')); - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY || 0); + const userAssigned = `${firstName} ${lastName}`; + + try { + const res = await axios.get(ENDPOINTS.USER_PROFILE_BY_NAME(userAssigned)); + if (res.data.length === 0) { + dispatch( + getMessage( + "Can't find that user. Step 1 to getting badges: Be in the system. Not in the system? No badges for you!", + 'danger', + ), + ); + setTimeout(() => { + dispatch(closeAlert()); + }, 6000); + } + const userToBeAssignedBadge = res.data[0]._id; + await dispatch(assignBadgesByUserID([userToBeAssignedBadge], selectedBadges)); + } catch (error) { + dispatch( + getMessage( + 'Oops, something is wrong!', + 'danger', + ), + ); + setTimeout(() => { + dispatch(closeAlert()); + }, 6000); } }; }; @@ -306,30 +403,24 @@ export const sendUpdatedBadgeCollectionReq = async ( selectedBadges, userToBeAssignedBadge, ) => { - return async dispatch => { - const url = ENDPOINTS.BADGE_ASSIGN(userToBeAssignedBadge); - try { - await axios.put(url, { badgeCollection, newBadges: selectedBadges.length }); - dispatch( - getMessage( - "Awesomesauce! Not only have you increased a person's badges, you've also proportionally increased their life happiness!", - 'success', - ), - ); - if (ALERT_DELAY === 0) { - dispatch(closeAlert()); - } else { - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY); - } - } catch (e) { - dispatch(getMessage('Oops, something is wrong!', 'danger')); - if (ALERT_DELAY === 0) { - dispatch(closeAlert()); - } else { - setTimeout(() => dispatch(closeAlert()), ALERT_DELAY); - } - } - }; + const url = ENDPOINTS.BADGE_ASSIGN(userToBeAssignedBadge); + try { + await axios.put(url, { badgeCollection, newBadges: selectedBadges.length }); + dispatch( + getMessage( + "Awesomesauce! Not only have you increased a person's badges, you've also proportionally increased their life happiness!", + 'success', + ), + ); + setTimeout(() => { + dispatch(closeAlert()); + }, 6000); + } catch (e) { + dispatch(getMessage('Oops, something is wrong!', 'danger')); + setTimeout(() => { + dispatch(closeAlert()); + }, 6000); + } }; export const changeBadgesByUserID = (userId, badgeCollection) => { diff --git a/src/actions/bmdashboard/equipmentActions.js b/src/actions/bmdashboard/equipmentActions.js index 331868fd56..7e604125ad 100644 --- a/src/actions/bmdashboard/equipmentActions.js +++ b/src/actions/bmdashboard/equipmentActions.js @@ -1,12 +1,12 @@ import axios from 'axios'; import { toast } from 'react-toastify'; -import { SET_EQUIPMENTS } from '../../constants/bmdashboard/equipmentConstants'; +import { GET_EQUIPMENT_BY_ID, SET_EQUIPMENTS } from '../../constants/bmdashboard/equipmentConstants'; import { GET_ERRORS } from '../../constants/errors'; import { ENDPOINTS } from '~/utils/URL'; export const setEquipment = payload => { return { - type: SET_EQUIPMENTS, + type: GET_EQUIPMENT_BY_ID, payload, }; }; diff --git a/src/components/BMDashboard/Equipment/Detail/EquipmentDetail.jsx b/src/components/BMDashboard/Equipment/Detail/EquipmentDetail.jsx index 337500fc64..7d0c3da1dd 100644 --- a/src/components/BMDashboard/Equipment/Detail/EquipmentDetail.jsx +++ b/src/components/BMDashboard/Equipment/Detail/EquipmentDetail.jsx @@ -63,8 +63,11 @@ function EquipmentDetail() { dispatch(fetchEquipmentById(equipmentId)); }, [dispatch, equipmentId]); - const lastLogRecord = equipment?.logRecord?.[equipment.logRecord.length - 1]; - let currentUsage = 'Unknown'; + const lastLogRecord = + equipment?.logRecord && equipment.logRecord.length > 0 + ? equipment.logRecord[equipment.logRecord.length - 1] + : null; + let currentUsage = null; if (lastLogRecord?.type === 'Check In') { currentUsage = 'Checked In'; @@ -72,42 +75,132 @@ function EquipmentDetail() { currentUsage = 'Checked Out'; } + const formatValue = value => { + if (value === 0) return 0; + if (value === '' || value === null || value === undefined) return 'Not Available'; + return value; + }; + + const formatPersonName = person => { + if (!person) return null; + const fullName = `${person.firstName || ''} ${person.lastName || ''}`.trim(); + return fullName || null; + }; + function formatDateString(dateString) { + if (!dateString) return 'Not Available'; const date = new Date(dateString); - return date.toLocaleDateString(); + return Number.isNaN(date.getTime()) ? 'Not Available' : date.toLocaleDateString(); } const formattedRentedOnDate = formatDateString(equipment?.rentedOnDate); const formattedRentedDueDate = formatDateString(equipment?.rentalDueDate); + const latestUpdateRecord = + equipment?.updateRecord && equipment.updateRecord.length > 0 + ? equipment.updateRecord[equipment.updateRecord.length - 1] + : null; + const latestPurchaseRecord = + equipment?.purchaseRecord && equipment.purchaseRecord.length > 0 + ? equipment.purchaseRecord[equipment.purchaseRecord.length - 1] + : null; + + const formattedLastUpdateDate = formatDateString(latestUpdateRecord?.date); + + const formatCurrency = amount => { + if (amount === 0 || amount) { + const numericAmount = Number(amount); + if (Number.isNaN(numericAmount)) return amount; + return `$${numericAmount.toLocaleString()}`; + } + return null; + }; + + const invoiceNumber = + latestPurchaseRecord?.invoiceId || + latestPurchaseRecord?.invoiceNo || + latestPurchaseRecord?.invoice; + const purchaseLink = + latestPurchaseRecord?.purchaseLink || + equipment?.purchaseLink || + equipment?.itemType?.purchaseLink; + const price = formatCurrency(latestPurchaseRecord?.price ?? equipment?.price ?? equipment?.cost); + const condition = equipment?.condition || latestUpdateRecord?.condition; + const shippingFee = formatCurrency( + latestPurchaseRecord?.shippingFee ?? equipment?.shippingFee ?? equipment?.shippingCost, + ); + const taxes = formatCurrency( + latestPurchaseRecord?.tax ?? latestPurchaseRecord?.taxes ?? equipment?.taxes, + ); + const supplierPhoneNumber = + equipment?.supplierPhoneNumber || + latestPurchaseRecord?.supplierPhoneNumber || + latestPurchaseRecord?.supplier?.phoneNumber; + const description = equipment?.itemType?.description || equipment?.description; + const currentStatus = latestUpdateRecord?.condition; + const lastUsedPerson = formatPersonName(latestUpdateRecord?.createdBy); + const lastUsedTask = latestUpdateRecord?.task || latestUpdateRecord?.usedFor; + const replacementRequested = + typeof latestUpdateRecord?.replacementRequired === 'boolean' + ? latestUpdateRecord.replacementRequired + ? 'Yes' + : 'No' + : latestUpdateRecord?.replacementRequired; const details = [ - { label: 'Belongs to Project', value: equipment?.project?.name }, - { label: 'Class', value: equipment?.itemType?.category }, - { label: 'Name', value: equipment?.itemType?.name }, - { label: 'Number', value: equipment?.code }, - { label: 'Ownership', value: equipment?.purchaseStatus }, - { label: 'Add Date', value: 'MM - DD - YYYY' }, + { label: 'Belongs to Project', value: formatValue(equipment?.project?.name) }, + { label: 'Class', value: formatValue(equipment?.itemType?.category) }, + { label: 'Name', value: formatValue(equipment?.itemType?.name) }, + { label: 'Number', value: formatValue(equipment?.code) }, + { label: 'Ownership', value: formatValue(equipment?.purchaseStatus) }, + { label: 'Add Date', value: formatDateString(equipment?.createdAt) }, // Remove 'Rental Duration' from details if 'Ownership' is 'Purchase' - equipment?.purchaseStatus === 'Purchase' ? null : { label: 'Rental Duration' }, - { label: 'Current Usage', value: currentUsage }, + equipment?.purchaseStatus === 'Purchased' ? null : { label: 'Rental Duration' }, + { label: 'Current Usage', value: formatValue(currentUsage) }, { label: 'Dashed Line' }, - { label: 'Input Invoice No or ID', value: 'No123ABC' }, - { label: 'Price', value: '$150' }, - { label: 'Add Condition', value: 'New' }, - { label: 'Shipping Fee', value: '$25' }, - { label: 'Taxes', value: '$15' }, - { label: 'Supplier Phone Number', value: '555-33-3333' }, + { + label: 'Input Invoice No or ID', + value: formatValue(invoiceNumber), + }, + { + label: 'Price', + value: formatValue(price), + }, + { + label: 'Add Condition', + value: formatValue(condition), + }, + { + label: 'Shipping Fee', + value: formatValue(shippingFee), + }, + { + label: 'Taxes', + value: formatValue(taxes), + }, + { + label: 'Supplier Phone Number', + value: formatValue(supplierPhoneNumber), + }, { label: 'Link To Buy/Rent', - value: 'https://www.homedepot.com/', + value: formatValue(purchaseLink), + }, + { + label: 'Description', + value: formatValue(description), }, - { label: 'Description', value: 'Testing Description' }, { label: 'Dashed Line' }, - { label: 'Current Status', value: 'Tested' }, - { label: 'Last Update Date', value: '03-01-2024' }, - { label: 'Last Used Person', value: 'Jae' }, - { label: 'Last Used Task', value: 'Garden clean up' }, - { label: 'Asked for a replacement?', value: 'No' }, + { label: 'Current Status', value: formatValue(currentStatus) }, + { label: 'Last Update Date', value: formattedLastUpdateDate }, + { + label: 'Last Used Person', + value: formatValue(lastUsedPerson), + }, + { label: 'Last Used Task', value: formatValue(lastUsedTask) }, + { + label: 'Asked for a replacement?', + value: formatValue(replacementRequested), + }, ]; const generateKey = () => uuidv4(); @@ -116,9 +209,12 @@ function EquipmentDetail() { ); - const renderLinkItem = detail => ( - - ); + const renderLinkItem = detail => { + if (detail.value === 'Not Available') { + return renderDetailItem(detail); + } + return ; + }; const renderRentalDurationItem = detail => ( ); - const renderDescriptionItem = detail => ( - - ); + const renderDescriptionItem = detail => { + if (detail.value === 'Not Available') { + return renderDetailItem(detail); + } + return ( + + ); + }; const renderDashedLineItem = detail => ( @@ -165,7 +266,11 @@ function EquipmentDetail() {

- + {equipment?.itemType?.name

{details.filter(Boolean).map(renderDetails)} - - - {errMsg && ( - {errMsg} - )} - - - - {/* MAP — full width, flexible height */} -
- {/* Bottom-left label inside map */} -
- Showing {filteredOrgs.length} of {orgs.length} projects +
+
+ {/* TITLE ROW */} +
+

Global Distribution and Project Status Overview

- {loading ? ( -
Loading…
- ) : ( - - - - +
+ setStartDate(date)} + className={`${styles.dateInput} ${darkMode ? styles.dateInputDark : ''}`} + placeholderText="Start Date" + calendarClassName={darkMode ? styles.calendarDark : styles.calendarLight} + /> + setEndDate(date)} + className={`${styles.dateInput} ${darkMode ? styles.dateInputDark : ''}`} + placeholderText="End Date" + calendarClassName={darkMode ? styles.calendarDark : styles.calendarLight} /> + + {errMsg && {errMsg}} +
+
Total Projects: {orgs.length}
+
- - - - {pseudoOrgs.map((org, index) => ( - - View Project #{org.orgId} - - -

Project #{org.orgId}

-

Name: {org.name}

-

Status: {org.status}

-

Country: {org.country}

-

Start: {new Date(org.startDate).toLocaleDateString()}

- - -
-
- ))} -
- - )} + {/* MAP */} +
+ + + {loading ? ( +
Loading…
+ ) : ( + + + + + {filteredOrgs.map(org => ( + + + View Project #{org.orgId} + + +

Project #{org.orgId}

+

Name: {org.name}

+

Status: {org.status}

+

Start: {new Date(org.startDate).toLocaleDateString()}

+ +
+
+ ))} +
+
+ )} +
); diff --git a/src/components/BMDashboard/InteractiveMap/InteractiveMap.module.css b/src/components/BMDashboard/InteractiveMap/InteractiveMap.module.css index e50d857cbe..bd29f18833 100644 --- a/src/components/BMDashboard/InteractiveMap/InteractiveMap.module.css +++ b/src/components/BMDashboard/InteractiveMap/InteractiveMap.module.css @@ -1,119 +1,348 @@ -:global(.dark-mode-map) { - filter: brightness(0.8); +/* ---------------------------------- + PAGE BACKGROUND +---------------------------------- */ +.lightPage { + background: #ffffff; + width: 100%; + height: 100%; + color: #222; + --legend-bg: rgba(255, 255, 255, 0.9); } -:global(.leaflet-popup-content-wrapper), -:global(.leaflet-popup-content) { - background-color: white; - color: #222; - border-radius: 4px; +.darkPage { + background: #0d1b2a; + width: 100%; + height: 100%; + color: #e6e6e6; + --legend-bg: rgba(0, 0, 0, 0.55); } -:global(.dark-mode-popup .leaflet-popup-content-wrapper), -:global(.dark-mode-popup .leaflet-popup-content) { - background-color: #2d4059 !important; - color: white !important; +/* ---------------------------------- + MAIN CONTAINER +---------------------------------- */ +.container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + position: relative; + padding: 12px 18px; + z-index: 0; } -:global(.dark-mode-popup .leaflet-popup-tip) { - background-color: #2d4059; +/* ---------------------------------- + HEADER +---------------------------------- */ +.headerRow { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + padding: 8px 4px 16px; + position: relative; + z-index: 1200; } -:global(.leaflet-tooltip) { - background-color: white; - color: #222; - border: 1px solid #ddd; +.titleText { + font-size: 22px; + font-weight: 600; + margin: 0; + color: inherit; } -:global(.dark-mode-tooltip .leaflet-tooltip) { - background-color: #2d4059; - color: white !important; - border: 1px solid #555; +/* ---------------------------------- + ENABLE CONTROLS TO RENDER CORRECTLY +---------------------------------- */ + +/* Before: pointer-events:none blocked legend + hid it entirely */ +:global(.leaflet-control-container) { + pointer-events: auto !important; + z-index: 450 !important; /* prevent covering header */ } -:global(.dark-mode-tooltip .leaflet-tooltip-top:before) { - border-top-color: #2d4059; +/* Leaflet bottom-right container */ +:global(.leaflet-bottom.leaflet-right) { + bottom: 20px !important; + right: 20px !important; + z-index: 600 !important; + pointer-events: auto !important; /* container does not capture clicks */ } -:global(.dark-mode-tooltip .leaflet-tooltip-bottom:before) { - border-bottom-color: #2d4059; +/* ---------------------------------- + FILTER ROW +---------------------------------- */ +.filterRow { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + z-index: 2000; } -:global(.dark-mode-tooltip .leaflet-tooltip-left:before) { - border-left-color: #2d4059; +/* ---------------------------------- + DATE INPUT FIELDS +---------------------------------- */ +.dateInput { + padding: 6px 10px; + border-radius: 8px; + border: 1px solid #ccc; + font-size: 14px; + width: 150px; + transition: 0.15s ease; } -:global(.dark-mode-tooltip .leaflet-tooltip-right:before) { - border-right-color: #2d4059; +.dateInputDark { + background: #1a2738; + color: #ffffff; + border-color: #465a74; } -:global(.dark-mode-map .leaflet-control-attribution) { - background-color: rgba(45, 64, 89, 0.8); - color: #ddd; +.dateInputDark:hover { + border-color: #6acfde; } -:global(.dark-mode-map .leaflet-control-attribution a) { - color: #6ACFDE; +.dateInputDark:focus { + outline: none; + border-color: #6acfde; + box-shadow: 0 0 4px rgba(106, 207, 222, 0.6); } -:global(.dark-mode-map .leaflet-control-zoom a) { - background-color: #2d4059; - color: white; - border-color: #3a506b; +/* Light mode */ +.lightPage .dateInput { + background: #ffffff; + color: #222; + border-color: #ccc; } -:global(.dark-mode-map .leaflet-control-zoom a:hover) { - background-color: #3a506b; +.lightPage .dateInput:hover { + border-color: #6acfde; } -:global(.dark-mode-popup .leaflet-popup-content-wrapper), -:global(.dark-mode-popup .leaflet-popup-content), -:global(.dark-mode-popup .leaflet-popup-content *) { - color: white !important; +.lightPage .dateInput:focus { + outline: none; + border-color: #3a8bbd; + box-shadow: 0 0 3px rgba(58, 139, 189, 0.6); } -:global(.leaflet-popup-content-wrapper), -:global(.leaflet-popup-content), -:global(.leaflet-popup-content *) { - color: #222; +/* Error message */ +.errorMsg { + color: #de6a6a; + font-size: 13px; + font-weight: 500; +} + +/* ---------------------------------- + MAP AREA +---------------------------------- */ +.mapArea { + position: relative; + width: 100%; + height: 90vh; + border-radius: 12px; + overflow: visible !important; + margin-top: 10px; } -:global(.dark-mode-popup .leaflet-popup-content-wrapper) { - background-color: #2d4059 !important; +.mapContainer { + width: 100%; + height: 100%; + border-radius: 12px; } +/* ---------------------------------- + DARK MAP OVERLAY EFFECT +---------------------------------- */ +.darkMap { + filter: brightness(0.78) contrast(1.1); +} + +/* ---------------------------------- + POPUPS +---------------------------------- */ :global(.leaflet-popup-content-wrapper) { - background-color: white !important; + border-radius: 10px !important; + padding: 10px 12px !important; } -/* Marker cluster styling */ -:global(.marker-cluster-small) { - background-color: rgba(181, 226, 140, 0.6); +:global(.leaflet-popup-content) { + margin: 0 !important; + font-size: 14px !important; } -:global(.marker-cluster-small div) { - background-color: rgba(110, 204, 57, 0.6); + +/* Light */ +.lightPage :global(.leaflet-popup-content-wrapper) { + background: #ffffff !important; + color: #222 !important; + border: 1px solid #e3e3e3 !important; +} + +/* Dark */ +.darkPage :global(.leaflet-popup-content-wrapper), +.darkPage :global(.leaflet-popup-tip) { + background: #2d4059 !important; + border: 1px solid #4b5f79 !important; + color: #f5f5f5 !important; +} + +/* ---------------------------------- + TOOLTIP +---------------------------------- */ +.lightTooltip, +.darkTooltip { + padding: 6px 10px; + font-size: 13px; + border-radius: 6px; +} + +.lightTooltip { + background: #ffffff !important; + border: 1px solid #e3e3e3 !important; + color: #222 !important; +} + +.darkTooltip { + background: #2d4059 !important; + border: 1px solid #4b5f79 !important; + color: #ffffff !important; } -:global(.marker-cluster-medium) { - background-color: rgba(241, 211, 87, 0.6); +/* ---------------------------------- + MAP CONTROLS +---------------------------------- */ +.darkPage :global(.leaflet-control-attribution) { + background: rgba(45, 64, 89, 0.85) !important; + color: #ddd !important; } -:global(.marker-cluster-medium div) { - background-color: rgba(240, 194, 12, 0.6); + +.darkPage :global(.leaflet-control-attribution a) { + color: #6acfde !important; } -:global(.marker-cluster-large) { - background-color: rgba(253, 156, 115, 0.6); +/* Hide zoom buttons */ +:global(.leaflet-control-zoom) { + display: none !important; } -:global(.marker-cluster-large div) { - background-color: rgba(241, 128, 23, 0.6); + +:global(.leaflet-control) { + z-index: 2000 !important; } -/* Marker cluster hover effects */ +/* ---------------------------------- + CLUSTERS +---------------------------------- */ :global(.marker-cluster) { cursor: pointer; - transition: all 0.2s ease; + transition: transform 0.2s ease; } :global(.marker-cluster:hover) { - transform: scale(1.1); + transform: scale(1.12); } + +/* ---------------------------------- + LABELS & LEGEND +---------------------------------- */ +.bottomLeftLabel { + position: absolute; + left: 12px; + bottom: 12px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.75); + border-radius: 8px; + font-size: 13px; + backdrop-filter: blur(4px); + z-index: 1000; +} + +.darkPage .bottomLeftLabel { + background: rgba(0, 0, 0, 0.55); + color: #ffffff; +} + + +.legendItem { + display: flex; + align-items: center; + margin: 4px 0; +} + +.legendDot { + width: 13px; + height: 13px; + border-radius: 50%; + margin-right: 8px; +} + +/* ---------------------------------- + POPUP BUTTON +---------------------------------- */ +.detailsButton { + margin-top: 8px; + padding: 7px 12px; + background: #1f6feb; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + transition: 0.15s ease; +} + +.detailsButton:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.legendBox { + position: absolute; /* absolute inside the map container */ + width: 160px; /* fixed width */ + height: 180px; /* fixed height */ + cursor: move; + user-select: none; + padding: 10px 12px; + background: var(--legend-bg); + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + border: 1px solid rgba(0,0,0,0.15); + backdrop-filter: blur(4px); + font-size: 13px; + z-index: 10000; /* higher than map */ + pointer-events: auto !important; + opacity: 1 !important; + left: auto; /* initial position handled in JSX */ + top: auto; /* initial position handled in JSX */ + bottom: 20px; /* fallback bottom-right */ + right: 20px; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + + +.darkPage .legendBox { + border: 1px solid rgba(255,255,255,0.15); + color: #fff; +} + +/* New filter row styles */ +.filterRowExtra { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + margin-top: 8px; + margin-bottom: 16px; +} + +.filterInputs { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.totalProjects { + font-weight: 500; + margin-left: auto; +} \ No newline at end of file diff --git a/src/components/BMDashboard/ItemList/ItemListView.jsx b/src/components/BMDashboard/ItemList/ItemListView.jsx index 733ef3d61e..cf6451b90d 100644 --- a/src/components/BMDashboard/ItemList/ItemListView.jsx +++ b/src/components/BMDashboard/ItemList/ItemListView.jsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; @@ -14,6 +15,7 @@ export function ItemListView({ itemType, items, errors, UpdateItemModal, dynamic const [selectedItem, setSelectedItem] = useState('all'); const [isError, setIsError] = useState(false); const [selectedTime, setSelectedTime] = useState(new Date()); + const darkMode = useSelector(state => state.theme.darkMode); useEffect(() => { if (items) setFilteredItems([...items]); @@ -44,7 +46,7 @@ export function ItemListView({ itemType, items, errors, UpdateItemModal, dynamic if (isError) { return ( -
+

{itemType} {' List'} @@ -55,7 +57,7 @@ export function ItemListView({ itemType, items, errors, UpdateItemModal, dynamic } return ( -
+

{itemType}

@@ -71,6 +73,11 @@ export function ItemListView({ itemType, items, errors, UpdateItemModal, dynamic dateFormat="yyyy-MM-dd HH:mm:ss" placeholderText="Select date and time" inputId="itemListTime" // This is the key line + className={darkMode ? styles.darkDatePickerInput : styles.lightDatePickerInput} + calendarClassName={darkMode ? styles.darkDatePicker : styles.lightDatePicker} + popperClassName={ + darkMode ? styles.darkDatePickerPopper : styles.lightDatePickerPopper + } /> )}
diff --git a/src/components/BMDashboard/ItemList/ItemListView.module.css b/src/components/BMDashboard/ItemList/ItemListView.module.css index 4a829def15..21335b697f 100644 --- a/src/components/BMDashboard/ItemList/ItemListView.module.css +++ b/src/components/BMDashboard/ItemList/ItemListView.module.css @@ -91,3 +91,270 @@ .btnPrimary:hover { background-color: #3b82f6; } + +.darkMode { + color: #e8edf4; +} + +.darkMode h3 { + color: #ffffff; +} + +.darkMode .selectInput label { + color: #e8edf4; +} + +.darkMode .selectInput input, +.darkMode .selectInput select { + background-color: #2a3f5f; + color: #e8edf4; + border: 1px solid #3f5269; +} + +.darkMode .selectInput input::placeholder { + color: #c8d2e0; +} + +.darkMode .selectInput option { + background-color: #2a3f5f; + color: #e8edf4; +} + +.darkTableWrapper { + background-color: #1b2a41; + border: 1px solid #2f4157; + border-radius: 6px; +} + +.darkTable { + background-color: #1b2a41; + color: #e8edf4; +} + +.darkTable thead th { + background-color: #2f4157; + color: #f2f6ff !important; + border-color: #3f5269 !important; +} + +.darkTable tbody td, +.darkTable tbody th { + border-color: #2f4157 !important; +} + +.darkTable tbody tr:nth-child(odd) { + background-color: #1f2e45; +} + +.darkTable tbody tr:nth-child(even) { + background-color: #23375b; +} + +.darkDatePickerInput { + background-color: #2a3f5f !important; + color: #e8edf4 !important; + border: 1px solid #3f5269 !important; +} + +.darkDatePickerInput::placeholder { + color: #c8d2e0 !important; +} + +.darkDatePicker { + background-color: #1f2e45 !important; + color: #e8edf4 !important; + border: 1px solid #3f5269 !important; +} + +.darkDatePicker .react-datepicker__header { + background-color: #2f4157; + border-color: #3f5269; +} + +.darkDatePicker .react-datepicker__time-header { + color: #f2f6ff !important; + background-color: #2f4157 !important; + border-bottom: 1px solid #3f5269 !important; +} + +.darkDatePicker .react-datepicker__current-month, +.darkDatePicker .react-datepicker-year-header { + color: #f2f6ff !important; +} + +.darkDatePicker .react-datepicker__day-name, +.darkDatePicker .react-datepicker__day, +.darkDatePicker .react-datepicker__time-name { + color: #e8edf4; +} + +.darkDatePicker .react-datepicker__day--selected, +.darkDatePicker .react-datepicker__day--keyboard-selected, +.darkDatePicker .react-datepicker__time-list-item--selected { + background-color: #468ef9 !important; + color: #ffffff !important; +} + +.darkDatePicker .react-datepicker__time-container { + border-color: #3f5269 !important; + background-color: #2a3f5f !important; +} + +.darkDatePicker .react-datepicker__time-container .react-datepicker__time { + background-color: #2a3f5f !important; + border-color: #3f5269 !important; +} + +.darkDatePicker .react-datepicker__time-container .react-datepicker__time-box { + background-color: #2a3f5f !important; + border-color: #3f5269 !important; +} + +.darkDatePicker .react-datepicker__time-container .react-datepicker__time-list { + background-color: #2a3f5f !important; + color: #e8edf4 !important; + border-color: #3f5269 !important; +} + +.darkDatePicker + .react-datepicker__time-container + .react-datepicker__time-box + ul.react-datepicker__time-list + li { + color: #e8edf4 !important; + background-color: #2a3f5f !important; + border-color: #3f5269 !important; +} + +.darkDatePicker + .react-datepicker__time-container + .react-datepicker__time-box + ul.react-datepicker__time-list + li:hover { + background-color: #3a506b !important; + color: #ffffff !important; +} + +.darkDatePicker + .react-datepicker__time-container + .react-datepicker__time-box + ul.react-datepicker__time-list { + background-color: #2a3f5f !important; +} + +/* Explicitly target react-datepicker time list via :global to override inline white background */ +.darkDatePicker :global(.react-datepicker__time-container), +.darkDatePicker :global(.react-datepicker__time-container .react-datepicker__time), +.darkDatePicker :global(.react-datepicker__time-container .react-datepicker__time-box), +.darkDatePicker :global(.react-datepicker__time-container .react-datepicker__time-box ul.react-datepicker__time-list) { + background-color: #2a3f5f !important; + border-color: #3f5269 !important; + color: #e8edf4 !important; +} + +.darkDatePicker :global(.react-datepicker__time-list-item) { + background-color: #2a3f5f !important; + color: #e8edf4 !important; +} + +.darkDatePicker :global(.react-datepicker__time-list-item:hover), +.darkDatePicker :global(.react-datepicker__time-list-item:focus) { + background-color: #3a506b !important; + color: #ffffff !important; +} + +.darkDatePicker :global(.react-datepicker__time-list-item--selected) { + background-color: #468ef9 !important; + color: #ffffff !important; +} + +.darkDatePickerPopper .react-datepicker__triangle { + display: none; +} + +/* Light mode styles to improve time list contrast */ +.lightDatePickerInput { + background-color: #ffffff !important; + color: #111827 !important; + border: 1px solid #d1d5db !important; +} + +.lightDatePicker { + background-color: #ffffff !important; + color: #111827 !important; + border: 1px solid #d1d5db !important; +} + +.lightDatePicker .react-datepicker__header { + background-color: #f1f5f9; + border-color: #d1d5db; +} + +.lightDatePicker .react-datepicker__current-month, +.lightDatePicker .react-datepicker__time-header, +.lightDatePicker .react-datepicker-year-header { + color: #111827 !important; +} + +.lightDatePicker .react-datepicker__day-name, +.lightDatePicker .react-datepicker__day, +.lightDatePicker .react-datepicker__time-name { + color: #1f2937; +} + +.lightDatePicker .react-datepicker__day--selected, +.lightDatePicker .react-datepicker__day--keyboard-selected, +.lightDatePicker .react-datepicker__time-list-item--selected { + background-color: #468ef9 !important; + color: #ffffff !important; +} + +.lightDatePicker .react-datepicker__time-container { + border-color: #d1d5db !important; + background-color: #ffffff !important; +} + +.lightDatePicker .react-datepicker__time-container .react-datepicker__time { + background-color: #ffffff !important; + border-color: #d1d5db !important; +} + +.lightDatePicker .react-datepicker__time-container .react-datepicker__time-box { + background-color: #ffffff !important; + border-color: #d1d5db !important; +} + +.lightDatePicker .react-datepicker__time-container .react-datepicker__time-list { + background-color: #ffffff !important; + color: #1f2937 !important; + border-color: #d1d5db !important; +} + +.lightDatePicker :global(.react-datepicker__time-container), +.lightDatePicker :global(.react-datepicker__time-container .react-datepicker__time), +.lightDatePicker :global(.react-datepicker__time-container .react-datepicker__time-box), +.lightDatePicker :global(.react-datepicker__time-container .react-datepicker__time-box ul.react-datepicker__time-list) { + background-color: #ffffff !important; + border-color: #d1d5db !important; + color: #1f2937 !important; +} + +.lightDatePicker :global(.react-datepicker__time-list-item) { + background-color: #ffffff !important; + color: #1f2937 !important; +} + +.lightDatePicker :global(.react-datepicker__time-list-item:hover), +.lightDatePicker :global(.react-datepicker__time-list-item:focus) { + background-color: #e5e7eb !important; + color: #111827 !important; +} + +.lightDatePicker :global(.react-datepicker__time-list-item--selected) { + background-color: #468ef9 !important; + color: #ffffff !important; +} + +.lightDatePickerPopper .react-datepicker__triangle { + display: none; +} diff --git a/src/components/BMDashboard/ItemList/ItemsTable.jsx b/src/components/BMDashboard/ItemList/ItemsTable.jsx index 4b17f4f843..53ae6cc115 100644 --- a/src/components/BMDashboard/ItemList/ItemsTable.jsx +++ b/src/components/BMDashboard/ItemList/ItemsTable.jsx @@ -12,6 +12,7 @@ export default function ItemsTable({ filteredItems, UpdateItemModal, dynamicColumns, + darkMode = false, }) { const [sortedData, setData] = useState(filteredItems); const [modal, setModal] = useState(false); @@ -97,8 +98,8 @@ export default function ItemsTable({ recordType={recordType} /> -
- +
+
{selectedProject === 'all' ? ( diff --git a/src/components/BMDashboard/Lesson/LessonForm.jsx b/src/components/BMDashboard/Lesson/LessonForm.jsx index e91ab92d1d..7d5627f618 100644 --- a/src/components/BMDashboard/Lesson/LessonForm.jsx +++ b/src/components/BMDashboard/Lesson/LessonForm.jsx @@ -23,11 +23,10 @@ function LessonForm() { const roles = useSelector(state => state.role.roles); // grab all roles from store // const projects = useSelector(state => state.allProjects.projects); // grab all projects from store(not BM projects) const projects = useSelector(state => state.bmProjects); // grab all BM projects from store - const [LessonFormtags, setLessonFormTags] = useState(['Building 1', 'Building 2', 'Building 3']); // save all tags user inputs - const [permanentTags, setPermanentTags] = useState(['Building 1', 'Building 2', 'Building 3']); + const [LessonFormtags, setLessonFormTags] = useState([]); // save all tags user inputs + const [permanentTags, setPermanentTags] = useState([]); const [tagInput, setTagInput] = useState(''); // track user input in tag input const [selectedFile, setSelectedFile] = useState(null); // track file that was selected or droped in upload appendix - const [prevselectedProject, setprevSelectedProject] = useState(null); // used to track the previously project selected for deletion in tags when changed const [selectedProject, setSelectedProject] = useState(null); // Track selected project in Belongs to dropdown const [selectedRole, setSelectedRole] = useState('All'); // track selected role in View by dropdown const [LessonText, setLessonText] = useState(null); // track lesson text @@ -89,12 +88,6 @@ function LessonForm() { const newTags = LessonFormtags.filter(tag => tag !== tagToRemove); setLessonFormTags(newTags); }; - // removes the previously added project from tags if a new one is selected from belongs to dropdown - const removePreviousProject = prevproject => { - const newTags = LessonFormtags.filter(project => project !== prevproject); - setLessonFormTags(newTags); - }; - const fetchTags = async () => { try { const response = await axios.get(ENDPOINTS.BM_TAGS); @@ -137,7 +130,6 @@ function LessonForm() { // Set the project name as a tag setProjectName(foundProject.name); setSelectedProject(projectId); - setLessonFormTags([projectname]); } } }, [projectId, projects]); @@ -193,38 +185,10 @@ function LessonForm() { const lessonformtitleinput = e.target.value; setLessonTitleText(lessonformtitleinput); }; - useEffect(() => { - if (selectedProject && prevselectedProject !== selectedProject) { - // Find the project with the selected ID - const foundProject = projects.find(project => project._id === selectedProject); - // Check if the found project is valid - if (foundProject) { - setprevSelectedProject(selectedProject); - // Remove the tag for the previously selected project - if (prevselectedProject) { - removePreviousProject( - projects.find(project => project._id === prevselectedProject)?.name, - ); - } - // Add the project name to the tags array - setLessonFormTags(tags => [...tags, foundProject.name]); - } - } - }, [selectedProject, prevselectedProject, projects]); // Lesson submit. all the data from user input is in here const LessonFormSubmit = async e => { e.preventDefault(); - // console.log(LessonFormtags, "Tags") - // console.log(selectedProject, "selected project") - // console.log(selectedRole, "selecedRole") - // console.log(selectedFile, "selected file") - // console.log(LessonText, "lesson text") - // console.log(LessonTitleText,"lesson title") - if (!LessonFormtags.length) { - toast.info('Need atleast one tag'); - return; - } if (!selectedProject) { toast.info('Need to select a project'); return; diff --git a/src/components/BMDashboard/RentalChart/RentalChart.css b/src/components/BMDashboard/RentalChart/RentalChart.css deleted file mode 100644 index 6c015ef644..0000000000 --- a/src/components/BMDashboard/RentalChart/RentalChart.css +++ /dev/null @@ -1,153 +0,0 @@ -.body.dark-mode { - background-color: #1b2a41; - color: #e0e0e0; -} - -.rental-container { - position: relative; - width: 100%; - margin: 0 auto; - padding: 20px; -} - -.rental-container.dark-mode { - background-color: #1b2a41; - color: #e0e0e0; -} - -.text-light { - color: #e0e0e0; -} - -.dark-select { - background-color: #333; - color: #e0e0e0; - border: 1px solid #555; -} - -.dark-date-picker { - background-color: #333; - color: #e0e0e0; - border: 1px solid #555; -} - -.dark-chart { - background-color: #ab2a41; - border-radius: 8px; - padding: 16px; -} - -.react-datepicker { - background-color: #333; - color: #e0e0e0; - border: 1px solid #555; -} - -.react-datepicker__header { - background-color: #444; - border-bottom: 1px solid #555; -} - -.react-datepicker__current-month, -.react-datepicker__day-name, -.react-datepicker__day, -.react-datepicker__month-text, -.react-datepicker__year-text { - color: #e0e0e0; -} - -.react-datepicker__day:hover, -.react-datepicker__month-text:hover, -.react-datepicker__year-text:hover { - background-color: #555; -} - -.react-datepicker__day--selected, -.react-datepicker__day--in-selecting-range, -.react-datepicker__day--in-range { - background-color: #0078d7; - color: white; -} - .chart-controls { - display: flex; - flex-direction: column; - gap: 15px; - margin-bottom: 20px; - width: 100%; - } - - .filter-row { - display: flex; - flex-wrap: wrap; - gap: 20px; - margin-top: 40px; - align-items: center; - justify-content: flex-end; - } - - .top-filters { - width: 100%; - margin-bottom: 10px; - float: right; - } - - .date-filters { - width: 100%; - float:right; - } - - .filter-group { - display: flex; - align-items: center; - white-space: nowrap; - } - - .filter-text { - margin-right: 8px; - white-space: nowrap; - } - - .chart-wrapper { - height: 600px; - width: 100%; - margin: 0 auto; - padding: 40px; - box-sizing: border-box; - border-radius: 8px; - background-color: #fff; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); - } - - .date-picker { - padding: 6px 10px; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 14px; - width: 120px; - } - - .rental-char-select { - padding: 6px 10px; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 14px; - min-width: 150px; - } - - .loading, .error, .no-data { - display: flex; - justify-content: center; - align-items: center; - height: 100%; - font-size: 16px; - color: #666; - } - - .error { - color: #d32f2f; - } - - .react-datepicker-popper { - z-index: 999 !important; - max-width: 100vw; - } \ No newline at end of file diff --git a/src/components/BMDashboard/RentalChart/RentalChart.jsx b/src/components/BMDashboard/RentalChart/RentalChart.jsx index e3e407ef22..f00bdeafcc 100644 --- a/src/components/BMDashboard/RentalChart/RentalChart.jsx +++ b/src/components/BMDashboard/RentalChart/RentalChart.jsx @@ -4,8 +4,9 @@ import axios from 'axios'; import { ENDPOINTS } from '~/utils/URL'; import { Line } from 'react-chartjs-2'; import DatePicker from 'react-datepicker'; -import './RentalChart.css'; +import styles from './RentalChart.module.css'; import { toast } from 'react-toastify'; +import ChartDataLabels from 'chartjs-plugin-datalabels'; import { Chart as ChartJS, CategoryScale, @@ -17,7 +18,16 @@ import { Legend, } from 'chart.js'; -ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + ChartDataLabels, +); // these colors can be randomly generated once more projects are shared here. Colors generated from ChatGPT const PROJECT_COLORS = [ @@ -45,6 +55,7 @@ const MONTHS = [ ]; export default function RentalChart() { + const containerRef = useRef(null); const chartRef = useRef(null); const [chartData, setChartData] = useState({ labels: [], @@ -66,6 +77,7 @@ export default function RentalChart() { const [availableProjects, setAvailableProjects] = useState([]); const [availableTools, setAvailableTools] = useState([]); const [rawData, setRawData] = useState([]); + const [hiddenSeries, setHiddenSeries] = useState({}); // Function to process data for the chart const processChartData = data => { @@ -247,17 +259,67 @@ export default function RentalChart() { } }, [chartType, selectedProject, selectedTool, dateRange, groupBy, rawData]); + useEffect(() => { + setHiddenSeries({}); + }, [chartData.datasets]); + + const readThemeVars = node => { + const styles = + typeof window !== 'undefined' + ? getComputedStyle(node ?? document.documentElement) + : { + getPropertyValue: () => '', + }; + const getVar = (name, fallback) => { + const value = styles.getPropertyValue(name); + return value ? value.trim() : fallback; + }; + + return { + background: getVar('--chart-bg', darkMode ? '#1b2a41' : '#ffffff'), + legendColor: getVar('--chart-legend-color', darkMode ? '#e0e0e0' : '#1b2a41'), + axisColor: getVar('--chart-axis-color', darkMode ? '#e0e0e0' : '#333333'), + gridColor: getVar('--chart-grid-color', darkMode ? 'rgba(255,255,255,0.16)' : '#d9d9d9'), + tooltipBg: getVar('--chart-tooltip-bg', darkMode ? '#1b2a41' : 'rgba(255,255,255,0.92)'), + tooltipTitle: getVar('--chart-tooltip-title', darkMode ? '#ffffff' : '#1b2a41'), + tooltipText: getVar('--chart-tooltip-text', darkMode ? '#e0e0e0' : '#333333'), + legendBg: getVar( + '--chart-legend-bg', + darkMode ? 'rgba(27,42,65,0.7)' : 'rgba(255,255,255,0.75)', + ), + pointLabel: getVar('--chart-point-label', darkMode ? '#e0e0e0' : '#1b2a41'), + pointLabelBg: getVar( + '--chart-point-label-bg', + darkMode ? 'rgba(15,23,42,0.75)' : 'rgba(255,255,255,0.8)', + ), + }; + }; + + const [themeVars, setThemeVars] = useState(() => readThemeVars(document.documentElement)); + + useEffect(() => { + const node = containerRef.current || document.documentElement; + // wait until class toggles have applied + const id = requestAnimationFrame(() => setThemeVars(readThemeVars(node))); + return () => cancelAnimationFrame(id); + }, [darkMode]); + const options = useMemo(() => { return { responsive: true, maintainAspectRatio: false, - backgroundColor: darkMode ? '#1b2a41' : '#ffffff', + backgroundColor: themeVars.background, + layout: { + padding: { + left: 16, + right: 28, + top: 32, + bottom: 16, + }, + }, plugins: { legend: { - position: 'top', - labels: { - color: darkMode ? '#1b2a41' : '#333333', - }, + display: false, }, title: { display: true, @@ -265,7 +327,7 @@ export default function RentalChart() { font: { size: 18, }, - color: darkMode ? '#ffffff' : '#1b2a41', + color: themeVars.legendColor, }, tooltip: { callbacks: { @@ -283,25 +345,41 @@ export default function RentalChart() { return label; }, }, - backgroundColor: darkMode ? '#1b2a41' : 'rgba(255,255,255,0.8)', - titleColor: darkMode ? '#ffffff' : '#1b2a41', - bodyColor: darkMode ? '#ffffff' : '#333333', - borderColor: darkMode ? 'rgba(255,255,255, 0.2)' : '#1b2a41', + backgroundColor: themeVars.tooltipBg, + titleColor: themeVars.tooltipTitle, + bodyColor: themeVars.tooltipText, + borderColor: themeVars.gridColor, borderWidth: 1, }, + datalabels: { + color: themeVars.pointLabel, + backgroundColor: themeVars.pointLabelBg, + borderRadius: 4, + padding: { top: 4, bottom: 4, left: 6, right: 6 }, + align: 'top', + anchor: 'end', + offset: 6, + clip: false, + clamp: true, + display: ctx => ctx.dataset.data[ctx.dataIndex] !== undefined, + formatter: value => { + if (value === undefined || value === null || Number.isNaN(value)) return ''; + return chartType === 'percentage' ? `${value}%` : `$${Math.round(value)}`; + }, + }, }, scales: { x: { title: { display: true, text: 'Month/Year', - color: darkMode ? '#e0e0e0' : '#333333', + color: themeVars.axisColor, }, ticks: { - color: darkMode ? '#e0e0e0' : '#333333', + color: themeVars.axisColor, }, grid: { - color: darkMode ? '#e0e0e0' : '#333333', + color: themeVars.gridColor, }, }, y: { @@ -312,21 +390,21 @@ export default function RentalChart() { chartType === 'percentage' ? 'Percentage of Total Materials Cost (%)' : 'Total Rental Cost ($)', - color: darkMode ? '#e0e0e0' : '#333333', + color: themeVars.axisColor, }, ticks: { callback(value) { return chartType === 'percentage' ? `${value}%` : `$${value}`; }, - color: darkMode ? '#e0e0e0' : '#333333', + color: themeVars.axisColor, }, grid: { - color: darkMode ? 'rgba(255,255,255,0.1)' : '#1b2a41', + color: themeVars.gridColor, }, }, }, }; - }, [darkMode, chartType, generateChartTitle]); + }, [chartType, generateChartTitle, themeVars]); const handleTypeChange = e => { setChartType(e.target.value); @@ -357,17 +435,19 @@ export default function RentalChart() { const renderChartContent = () => { if (loading) { return ( -
Loading Chart Data....
+
+ Loading Chart Data.... +
); } if (error) { - return
{error}
; + return
{error}
; } if (chartData.datasets.length === 0) { return ( -
+
No data available for the selected filters
); @@ -376,47 +456,81 @@ export default function RentalChart() { return ; }; + const handleLegendToggle = index => { + const chart = chartRef.current; + if (!chart) return; + const isVisible = chart.isDatasetVisible(index); + chart.setDatasetVisibility(index, !isVisible); + chart.update(); + setHiddenSeries(prev => ({ + ...prev, + [index]: isVisible, + })); + }; + + const legendItems = chartData.datasets.map((dataset, index) => { + const isHiddenFromChart = chartRef.current?.isDatasetVisible + ? !chartRef.current.isDatasetVisible(index) + : hiddenSeries[index]; + return { + label: dataset.label || `Series ${index + 1}`, + color: dataset.borderColor, + hidden: !!isHiddenFromChart, + index, + }; + }); + return (
-

Rental Cost Over Time

-
+

Rental Cost Over Time

+
-
-
@@ -211,29 +214,34 @@ function AssignBadge(props) { - - + toggle(false)} + backdrop="static" + className={darkMode ? 'text-light dark-mode' : ''} + > + toggle(false)}> Assign Badge toggle(true)} + selectedBadges={selectedBadges} /> - Please select a badge from the badge list. + Please select badge(s) from the badge list. - {' '} - {props.selectedBadges ? props.selectedBadges.length : '0'} badges selected + {selectedUserIds?.length} user(s) selected, + {selectedBadges?.length} badge(s) selected @@ -241,7 +249,7 @@ function AssignBadge(props) { } const mapStateToProps = state => ({ - selectedBadges: state.badge.selectedBadges, + selectedBadges: state.badge.selectedBadges || [], firstName: state.badge.firstName, lastName: state.badge.lastName, userId: state.badge.userId, @@ -258,7 +266,8 @@ const mapDispatchToProps = dispatch => ({ getUserId: userId => dispatch(getUserId(userId)), getAllUserProfile: () => dispatch(getAllUserProfile()), clearNameAndSelected: () => dispatch(clearNameAndSelected()), - assignBadgesByUserID: (id, selectedBadge) => dispatch(assignBadgesByUserID(id, selectedBadge)), + assignBadgesToMultipleUserID: (userIds, selectedBadges) => + dispatch(assignBadgesToMultipleUserID(userIds, selectedBadges)), assignBadges: (firstName, lastName, selectedBadge) => dispatch(assignBadges(firstName, lastName, selectedBadge)), validateBadges: (firstName, lastName) => dispatch(validateBadges(firstName, lastName)), diff --git a/src/components/Badge/AssignTableRow.jsx b/src/components/Badge/AssignTableRow.jsx index 6992cf6f6c..fe20bf8f92 100644 --- a/src/components/Badge/AssignTableRow.jsx +++ b/src/components/Badge/AssignTableRow.jsx @@ -1,21 +1,38 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Card, CardBody, CardImg, CardText, Popover, CustomInput } from 'reactstrap'; -import { useDispatch, useSelector } from 'react-redux'; +import { connect, useDispatch, useSelector } from 'react-redux'; import { addSelectBadge, removeSelectBadge } from '../../actions/badgeManagement'; -function AssignTableRow({ badge, index, existBadges: propExistBadges }) { +function AssignTableRow(props) { + // Pull selected badges from Redux if prop is not passed + const selectedFromStore = useSelector(s => s.badge?.selectedBadges || []); + const effectiveSelected = props?.selectedBadges ?? selectedFromStore; + const [isOpen, setOpen] = useState(false); + const dispatch = useDispatch(); const selectedBadges = useSelector(state => state.badge.selectedBadges); // array of badge._id - const [isOpen, setOpen] = useState(false); const toggle = () => setOpen(prev => !prev); - const badgeId = badge._id; + const initialChecked = useMemo(() => { + const id = `assign-badge-${props.badge?._id}`; + return Array.isArray(effectiveSelected) && effectiveSelected.includes(id); + }, [effectiveSelected, props.badge?._id]); + + const [isSelect, setSelect] = useState(initialChecked); + + useEffect(() => { + const id = `assign-badge-${props.badge?._id}`; + const next = Array.isArray(effectiveSelected) && effectiveSelected.includes(id); + setSelect(prev => (prev !== next ? next : prev)); + }, [effectiveSelected, props.badge?._id]); + + const badgeId = props.badge?._id; const domId = `assign-badge-${badgeId}`; // Initialize selection from props (badges that user already has) useEffect(() => { - if (propExistBadges?.includes(badgeId)) { + if (props.propExistBadges?.includes(badgeId)) { dispatch(addSelectBadge(badgeId)); } // run once on mount @@ -23,34 +40,38 @@ function AssignTableRow({ badge, index, existBadges: propExistBadges }) { }, []); const isSelected = selectedBadges.includes(badgeId); + // eslint-disable-next-line no-console + console.log(selectedBadges, 'sele', badgeId, props); const handleCheckBoxChange = e => { if (e.target.checked) { dispatch(addSelectBadge(badgeId)); + setSelect(true); } else { dispatch(removeSelectBadge(badgeId)); + setSelect(false); } }; return ( - +

Events

+ - {events.length > 0 ? ( - events.map(event => ( + {displayedEvents.length > 0 ? ( + displayedEvents.map(event => (
- {event.title}
{event.title}

- {event.date} + {formatDate(event.date)}

{event.location} @@ -183,6 +261,30 @@ export function CPDashboard() {

No events available
)} + + {/* Simple pagination controls if needed */} + {totalPages > 1 && ( +
+ + + Page {pagination.currentPage} of {totalPages} + + +
+ )} +
diff --git a/src/components/CommunityPortal/CPDashboard.module.css b/src/components/CommunityPortal/CPDashboard.module.css index fcde3d24aa..92bc8be79b 100644 --- a/src/components/CommunityPortal/CPDashboard.module.css +++ b/src/components/CommunityPortal/CPDashboard.module.css @@ -1,4 +1,4 @@ -body { +:global(body) { font-family: 'Poppins', sans-serif; background: #fff; margin: 0; @@ -6,8 +6,7 @@ body { } .dashboard-container { - padding: 20px 15px; - max-width: 1400px; + padding: 40px 10px; margin: 0 auto; background: #ffffff; border-radius: 16px; @@ -15,7 +14,7 @@ body { font-size: 16px; line-height: 1.6; letter-spacing: 0.2px; - width: min(95%, 1400px); + width: 100%; } .dashboard-header { @@ -23,10 +22,41 @@ body { justify-content: space-between; align-items: center; margin-bottom: 30px; - padding: 20px; + padding: 16px 24px; background: linear-gradient(120deg, #ffffff, #f8f9fa); border-radius: 12px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); +} + +.dashboard-content{ + width: 100%; + padding-left: 20px; + margin: 0; +} + +.dashboard-search { + border: none; + padding: 8px 16px; + min-width: 260px; + border-radius: 999px 0 0 999px; + outline: none; +} + +.dashboard-search:focus { + box-shadow: none; +} + +.dashboard-search-button { + border-radius: 0 999px 999px 0; + border: none; + display: flex; + align-items: center; + justify-content: center; + padding: 0 18px; +} + +.dashboard-search-icon { + font-size: 1rem; } .dashboard-header h1 { @@ -45,6 +75,57 @@ body { transition: all 0.3s ease; } +/* pill input */ +.dashboard-search-input { + padding: 8px 90px 8px 16px; /* extra right padding for icons */ + border-radius: 999px; + border: 2px solid #1b3c55; + min-width: 260px; + outline: none; +} + +/* clear X button */ +.dashboard-clear-btn { + position: absolute; + right: 35px; + top: 45%; + transform: translateY(-50%); + border: none; + background: transparent; + cursor: pointer; + font-size: 0.9rem; + color: #1b3c55; +} + +/* blue circular search button inside the bar */ +.dashboard-search-icon-btn { + position: absolute; + right: 1%; + top: 48%; + transform: translateY(-50%); + width: 34px; + height: 34px; + border-radius: 50%; + cursor: pointer; + background: transparent; /* 🔹 icon background now white */ + display: flex; + align-items: center; + justify-content: center; + color: #1b3c55; /* blue magnifying-glass icon */ + font-size: 0.9rem; +} + + +.dashboard-search-container { + position: relative; + display: inline-flex; + align-items: center; + border-radius: 999px; + overflow: hidden; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.1); + background: #ffffff; +} + .dashboard-search-container input:focus { border-color: #34495e; box-shadow: 0 0 8px rgba(52, 73, 94, 0.3); @@ -52,8 +133,10 @@ body { } .dashboard-sidebar { - padding: 30px; - background: #f9f9f9; + padding: 20px; + display: flex; + justify-content: flex-start; + background: #ffffff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } @@ -61,29 +144,29 @@ body { .filter-section h4 { font-size: 1.8rem; color: #2c3e50; - /* margin-bottom: 20px; */ font-weight: 600; } -.filter-section-divider { - height: fit-content; - margin: 10px 10px;; - border-radius: 1px; - display: flex; - flex-direction: column; -} - -.filter-item{ - padding: 8px 15px; +.filter-item input:not([type="checkbox"]):not([type="radio"]), +.filter-item select { + padding: 12px 15px; margin-top: 10px; - /* border: 1px solid #ddd; */ border-radius: 8px; width: 100%; + height: auto; font-size: 1rem; - /* background: #ffffff; */ transition: all 0.3s ease; } +.filter-item input[type="radio"], +.filter-item input[type="checkbox"] { + display: inline-block; /* <– keeps input on same line as text */ + width: auto; + padding: 0; + margin: 5px 0px 0px 120px; /* top margin to line up under "Dates" */ + vertical-align: middle; +} + .filter-item input:focus, .filter-item select:focus { border-color: #2c3e50; @@ -92,11 +175,7 @@ body { } .dashboard-main { - margin-top: 25px; - display: flex; - flex-wrap: wrap; - gap: 20px; - background-color: #e5e5e5; + width: 65%; padding: 30px; border-radius: 12px; box-shadow: 0 4px 12px #0000001a; @@ -119,6 +198,12 @@ body { transition: transform 0.3s ease, box-shadow 0.3s ease; } +.event-card-col { + display: flex; +} + + + .event-card:hover { transform: scale(1.05); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); @@ -171,6 +256,11 @@ body { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } -.date-filter { - margin-top: 15px; +/* Hide browser's built-in clear icon for search inputs */ +.dashboard-search-input::-webkit-search-cancel-button, +.dashboard-search-input::-webkit-search-decoration, +.dashboard-search-input::-webkit-search-results-button, +.dashboard-search-input::-webkit-search-results-decoration { + -webkit-appearance: none; + appearance: none; } \ No newline at end of file diff --git a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx index 44dbe0d734..bd56c2f0d0 100644 --- a/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx +++ b/src/components/HGNHelpSkillsDashboard/CommunityMembersPage.jsx @@ -1,10 +1,39 @@ -import { useState } from 'react'; -import RankedUserList from './RankedUserList'; // wherever your RankedUserList is +import { useEffect, useMemo, useState } from 'react'; +import axios from 'axios'; +import RankedUserList from './RankedUserList'; +import styles from './style/CommunityMembersPage.module.css'; const availableSkills = ['React', 'Redux', 'HTML', 'CSS', 'MongoDB', 'Database', 'Agile']; +const RANKED_USERS_ENDPOINT = 'http://localhost:4500/api/hgnform/ranked'; function CommunityMembersPage() { const [selectedSkills, setSelectedSkills] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [sortOrder, setSortOrder] = useState('asc'); + const [showFilters, setShowFilters] = useState(false); + const [rankedUsers, setRankedUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchRankedUsers = async () => { + setLoading(true); + try { + const params = { + skills: (selectedSkills.length ? selectedSkills : availableSkills).join(','), + }; + const response = await axios.get(RANKED_USERS_ENDPOINT, { params }); + setRankedUsers(response.data); + setError(null); + } catch (err) { + setError('Unable to load community members right now. Please try again later.'); + } finally { + setLoading(false); + } + }; + + fetchRankedUsers(); + }, [selectedSkills]); const handleCheckboxChange = skill => { setSelectedSkills(prev => @@ -12,23 +41,114 @@ function CommunityMembersPage() { ); }; + const toggleSortOrder = () => { + setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc')); + }; + + const clearFilters = () => { + setSelectedSkills([]); + }; + + const filteredUsers = useMemo(() => { + const normalizedSearch = searchTerm.trim().toLowerCase(); + const normalizedSelectedSkills = selectedSkills.map(skill => skill.toLowerCase()); + let result = rankedUsers; + + if (normalizedSearch) { + result = rankedUsers.filter(user => { + const nameMatches = user.name?.toLowerCase().includes(normalizedSearch); + const skillMatches = Array.isArray(user.topSkills) + ? user.topSkills.some(skill => skill.toLowerCase().includes(normalizedSearch)) + : false; + return nameMatches || skillMatches; + }); + } + + if (normalizedSelectedSkills.length) { + result = result.filter(user => { + if (!Array.isArray(user.topSkills) || user.topSkills.length === 0) return false; + return user.topSkills.some(skill => + normalizedSelectedSkills.includes((skill || '').toLowerCase()), + ); + }); + } + + return [...result].sort((a, b) => { + const first = a.name || ''; + const second = b.name || ''; + return sortOrder === 'asc' ? first.localeCompare(second) : second.localeCompare(first); + }); + }, [rankedUsers, searchTerm, sortOrder, selectedSkills]); + + const emptyMessage = + searchTerm || selectedSkills.length + ? 'No community members match your current filters.' + : 'No community members available yet.'; + return ( -
-

Select Skills to Filter Community Members

-
- {availableSkills.map(skill => ( - - ))} +
+

One Community Members

+
+
+ setSearchTerm(event.target.value)} + placeholder="Search by team member name or skills" + className={styles.searchInput} + aria-label="Search community members" + /> +
+ + + {(selectedSkills.length > 0 || searchTerm) && ( + + )}
- {selectedSkills.length > 0 && } + {showFilters && ( +
+ {availableSkills.map(skill => ( + + ))} +
+ )} + +

+ When multiple filters are selected, the score represents the average value, and the options + are ranked based on their scoring. Click each profile to learn more details. +

+ +
); } diff --git a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx index b7017b7715..50e75c1527 100644 --- a/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx +++ b/src/components/HGNHelpSkillsDashboard/RankedUserList.jsx @@ -1,41 +1,38 @@ -import { useEffect, useState } from 'react'; -import axios from 'axios'; +import PropTypes from 'prop-types'; import UserCard from './UserCard'; -import './style/UserCard.module.css'; +import styles from './style/UserCard.module.css'; -function RankedUserList({ selectedSkills }) { - const [rankedUsers, setRankedUsers] = useState([]); - const [loading, setLoading] = useState(true); +function RankedUserList({ users, loading, error, emptyMessage }) { + if (loading) { + return

Loading community members...

; + } - useEffect(() => { - if (!selectedSkills || selectedSkills.length === 0) return; + if (error) { + return

{error}

; + } - const fetchRankedUsers = async () => { - setLoading(true); - try { - const response = await axios.get('http://localhost:4500/api/hgnform/ranked', { - params: { skills: selectedSkills.join(',') }, - }); - setRankedUsers(response.data); - } catch (err) { - // console.error('Error fetching ranked users:', err); - } finally { - setLoading(false); - } - }; - - fetchRankedUsers(); - }, [selectedSkills]); - - if (loading) return

Loading ranked users...

; + if (!users.length) { + return

{emptyMessage}

; + } return ( -
- {rankedUsers.map(user => ( - +
+ {users.map(user => ( + ))}
); } +RankedUserList.propTypes = { + users: PropTypes.arrayOf(PropTypes.object).isRequired, + loading: PropTypes.bool.isRequired, + error: PropTypes.string, + emptyMessage: PropTypes.string.isRequired, +}; + +RankedUserList.defaultProps = { + error: null, +}; + export default RankedUserList; diff --git a/src/components/HGNHelpSkillsDashboard/UserCard.jsx b/src/components/HGNHelpSkillsDashboard/UserCard.jsx index 74e151a495..ed83b38e0c 100644 --- a/src/components/HGNHelpSkillsDashboard/UserCard.jsx +++ b/src/components/HGNHelpSkillsDashboard/UserCard.jsx @@ -4,7 +4,7 @@ import emailIcon from './style/email_icon.png'; import slackIcon from './style/slack_icon.png'; function UserCard({ user }) { - const { name, email, slack, score, topSkills } = user; + const { name, email, slack, score, topSkills = [] } = user; const getScoreColor = userScore => { if (userScore >= 5) return '#00754A'; diff --git a/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css new file mode 100644 index 0000000000..9fb9dc30b5 --- /dev/null +++ b/src/components/HGNHelpSkillsDashboard/style/CommunityMembersPage.module.css @@ -0,0 +1,88 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 32px 24px 48px; +} + +.heading { + font-size: 28px; + font-weight: 600; + margin-bottom: 16px; + color: #222222; +} + +.controlsRow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.searchWrapper { + flex: 1 1 280px; + min-width: 240px; +} + +.searchInput { + width: 100%; + border: 1px solid #cccccc; + border-radius: 6px; + padding: 10px 12px; + font-size: 16px; +} + +.filterButton, +.sortButton, +.clearButton { + border: 1px solid #1f6feb; + background-color: transparent; + color: #1f6feb; + border-radius: 6px; + font-size: 15px; + padding: 9px 16px; + cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.filterButton:hover, +.sortButton:hover, +.clearButton:hover { + background-color: #1f6feb; + color: #ffffff; +} + +.clearButton { + border-color: #d93d3d; + color: #d93d3d; +} + +.clearButton:hover { + background-color: #d93d3d; + color: #ffffff; +} + +.filtersPanel { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 16px; + padding: 12px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #fafafa; +} + +.filterOption { + display: flex; + align-items: center; + gap: 6px; + font-size: 15px; + color: #333333; +} + +.helperText { + font-size: 14px; + color: #555555; + margin-bottom: 20px; +} diff --git a/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css b/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css index 0d6da0a1d2..1b1ef2b75a 100644 --- a/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css +++ b/src/components/HGNHelpSkillsDashboard/style/UserCard.module.css @@ -1,7 +1,8 @@ .userCard { - width: 360px; + width: 100%; + max-width: 360px; height: auto; - padding: 30px 24px; + padding: 28px 24px; background: #ffffff; border: 2px solid #eeeeee; box-shadow: 1px 4px 4px rgba(0, 0, 0, 0.25); @@ -11,6 +12,7 @@ flex-direction: column; align-items: center; gap: 16px; + margin: 0 auto; } .avatar { @@ -23,31 +25,34 @@ .info { display: flex; flex-direction: column; - align-items: flex-start; /* keeps name/email/slack left-aligned */ + align-items: stretch; text-align: left; - margin-top: 16px; - padding: 0; - width: fit-content; - margin-left: auto; - margin-right: auto; + gap: 8px; + margin-top: 12px; + width: 100%; + padding: 0 16px; } .userName { - font-size: 30px; - font-weight: 400; + width: 100%; + font-size: 24px; + font-weight: 500; color: black; - margin-bottom: 8px; - white-space: nowrap; - /*font-size: Large;*/ + margin-bottom: 6px; + word-break: break-word; + line-height: 1.25; + text-align: center; } .contactLine { + width: 100%; display: flex; align-items: center; gap: 8px; - font-size: 16px; + font-size: 13px; color: #616161; margin-bottom: 6px; + flex-wrap: wrap; } .contactIcon { @@ -56,6 +61,10 @@ margin-top: 1px; } +.contactLine span { + word-break: break-all; +} + .scoreSkillsWrapper { width: 100%; display: flex; @@ -67,7 +76,7 @@ .scoreLine { font-size: 22px; text-align: left; - margin-left: 4px; + padding: 0 16px; } .scoreLabel { @@ -87,20 +96,28 @@ .skillsSection { width: 100%; text-align: left; - padding: 0 4px; + padding: 0 16px 0 16px; } .skillsLabel { - font-size: 20px; + font-size: 18px; font-weight: 400; text-decoration: underline; margin-bottom: 4px; } .skillsText { - font-size: 16px; + font-size: 15px; font-weight: 400; color: #616161; - line-height: 22px; + line-height: 21px; word-wrap: break-word; } + +.containerGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 32px; + padding: 24px 0 32px; + justify-items: center; +} diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index b45ed0a2ea..24f492e9b1 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -87,68 +87,69 @@ export function Header(props) { () => ALLOWED_ROLES_TO_INTERACT.includes(props.auth.user.role), [ALLOWED_ROLES_TO_INTERACT, props.auth.user.role], ); + const headerDisabled = isAuthUser ? false : !canInteractWithViewingUser; // Reports const canGetReports = props.hasPermission( 'getReports', - !isAuthUser && canInteractWithViewingUser, + !isAuthUser , ); const canGetWeeklySummaries = props.hasPermission( 'getWeeklySummaries', - !isAuthUser && canInteractWithViewingUser, + !isAuthUser, ); const canGetWeeklyVolunteerSummary = props.hasPermission('getWeeklySummaries'); const canGetJobAnalytics = props.hasPermission('getJobReports'); // Users const canAccessUserManagement = - props.hasPermission('postUserProfile', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('deleteUserProfile', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('changeUserStatus', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('getUserProfiles', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('setFinalDay', !isAuthUser && canInteractWithViewingUser); + props.hasPermission('postUserProfile', !isAuthUser ) || + props.hasPermission('deleteUserProfile', !isAuthUser ) || + props.hasPermission('changeUserStatus', !isAuthUser ) || + props.hasPermission('getUserProfiles', !isAuthUser ) || + props.hasPermission('setFinalDay', !isAuthUser); // Badges const canAccessBadgeManagement = - props.hasPermission('seeBadges', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('createBadges', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('updateBadges', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('deleteBadges', !isAuthUser && canInteractWithViewingUser); + props.hasPermission('seeBadges', !isAuthUser ) || + props.hasPermission('createBadges', !isAuthUser ) || + props.hasPermission('updateBadges', !isAuthUser) || + props.hasPermission('deleteBadges', !isAuthUser ); // Projects const canAccessProjects = - props.hasPermission('postProject', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('deleteProject', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('putProject', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('getProjectMembers', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('assignProjectToUsers', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('postWbs', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('deleteWbs', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('postTask', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('updateTask', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('deleteTask', !isAuthUser && canInteractWithViewingUser); + props.hasPermission('postProject', !isAuthUser ) || + props.hasPermission('deleteProject', !isAuthUser ) || + props.hasPermission('putProject', !isAuthUser ) || + props.hasPermission('getProjectMembers', !isAuthUser ) || + props.hasPermission('assignProjectToUsers', !isAuthUser ) || + props.hasPermission('postWbs', !isAuthUser ) || + props.hasPermission('deleteWbs', !isAuthUser ) || + props.hasPermission('postTask', !isAuthUser ) || + props.hasPermission('updateTask', !isAuthUser ) || + props.hasPermission('deleteTask', !isAuthUser); // Tasks const canUpdateTask = props.hasPermission( 'updateTask', - !isAuthUser && canInteractWithViewingUser, + !isAuthUser, ); // Teams const canAccessTeams = - props.hasPermission('postTeam', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('putTeam', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('deleteTeam', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('assignTeamToUsers', !isAuthUser && canInteractWithViewingUser); + props.hasPermission('postTeam', !isAuthUser ) || + props.hasPermission('putTeam', !isAuthUser) || + props.hasPermission('deleteTeam', !isAuthUser ) || + props.hasPermission('assignTeamToUsers', !isAuthUser); // Popups const canAccessPopups = - props.hasPermission('createPopup', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('updatePopup', !isAuthUser && canInteractWithViewingUser); + props.hasPermission('createPopup', !isAuthUser) || + props.hasPermission('updatePopup', !isAuthUser ); // SendEmails const canAccessSendEmails = props.hasPermission('sendEmails', !isAuthUser); // Permissions const canAccessPermissionsManagement = - props.hasPermission('postRole', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('putRole', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('deleteRole', !isAuthUser && canInteractWithViewingUser) || - props.hasPermission('putUserProfilePermissions', !isAuthUser && canInteractWithViewingUser); + props.hasPermission('postRole', !isAuthUser ) || + props.hasPermission('putRole', !isAuthUser ) || + props.hasPermission('deleteRole', !isAuthUser ) || + props.hasPermission('putUserProfilePermissions', !isAuthUser); // Blue Square Email Management const canAccessBlueSquareEmailManagement = props.hasPermission('resendBlueSquareAndSummaryEmails', !isAuthUser); @@ -413,7 +414,7 @@ export function Header(props) { > {canUpdateTask && ( - +
{props.taskEditSuggestionCount}
@@ -421,22 +422,22 @@ export function Header(props) {
)} - + {DASHBOARD} - + {TIMELOG} {showProjectDropdown && ( - + {PROJECTS} - + Add Material - + Log Material - + Material List Add Equipment/Tool @@ -461,6 +463,7 @@ export function Header(props) { tag={Link} to="/bmdashboard/equipment/:equipmentId" className={fontColor} + disabled={headerDisabled} > Log Equipment/Tool @@ -468,18 +471,22 @@ export function Header(props) { tag={Link} to="/bmdashboard/tools/:equipmentId/update" className={fontColor} + disabled={headerDisabled} > Update Equipment/Tool - + Equipment/Tool List - + Issue - + Lesson + + Team + )} @@ -492,48 +499,49 @@ export function Header(props) { {canGetReports && ( - + {REPORTS} )} {canGetWeeklySummaries && ( - + {WEEKLY_SUMMARIES_REPORT} )} {canGetWeeklyVolunteerSummary && ( - + {TOTAL_ORG_SUMMARY} )} {canGetJobAnalytics && ( - + {JOB_ANALYTICS_REPORT} )} - + {TEAM_LOCATIONS} {TOTAL_CONSTRUCTION_SUMMARY} - setShowPromotionsPopup(true)} className={fontColor}> + setShowPromotionsPopup(true)} className={fontColor} disabled={headerDisabled}> {PR_PROMOTIONS} ) : ( - + {TEAM_LOCATIONS} )} - + {(canAccessUserManagement || @@ -550,27 +558,27 @@ export function Header(props) { {canAccessUserManagement && ( - + {USER_MANAGEMENT} )} {canAccessBadgeManagement && ( - + {BADGE_MANAGEMENT} )} {canAccessProjects && ( - + {PROJECTS} )} {canAccessTeams && ( - + {TEAMS} )} {canAccessSendEmails && ( - + {SEND_EMAILS} )} @@ -581,13 +589,14 @@ export function Header(props) { tag={Link} to="/permissionsmanagement" className={fontColor} + disabled={headerDisabled} > {PERMISSIONS_MANAGEMENT} )} - + PR Team Analytics {canAccessBlueSquareEmailManagement && ( @@ -595,6 +604,7 @@ export function Header(props) { tag={Link} to="/bluesquare-email-management" className={fontColor} + disabled={headerDisabled} > {BLUE_SQUARE_EMAIL_MANAGEMENT} @@ -634,6 +644,7 @@ export function Header(props) { tag={Link} to={`/userprofile/${displayUserId}`} className={fontColor} + disabled={headerDisabled} > {VIEW_PROFILE} @@ -653,7 +664,7 @@ export function Header(props) { - + {LOGOUT} diff --git a/src/components/Projects/Project/Project.jsx b/src/components/Projects/Project/Project.jsx index dac0dbee39..ed8a5e5a5b 100644 --- a/src/components/Projects/Project/Project.jsx +++ b/src/components/Projects/Project/Project.jsx @@ -1,7 +1,9 @@ import { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { ARCHIVE } from './../../../languages/en/ui'; -import './../projects.css'; +// old CSS removed +// import './../projects.css'; +import styles from './../projects.module.css'; import { Link } from 'react-router-dom'; import { NavItem } from 'reactstrap'; import { connect } from 'react-redux'; @@ -9,7 +11,7 @@ import hasPermission from '~/utils/permissions'; import { boxStyle } from '~/styles'; import { toast } from 'react-toastify'; import { modifyProject } from '../../../actions/projects'; -import { CONFIRM_ARCHIVE } from './../../../languages/en/messages'; +// import { CONFIRM_ARCHIVE } from './../../../languages/en/messages'; // unused, removed const Project = props => { const { darkMode, index } = props; @@ -53,14 +55,15 @@ const Project = props => { setDisplayName(previousProject?.projectName || ''); } - const errorMessage = err?.response?.data?.message || 'An error occurred while updating the project'; + const errorMessage = + err?.response?.data?.message || 'An error occurred while updating the project'; toast.error(errorMessage); } }; - const onDisplayNameChange = (e) => { + const onDisplayNameChange = e => { setDisplayName(e.target.value); - } + }; const onUpdateProjectName = async () => { if (displayName.length < 3) { @@ -76,7 +79,7 @@ const Project = props => { props.onClickProjectStatusBtn(projectData); // This will open the modal }; - const onUpdateProjectCategory = (e) => { + const onUpdateProjectCategory = e => { const newCategory = e.target.value; setCategory(newCategory); persistProjectUpdate('category', newCategory); @@ -84,7 +87,7 @@ const Project = props => { const onArchiveProject = () => { props.onClickArchiveBtn(projectData); - } + }; useEffect(() => { setProjectData(props.projectData); @@ -93,125 +96,126 @@ const Project = props => { }, [props.projectData, props.category]); return ( - <> -
- - - - - - - {/* - - - + <> + + + + + + + + + + + + + + + + {canDeleteProject ? ( - - - {(canDeleteProject) ? ( - - // - // ) : null} - // - // - - ) : null} - - + ) : null} + + ); }; diff --git a/src/components/Projects/ProjectTableHeader/ProjectTableHeader.jsx b/src/components/Projects/ProjectTableHeader/ProjectTableHeader.jsx index dc2ec26ae3..b1d7e58a43 100644 --- a/src/components/Projects/ProjectTableHeader/ProjectTableHeader.jsx +++ b/src/components/Projects/ProjectTableHeader/ProjectTableHeader.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import './../projects.css'; +import './../projects.module.css'; import { PROJECT_NAME, ACTIVE, diff --git a/src/components/Projects/Projects.jsx b/src/components/Projects/Projects.jsx index c42d957501..10d910e3a6 100644 --- a/src/components/Projects/Projects.jsx +++ b/src/components/Projects/Projects.jsx @@ -14,7 +14,7 @@ import ProjectTableHeader from './ProjectTableHeader'; import Project from './Project'; import ModalTemplate from './../common/Modal'; import { CONFIRM_ARCHIVE, PROJECT_INACTIVE_CONFIRMATION, PROJECT_ACTIVE_CONFIRMATION } from './../../languages/en/messages'; -import './projects.css'; +import './projects.module.css'; import Loading from '../common/Loading'; import hasPermission from '../../utils/permissions'; import EditableInfoModal from '../UserProfile/EditableModal/EditableInfoModal'; @@ -322,7 +322,10 @@ const projectFetchStatus = useSelector(state => state.allProjects.status); return ( <>
-
+
{fetching || !fetched ? : null}

Projects

@@ -358,7 +361,7 @@ const projectFetchStatus = useSelector(state => state.allProjects.status);
-
+
- + - + - {badge.description} + {props.badge.description} {badge.badgeName}{props?.badge.badgeName} ({ + addSelectBadge: badgeId => dispatch(addSelectBadge(badgeId)), + removeSelectBadge: badgeId => dispatch(removeSelectBadge(badgeId)), +}); + +export default connect(null, mapDispatchToProps)(AssignTableRow); diff --git a/src/components/Badge/__tests__/AssignBadge.test.jsx b/src/components/Badge/__tests__/AssignBadge.test.jsx index 0b7e8dad79..43c2dabcbe 100644 --- a/src/components/Badge/__tests__/AssignBadge.test.jsx +++ b/src/components/Badge/__tests__/AssignBadge.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import thunk from 'redux-thunk'; import { configureStore } from 'redux-mock-store'; import { Provider } from 'react-redux'; @@ -9,25 +9,14 @@ import { vi } from 'vitest'; const mockUserProfilesData = [ { _id: 'user1', - firstName: 'jerry', - lastName: 'volunteer1', - permissions: { - frontPermissions: ['getWeeklySummaries', 'seeUserManagement'], - backPermissions: [], - }, - isActive: true, - weeklycommittedHours: 50, - role: 'Volunteer', + firstName: 'Jerry', + lastName: 'Volunteer1', email: 'jerryvolunteer1@gmail.com', }, { _id: 'user2', - firstName: 'jerry', - lastName: 'volunteer2', - permissions: { frontPermissions: ['editTimeEntry', 'toggleTangibleTime'], backPermissions: [] }, - isActive: true, - weeklycommittedHours: 10, - role: 'Volunteer', + firstName: 'Jerry', + lastName: 'Volunteer2', email: 'jerryvolunteer2@gmail.com', }, ]; @@ -67,31 +56,17 @@ describe('AssignBadge component', () => { let store; beforeEach(() => { - vi.clearAllMocks(); - store = mockStore({ - auth: { user: {} }, badge: { - selectedBadges: ['Badge 1', 'Badge 2'], firstName: '', lastName: '', - userId: null, - message: '', - alertVisible: false, - color: '', + selectedBadges: ['Badge 1', 'Badge 2'], }, allUserProfiles: { userProfiles: mockUserProfilesData, - fetching: false, - fetched: true, }, theme: { darkMode: false }, }); - - const origDispatch = store.dispatch; - store.dispatch = vi.fn(action => - typeof action === 'function' ? action(origDispatch) : origDispatch(action), - ); }); const renderComponent = () => @@ -105,37 +80,53 @@ describe('AssignBadge component', () => { renderComponent(); }); - it('renders label and input', () => { + it('renders the search input and label', () => { renderComponent(); expect(screen.getByText('Search by Full Name')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Full Name')).toBeInTheDocument(); }); - it('filters user list based on input', async () => { + it('displays correct number of selected badges', () => { renderComponent(); - fireEvent.change(screen.getByPlaceholderText('Full Name'), { target: { value: 'jerry' } }); - const rows = await screen.findAllByRole('row', { name: /jerry/i }); - expect(rows).toHaveLength(2); + const alertText = screen.getByRole('alert').textContent; + expect(alertText).toContain('2 badge(s) selected'); }); - it('disables the Assign Badge button until a user is selected', () => { + it('filters and displays users based on search input', async () => { renderComponent(); - expect(screen.getByText('Assign Badge')).toBeDisabled(); + const input = screen.getByPlaceholderText('Full Name'); + fireEvent.change(input, { target: { value: 'Jerry' } }); + + await waitFor(() => { + expect(screen.getAllByRole('row')).toHaveLength(3); // 2 users + header row + }); }); - it('shows tooltip messages on hover', async () => { + it('allows selecting users and updates the count', async () => { renderComponent(); - fireEvent.mouseEnter(screen.getByTestId('NameInfo')); - expect( - await screen.findByText(/Start typing a name and a list of the active members/), - ).toBeInTheDocument(); - expect( - await screen.findByText(/After selecting a person, click "Assign Badge"/), - ).toBeInTheDocument(); + const input = screen.getByPlaceholderText('Full Name'); + fireEvent.change(input, { target: { value: 'Jerry' } }); + + const userRows = (await screen.findAllByRole('row')).slice(1); // Exclude header row + fireEvent.click(userRows[0]); + + const alertText = screen.getByRole('alert').textContent; + expect(alertText).toContain('1 user(s) selected'); + }); + + it('disables Assign Badge button when no users are selected', () => { + renderComponent(); + expect(screen.getByText('Assign Badge')).toBeDisabled(); }); - it('displays the count of badges selected', () => { + it('enables Assign Badge button when users are selected', async () => { renderComponent(); - expect(screen.getByText('2 badges selected')).toBeInTheDocument(); + const input = screen.getByPlaceholderText('Full Name'); + fireEvent.change(input, { target: { value: 'Jerry' } }); + + const userRows = (await screen.findAllByRole('row')).slice(1); // Exclude header row + fireEvent.click(userRows[0]); + + expect(screen.getByText('Assign Badge')).toBeEnabled(); }); }); diff --git a/src/components/Badge/__tests__/AssignTableRow.test.jsx b/src/components/Badge/__tests__/AssignTableRow.test.jsx index 0d3d5f4c02..41a3ec73fe 100644 --- a/src/components/Badge/__tests__/AssignTableRow.test.jsx +++ b/src/components/Badge/__tests__/AssignTableRow.test.jsx @@ -10,7 +10,7 @@ vi.mock('react-redux', async importOriginal => { }); import * as reactRedux from 'react-redux'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import AssignTableRow from '~/components/Badge/AssignTableRow'; import { Provider } from 'react-redux'; import { configureStore } from 'redux-mock-store'; @@ -65,7 +65,7 @@ describe('AssignTableRow', () => { ); }); - it('dispatches REMOVE_SELECT_BADGE when unchecking a checked box', () => { + it('dispatches REMOVE_SELECT_BADGE when unchecking a checked box', async () => { const dispatch = vi.fn(); reactRedux.useDispatch.mockReturnValue(dispatch); @@ -74,7 +74,7 @@ describe('AssignTableRow', () => { renderComponent({ badge: defaultBadge, index: 0 }); const cb = screen.getByRole('checkbox'); - expect(cb).toBeChecked(); + await waitFor(() => expect(cb).toBeChecked()); fireEvent.click(cb); expect(dispatch).toHaveBeenCalledWith( diff --git a/src/components/Badge/__tests__/BadgeManagement.test.jsx b/src/components/Badge/__tests__/BadgeManagement.test.jsx index 534ad15cd3..56548b2317 100644 --- a/src/components/Badge/__tests__/BadgeManagement.test.jsx +++ b/src/components/Badge/__tests__/BadgeManagement.test.jsx @@ -92,7 +92,8 @@ describe('BadgeManagement validateBadges action', () => { expect(actions).toContainEqual({ type: GET_MESSAGE, - message: 'The Name Find function does not work without entering a name. Nice try though.', + message: + 'The Name Find function does not work without entering first and last name. Nice try though.', color: 'danger', }); // since setTimeout → immediate, CLOSE_ALERT is already dispatched @@ -110,16 +111,19 @@ describe('BadgeManagement assignBadges action', () => { beforeEach(() => { store = mockStore({}); + vi.useFakeTimers(); }); afterEach(() => { + vi.runOnlyPendingTimers(); + vi.useRealTimers(); vi.clearAllMocks(); store.clearActions(); }); it('dispatches error + CLOSE_ALERT when no badges selected', async () => { await store.dispatch(assignBadges('John', 'Doe', [])); + vi.runAllTimers(); const actions = store.getActions(); - expect(actions).toContainEqual({ type: GET_MESSAGE, message: @@ -132,6 +136,7 @@ describe('BadgeManagement assignBadges action', () => { it('dispatches error + CLOSE_ALERT when user not found', async () => { axios.get.mockResolvedValue({ data: [] }); await store.dispatch(assignBadges('John', 'Doe', ['badge1'])); + vi.runAllTimers(); const actions = store.getActions(); expect(actions).toContainEqual({ @@ -144,9 +149,12 @@ describe('BadgeManagement assignBadges action', () => { }); it('dispatches success + CLOSE_ALERT on successful assign', async () => { - axios.get.mockResolvedValue({ data: [{ badgeCollection: [], _id: 'user1' }] }); - axios.put.mockResolvedValue({}); + axios.get + .mockResolvedValueOnce({ data: [{ badgeCollection: [], _id: 'user1' }] }) + .mockResolvedValueOnce({ data: { badgeCollection: [], _id: 'user1' } }); + axios.put.mockResolvedValue({}); // Mock successful PUT request await store.dispatch(assignBadges('John', 'Doe', ['badge1'])); + vi.runAllTimers(); const actions = store.getActions(); expect(actions).toContainEqual({ @@ -162,6 +170,7 @@ describe('BadgeManagement assignBadges action', () => { axios.get.mockResolvedValue({ data: [{ badgeCollection: [], _id: 'user1' }] }); axios.put.mockRejectedValue(new Error('API Error')); await store.dispatch(assignBadges('John', 'Doe', ['badge1'])); + vi.runAllTimers(); const actions = store.getActions(); expect(actions).toContainEqual({ diff --git a/src/components/CommunityPortal/CPDashboard.jsx b/src/components/CommunityPortal/CPDashboard.jsx index 1e13f76809..dd01ea1e0e 100644 --- a/src/components/CommunityPortal/CPDashboard.jsx +++ b/src/components/CommunityPortal/CPDashboard.jsx @@ -1,13 +1,40 @@ import { useState, useEffect } from 'react'; import { Container, Row, Col, Card, CardBody, Button, Input } from 'reactstrap'; +import { FaCalendarAlt, FaMapMarkerAlt, FaUserAlt, FaSearch, FaTimes } from 'react-icons/fa'; import styles from './CPDashboard.module.css'; -import { FaCalendarAlt, FaMapMarkerAlt, FaUserAlt } from 'react-icons/fa'; import { ENDPOINTS } from '../../utils/URL'; import axios from 'axios'; +const FixedRatioImage = ({ src, alt, fallback }) => ( +
+ {alt} { + if (e.currentTarget.src !== fallback) e.currentTarget.src = fallback; + }} + style={{ + width: '100%', + height: '100%', + objectFit: 'cover', + display: 'block', + }} + /> +
+); + export function CPDashboard() { const [events, setEvents] = useState([]); - const [search, setSearch] = useState(''); + const [searchInput, setSearchInput] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [pagination, setPagination] = useState({ @@ -20,32 +47,6 @@ export function CPDashboard() { const FALLBACK_IMG = 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=600&q=60'; - const FixedRatioImage = ({ src, alt, fallback }) => ( -
- {alt} { - if (e.currentTarget.src !== fallback) e.currentTarget.src = fallback; - }} - style={{ - width: '100%', - height: '100%', - objectFit: 'cover', - display: 'block', - }} - /> -
- ); - useEffect(() => { const fetchEvents = async () => { setIsLoading(true); @@ -53,7 +54,11 @@ export function CPDashboard() { try { const response = await axios.get(ENDPOINTS.EVENTS); console.log('Fetched events:', response.data.events); - setEvents(response.data.events); + setEvents(response.data.events || []); + setPagination(prev => ({ + ...prev, + total: response.data.events?.length || 0, + })); } catch (err) { console.error('Here', err); setError('Failed to load events'); @@ -65,6 +70,20 @@ export function CPDashboard() { fetchEvents(); }, []); + const handleSearchClick = () => { + const trimmed = searchInput.trim(); + setSearchQuery(trimmed); + setPagination(prev => ({ ...prev, currentPage: 1 })); + }; + + const handleSearchKeyDown = e => { + if (e.key === 'Enter') { + const trimmed = searchInput.trim(); + setSearchQuery(trimmed); + setPagination(prev => ({ ...prev, currentPage: 1 })); + } + }; + const formatDate = dateStr => { if (!dateStr) return 'Date TBD'; const date = new Date(dateStr); @@ -77,30 +96,84 @@ export function CPDashboard() { }); }; - const filteredEvents = events.filter(event => - event.title?.toLowerCase().includes(search.toLowerCase()), - ); + const filteredEvents = events.filter(event => { + if (!searchQuery) return true; + const term = searchQuery.toLowerCase(); + + return ( + event.title?.toLowerCase().includes(term) || + event.location?.toLowerCase().includes(term) || + event.organizer?.toLowerCase().includes(term) + ); + }); - const totalPages = Math.ceil(filteredEvents.length / pagination.limit); + const totalPages = Math.ceil(filteredEvents.length / pagination.limit) || 1; const displayedEvents = filteredEvents.slice( (pagination.currentPage - 1) * pagination.limit, pagination.currentPage * pagination.limit, ); + const goToPage = newPage => { + if (newPage < 1 || newPage > totalPages) return; + setPagination(prev => ({ ...prev, currentPage: newPage })); + }; + + if (isLoading) { + return ( + +

Loading events...

+
+ ); + } + + if (error) { + return ( + +

{error}

+
+ ); + } + return (

All Events

+
setSearch(e.target.value)} - className={styles['dashboard-search']} + value={searchInput} + onChange={e => setSearchInput(e.target.value)} + onKeyDown={handleSearchKeyDown} + className={styles['dashboard-search-input']} /> + + {searchInput && ( + + )} + +
@@ -122,24 +195,28 @@ export function CPDashboard() { +
Online Only
+
+
+
@@ -152,22 +229,23 @@ export function CPDashboard() {
-
{index + 1}
-
- {(canPutProject || canSeeProjectManagementFullFunctionality) ? ( - - - - ) : ( - projectName - )} - - - {canEditCategoryAndStatus || canPutProject ? ( - - - ) : ( - category - )} - - {props.active ? ( */} - - {isActive ? ( -
- -
- ) : ( -
- -
- )} -
- - - - - - - -
+
{index + 1}
+
+ {canPutProject || canSeeProjectManagementFullFunctionality ? ( + + ) : ( + projectName + )} + + {canEditCategoryAndStatus || canPutProject ? ( + + ) : ( + category + )} + + {isActive ? ( +
+
+ + + + + + - - - + - // - //
- -
)} diff --git a/src/components/TeamMemberTasks/style.module.css b/src/components/TeamMemberTasks/style.module.css index efc16bd720..53fe1866b2 100644 --- a/src/components/TeamMemberTasks/style.module.css +++ b/src/components/TeamMemberTasks/style.module.css @@ -11,9 +11,21 @@ overflow: hidden; } +.progress-wrapper { + display: inline-block; +} +.team-task-progress-container { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: wrap; +} - +.team-task-progress-bar { + width: 100%; + margin-top: 4px; +} .team-member-task-review-button { @@ -491,13 +503,6 @@ /* min-width: 150px; */ } -.team-task-progress-container { - display: flex; - flex-direction: row; - align-items: center; - flex-wrap: wrap; -} - .progress-text{ margin: 0; /* border: green 1px solid; */ @@ -522,17 +527,6 @@ align-self: center; } -.team-task-progress-bar { - /* width: 60%; - max-width: 180px; - min-width: 100px; */ - align-self: flex-start; - margin-top: 4px; - height: 10px; - border-radius: 4px; -} - - .hours-btn-div { display: flex; flex-direction: row; diff --git a/src/components/Teams/Team.jsx b/src/components/Teams/Team.jsx index 59283fc09a..f2b2a888b4 100644 --- a/src/components/Teams/Team.jsx +++ b/src/components/Teams/Team.jsx @@ -5,6 +5,7 @@ import { Button } from 'reactstrap'; import hasPermission from '~/utils/permissions'; import { boxStyle, boxStyleDark } from '~/styles'; import { DELETE } from '../../languages/en/ui'; +import headerStyles from './TeamTableHeader.module.css'; export function Team(props) { const darkMode = useSelector(state => state.theme.darkMode); @@ -17,7 +18,7 @@ export function Team(props) {
{(props.index ?? 0) + 1}
{/* Wrap long names vertically */} - +
diff --git a/src/components/TotalOrgSummary/TotalOrgSummary.module.css b/src/components/TotalOrgSummary/TotalOrgSummary.module.css index c19a368002..eb9443fdd4 100644 --- a/src/components/TotalOrgSummary/TotalOrgSummary.module.css +++ b/src/components/TotalOrgSummary/TotalOrgSummary.module.css @@ -1,15 +1,10 @@ +/* Chart labels stay dark in dark mode for readability against light label chips */ .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper text, .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper tspan { fill: #000 !important; color: #000 !important; text-shadow: 1px 1px 3px rgba(0,0,0,0.25), 0 0 2px #fff; } -/* Chart title stays white, but chart numbers/labels inside the donut graph are black for better contrast */ -.containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper text, -.containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer .recharts-wrapper tspan { - fill: #000 !important; - color: #000 !important; -} /* Chart and graph titles/text should be white in dark mode for visibility */ .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer h3, .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer p, @@ -73,11 +68,6 @@ box-shadow: 0 4px 12px rgba(0, 123, 255, 0.4) !important; } -.containerTotalOrgWrapper:global(.bg-oxford-blue) .componentBorder { - background-color: #1c2541 !important; - border: none !important; -} - .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentContainer { background-color: #1c2541 !important; border: none !important; @@ -279,25 +269,6 @@ } /* Dark mode dropdown consistency */ -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-toggle) { - background-color: #6f42c1 !important; - border-color: #6f42c1 !important; -} - -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-menu) { - background-color: #1c2541 !important; - border-color: #6f42c1 !important; -} - -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-item) { - background-color: #1c2541 !important; - color: #fff !important; -} - -.containerTotalOrgWrapper:global(.bg-oxford-blue) .reportHeaderActions :global(.dropdown-item):hover { - background-color: #6f42c1 !important; -} - /* Component containers - Clean borderless design */ .componentContainer { margin: 15px 0; @@ -315,6 +286,10 @@ background-color: #fff; overflow: hidden; } + +.componentBorderLoose { + overflow: visible; +} .containerTotalOrgWrapper:global(.bg-oxford-blue) .componentBorder { background-color: #1c2541 !important; border: 1.5px solid #2f4157 !important; diff --git a/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.jsx b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.jsx new file mode 100644 index 0000000000..f4d36a9a61 --- /dev/null +++ b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.jsx @@ -0,0 +1,109 @@ +import PropTypes from 'prop-types'; +import { Doughnut } from 'react-chartjs-2'; +import { Chart, ArcElement } from 'chart.js'; +import styles from './MentorStatusPieChart.module.css'; +import externalLabelGuidesPlugin from './externalLabelGuidesPlugin'; + +Chart.register(ArcElement); + +function MentorStatusPieChart({ + data: { totalMentors, percentageChange, data: mentorData }, + comparisonType, +}) { + const chartData = { + labels: mentorData.map(item => item.label), + datasets: [ + { + data: mentorData.map(item => item.value), + backgroundColor: ['#287D5A', '#2D9DA6', '#F26B38'], + borderWidth: 1, + }, + ], + }; + + const options = { + plugins: { + datalabels: { + display: false, + }, + legend: { + display: false, + }, + tooltip: { + enabled: false, + }, + externalLabelGuides: { + offset: 20, + horizontalSpread: 32, + horizontalSpreadMap: { 0: 32, 1: 46, 2: 5 }, + verticalOffsetMap: { 0: 34, 1: -20, 2: -46 }, + sideMap: { 0: 1, 1: -1, 2: 1 }, + total: totalMentors, + formatter: ({ value, percentage }) => [`${value}`, `(${percentage}%)`], + }, + }, + maintainAspectRatio: false, + cutout: '60%', + layout: { + padding: 20, + }, + }; + + const percentageChangeColor = percentageChange >= 0 ? 'green' : 'red'; + + return ( +
+
+ +
+

TOTAL MENTORS

+

{totalMentors}

+ {comparisonType !== 'No Comparison' && ( +

+ {percentageChange >= 0 + ? `+${percentageChange}% ${comparisonType.toUpperCase()}` + : `${percentageChange}% ${comparisonType.toUpperCase()}`} +

+ )} +
+
+
+ {mentorData.map((item, index) => ( +
+
+ ))} +
+
+ ); +} + +MentorStatusPieChart.propTypes = { + data: PropTypes.shape({ + totalMentors: PropTypes.number.isRequired, + percentageChange: PropTypes.number.isRequired, + data: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, + }), + ).isRequired, + }).isRequired, + comparisonType: PropTypes.string.isRequired, +}; + +export default MentorStatusPieChart; diff --git a/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.module.css b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.module.css new file mode 100644 index 0000000000..abf0772c15 --- /dev/null +++ b/src/components/TotalOrgSummary/VolunteerStatus/MentorStatusPieChart.module.css @@ -0,0 +1,106 @@ +.mentorStatusContainer { + margin-top: 24px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + width: 100%; + height: 100%; + gap: 12px; +} + +.mentorStatusChart { + position: relative; + width: min(320px, 100%); + max-width: 320px; + aspect-ratio: 1 / 1; + overflow: visible; +} + +.mentorStatusCenter { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + font-size: 14px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.mentorStatusHeading { + color: #828282; + font-size: 1.2rem; + letter-spacing: 0.5px; + text-transform: uppercase; + transform: translateY(6px); + width: 100%; + text-align: center; +} + +.mentorCount { + color: #6c6c6c; + font-size: 1.8rem; + font-weight: 800; + line-height: 1.18; + margin-top: -10px; +} + +.mentorPercentageChange { + font-weight: 600; +} + +.mentorStatusLabels { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + margin-top: 48px; + margin-bottom: 24px; + flex-wrap: nowrap; + padding: 0 20px; + width: max-content; + max-width: 100%; + box-sizing: border-box; +} + +.mentorStatusLabel { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.875rem; + white-space: nowrap; +} + +.mentorStatusColor { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; +} + +:global(.bg-oxford-blue) .mentorStatusHeading { + color: #f1f5ff; +} + +:global(.bg-oxford-blue) .mentorCount { + color: #ffffff; +} + +@media (max-width: 768px) { + .mentorStatusChart { + width: min(280px, 100%); + } + + .mentorStatusLabels { + gap: 12px; + } +} + +@media (min-width: 768px) { + .mentorStatusChart { + max-width: 280px; + } +} diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx index ed885a04c8..7748f8e143 100644 --- a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx +++ b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.jsx @@ -2,9 +2,16 @@ import { useMemo } from 'react'; import PropTypes from 'prop-types'; import Loading from '~/components/common/Loading'; import VolunteerStatusPieChart from './VolunteerStatusPieChart'; +import MentorStatusPieChart from './MentorStatusPieChart'; +import styles from './VolunteerStatusChart.module.css'; -function VolunteerStatusChart({ isLoading, volunteerNumberStats, comparisonType }) { - const chartData = useMemo(() => { +function VolunteerStatusChart({ + isLoading, + volunteerNumberStats, + mentorNumberStats, + comparisonType, +}) { + const volunteerChartData = useMemo(() => { if (!volunteerNumberStats) { return null; } @@ -42,8 +49,43 @@ function VolunteerStatusChart({ isLoading, volunteerNumberStats, comparisonType }; }, [volunteerNumberStats]); + const mentorChartData = useMemo(() => { + if (!mentorNumberStats) { + return null; + } + + const { + donutChartData, + activeMentors, + deactivatedMentors, + newMentors, + totalMentors, + } = mentorNumberStats; + + let chartDataValues; + if (donutChartData && donutChartData.existingActive !== undefined) { + chartDataValues = [ + { label: 'Existing Active', value: donutChartData.existingActive.count }, + { label: 'New Active', value: donutChartData.newActive.count }, + { label: 'Deactivated', value: donutChartData.deactivated.count }, + ]; + } else { + chartDataValues = [ + { label: 'Active', value: activeMentors.count }, + { label: 'New', value: newMentors.count }, + { label: 'Deactivated This Week', value: deactivatedMentors.count }, + ]; + } + + return { + totalMentors: totalMentors.count, + percentageChange: Number(totalMentors.comparisonPercentage) || 0, + data: chartDataValues, + }; + }, [mentorNumberStats]); + return ( -
+
{isLoading ? (
@@ -51,7 +93,28 @@ function VolunteerStatusChart({ isLoading, volunteerNumberStats, comparisonType
) : ( - + <> +
+
+ {volunteerChartData && ( + + )} +
+ {mentorChartData && ( +
+ +
+ )} +
+ {(volunteerChartData || mentorChartData) && ( +

+ *Does not include the “Mentor” members shown in the graph to the right. +

+ )} + )}
); @@ -86,6 +149,32 @@ VolunteerStatusChart.propTypes = { comparisonPercentage: PropTypes.number, }), }), + mentorNumberStats: PropTypes.shape({ + donutChartData: PropTypes.shape({ + existingActive: PropTypes.shape({ + count: PropTypes.number, + }), + newActive: PropTypes.shape({ + count: PropTypes.number, + }), + deactivated: PropTypes.shape({ + count: PropTypes.number, + }), + }), + activeMentors: PropTypes.shape({ + count: PropTypes.number, + }), + newMentors: PropTypes.shape({ + count: PropTypes.number, + }), + deactivatedMentors: PropTypes.shape({ + count: PropTypes.number, + }), + totalMentors: PropTypes.shape({ + count: PropTypes.number, + comparisonPercentage: PropTypes.number, + }), + }), }; export default VolunteerStatusChart; diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.module.css b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.module.css new file mode 100644 index 0000000000..4968093f95 --- /dev/null +++ b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusChart.module.css @@ -0,0 +1,69 @@ +.chartRoot { + margin-top: 1.5rem; + height: 100%; + display: flex; + flex-direction: column; +} + +.volunteerMentorChartsWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 24px; + width: 100%; + flex: 1 1 auto; +} + +.volunteerChartSection, +.mentorChartSection { + width: 100%; + display: flex; + justify-content: center; + align-items: stretch; + flex: 1 1 100%; + min-width: 0; +} + +@media (min-width: 768px) { + .volunteerMentorChartsWrapper { + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + justify-content: center; + } + + .volunteerChartSection, + .mentorChartSection { + flex: 1 1 280px; + max-width: 360px; + } + + .chartRoot { + min-height: 520px; + } +} + +@media (min-width: 1200px) { + .volunteerMentorChartsWrapper { + flex-wrap: nowrap; + justify-content: space-evenly; + } + + .volunteerChartSection { + flex: 1 1 340px; + } + + .mentorChartSection { + flex: 0 1 320px; + } +} + +.volunteerMentorFootnote { + margin-top: 105px; + text-align: center; + font-size: 0.75rem; + color: #4f4f4f; + max-width: 560px; + align-self: center; +} diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.css b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.css deleted file mode 100644 index da4fed0d08..0000000000 --- a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.css +++ /dev/null @@ -1,62 +0,0 @@ -.volunteer-status-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; -} - -.volunteer-status-chart { - position: relative; - width: 100%; - max-width: 400px; - height: 400px; -} - -.volunteer-status-center { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - font-size: 14px; -} - -.volunteer-status-center .volunteer-status-heading { - color: #828282; - font-size: 1.2rem; -} - -.volunteer-status-center .volunteer-count { - color: #6c6c6c; - font-size: 2rem; - font-weight: bolder; -} - -.volunteer-status-center > p { - font-weight: bold; -} - -.volunteer-status-center div { - margin: 2px 0; -} - -.volunteer-status-labels { - display: flex; - justify-content: center; - margin-top: 20px; - margin-bottom: 50px; -} - -.volunteer-status-label { - display: flex; - align-items: center; - margin: 0 10px; -} - -.volunteer-status-color { - display: inline-block; - width: 12px; - height: 12px; - margin-right: 5px; -} diff --git a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx index bf3f82c9d8..54c8f10286 100644 --- a/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx +++ b/src/components/TotalOrgSummary/VolunteerStatus/VolunteerStatusPieChart.jsx @@ -1,8 +1,8 @@ import PropTypes from 'prop-types'; import { Doughnut } from 'react-chartjs-2'; -import ChartDataLabels from 'chartjs-plugin-datalabels'; import { Chart, ArcElement } from 'chart.js'; -import './VolunteerStatusPieChart.css'; +import styles from './VolunteerStatusPieChart.module.css'; +import externalLabelGuidesPlugin from './externalLabelGuidesPlugin'; Chart.register(ArcElement); @@ -26,21 +26,8 @@ function VolunteerStatusPieChart({ const options = { plugins: { datalabels: { - color: '#000', - font: { - size: 20, - weight: 'bolder', - lineHeight: 1.8, - }, - formatter: function(value, context) { - const percentage = ((value / totalVolunteers) * 100).toFixed(0); - // Show value and percent as two lines for clarity - return [`${value}`, `(${percentage}%)`]; - }, - display: true, - offset: 0, - align: 'center', - anchor: 'center', + // Hide in-slice labels because values are already shown with external guides. + display: false, }, legend: { display: false, @@ -48,23 +35,39 @@ function VolunteerStatusPieChart({ tooltip: { enabled: false, }, + externalLabelGuides: { + offset: 20, + horizontalSpread: 34, + horizontalSpreadMap: { 0: 34, 1: 48, 2: 5 }, + verticalOffsetMap: { 0: 38, 1: -22, 2: -50 }, + sideMap: { 0: 1, 1: -1, 2: 1 }, + total: totalVolunteers, + formatter: ({ value, percentage }) => [`${value}`, `(${percentage}%)`], + }, }, maintainAspectRatio: false, cutout: '55%', + layout: { + padding: 24, + }, }; const percentageChangeColor = percentageChange >= 0 ? 'green' : 'red'; return ( -
-
- -
-

TOTAL VOLUNTEERS

-

{totalVolunteers}

+
+
+ +
+

TOTAL VOLUNTEERS*

+

{totalVolunteers}

{comparisonType !== 'No Comparison' && (

@@ -75,11 +78,11 @@ function VolunteerStatusPieChart({ )}

-
+
{volunteerData.map((item, index) => ( -
+
{jobTitle}
diff --git a/src/utils/URL.js b/src/utils/URL.js index 45ea4834a1..80b699104c 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -147,6 +147,7 @@ export const ENDPOINTS = { APPLICANT_VOLUNTEER_RATIO: `${APIEndpoint}/applicant-volunteer-ratio`, USER_UNREAD_TASK_NOTIFICATIONS: userId => `${APIEndpoint}/tasknotification/user/${userId}`, BADGE: () => `${APIEndpoint}/badge`, + BADGE_ASSIGN_MULTIPLE: `${APIEndpoint}/badge/assign`, BADGE_ASSIGN: userId => `${APIEndpoint}/badge/assign/${userId}`, BADGE_BY_ID: badgeId => `${APIEndpoint}/badge/${badgeId}`, diff --git a/yarn.lock b/yarn.lock index 4937082d4b..6530f96a47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2074,12 +2074,21 @@ resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== dependencies: - "@jest/schemas" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" @@ -5178,6 +5187,13 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, d dependencies: ms "^2.1.3" +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -7355,20 +7371,15 @@ jest-message-util@30.2.0: slash "^3.0.0" stack-utils "^2.0.6" -jest-message-util@^29.7.0: +jest-matcher-utils@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.6.3" - "@types/stack-utils" "^2.0.0" chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" pretty-format "^29.7.0" - slash "^3.0.0" - stack-utils "^2.0.3" jest-mock@30.2.0: version "30.2.0" @@ -7379,14 +7390,20 @@ jest-mock@30.2.0: "@types/node" "*" jest-util "30.2.0" -jest-mock@^29.7.0: +jest-message-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== dependencies: + "@babel/code-frame" "^7.12.13" "@jest/types" "^29.6.3" - "@types/node" "*" - jest-util "^29.7.0" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" jest-pnp-resolver@^1.2.3: version "1.2.3" @@ -9876,6 +9893,14 @@ ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: hash-base "^3.1.2" inherits "^2.0.4" +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + robust-predicates@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" @@ -10244,6 +10269,14 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
-
+
showUpDeleteModal()} - style={darkMode ? boxStyleDark : boxStyle} > Delete diff --git a/src/components/Projects/WBS/WBSDetail/Task/Task.jsx b/src/components/Projects/WBS/WBSDetail/Task/Task.jsx index 1f9d262f2a..2082b1a833 100644 --- a/src/components/Projects/WBS/WBSDetail/Task/Task.jsx +++ b/src/components/Projects/WBS/WBSDetail/Task/Task.jsx @@ -9,7 +9,7 @@ import { deleteChildrenTasks, } from '../../../../../actions/task.js'; import './tagcolor.css'; -import './task.css'; +import styles from './task.module.css'; import '../../../../Header/index.css' import { Editor } from '@tinymce/tinymce-react'; import { getPopupById } from './../../../../../actions/popupEditorAction'; @@ -206,12 +206,12 @@ function Task(props) { id={`r_${props.num}_${props.taskId}`} // eslint-disable-next-line jsx-a11y/scope scope="row" - className={`taskNum ${props.hasChildren ? 'has_children' : ''} text-left`} + className={`taskNum ${props.hasChildren ? styles.has_children : ''} ${styles.textLeft}`} onClick={openChild} > {props.num.replaceAll('.0', '')} -
+ {
{props.hasChildren ? ( @@ -245,45 +245,56 @@ function Task(props) { ) : null}
- {props.resources.length - ? props.resources - .filter((elm, i) => i < 2 || showMoreResources) - .map((elm, i) => { - const name = elm.name; //Getting initials and formatting them here - const initials = getInitials(name); - //getting background color here - const bg = colors_objs[name].color; - return ( - - {!elm.profilePic || elm.profilePic === '/defaultprofilepic.png' ? ( - - {initials}{' '} - - ) : ( - // eslint-disable-next-line jsx-a11y/alt-text - - )} - - ); - }) - : null} + {props.resources.length + ? props.resources + .filter((elm, i) => i < 2 || showMoreResources) + .map((elm, i) => { + const name = elm.name; + const initials = getInitials(name); + const bg = colors_objs[name].color; + return ( + + {!elm.profilePic || elm.profilePic === '/defaultprofilepic.png' ? ( + + {initials} + + ) : ( + {elm.name} + )} + + ); + }) + : null} + {props.resources.length > 2 ? ( - // eslint-disable-next-line jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions - setShowMoreResources(!showMoreResources)} + aria-label={ + showMoreResources + ? 'Show fewer assigned resources' + : `Show ${props.resources.length - 2} more assigned resources` + } > - + {showMoreResources ? : `${props.resources.length - 2}+`} - + ) : null} diff --git a/src/components/Projects/WBS/WBSDetail/Task/task.css b/src/components/Projects/WBS/WBSDetail/Task/task.css deleted file mode 100644 index 8c4ae1c184..0000000000 --- a/src/components/Projects/WBS/WBSDetail/Task/task.css +++ /dev/null @@ -1,54 +0,0 @@ -.has_children { - cursor: pointer; - font-weight: bold; -} - -.task-link { - word-break: break-all; -} - - -.remove-link { - margin-right: 5px; - color: red; - cursor: pointer; -} - -.disable-div { - pointer-events: none; -} - -@media only screen and (max-width: 600px) { - .container-tasks tbody tr:nth-child(8n + 0) { - background-color: rgb(255, 212, 212); - } - - .container-tasks tbody tr:nth-child(8n + 1) { - background-color: rgb(255, 252, 213); - } - - .container-tasks tbody tr:nth-child(8n + 2) { - background-color: rgb(222, 255, 254); - } - .container-tasks tbody tr:nth-child(8n + 3) { - background-color: rgb(230, 227, 255); - } - - .container-tasks tbody tr:nth-child(8n + 4) { - background-color: rgb(255, 235, 255); - } - - .container-tasks tbody tr:nth-child(8n + 5) { - background-color: rgb(245, 255, 228); - } - - .container-tasks tbody tr:nth-child(8n + 6) { - background-color: rgb(228, 255, 236); - } - .container-tasks tbody tr:nth-child(8n + 7) { - background-color: rgb(228, 255, 255); - } -} -.text-left { - text-align: left !important; -} \ No newline at end of file diff --git a/src/components/Projects/WBS/WBSDetail/Task/task.module.css b/src/components/Projects/WBS/WBSDetail/Task/task.module.css new file mode 100644 index 0000000000..64b1a28496 --- /dev/null +++ b/src/components/Projects/WBS/WBSDetail/Task/task.module.css @@ -0,0 +1,89 @@ +.has_children { + cursor: pointer; + font-weight: bold; +} + +.taskLink { + word-break: break-all; +} + +.removeLink { + margin-right: 5px; + color: red; + cursor: pointer; +} + +.disableDiv { + pointer-events: none; +} + +@media only screen and (max-width: 600px) { + :global(.container-tasks) tbody tr:nth-child(8n + 0) { + background-color: rgb(255, 212, 212); + } + + :global(.container-tasks) tbody tr:nth-child(8n + 1) { + background-color: rgb(255, 252, 213); + } + + :global(.container-tasks) tbody tr:nth-child(8n + 2) { + background-color: rgb(222, 255, 254); + } + + :global(.container-tasks) tbody tr:nth-child(8n + 3) { + background-color: rgb(230, 227, 255); + } + + :global(.container-tasks) tbody tr:nth-child(8n + 4) { + background-color: rgb(255, 235, 255); + } + + :global(.container-tasks) tbody tr:nth-child(8n + 5) { + background-color: rgb(245, 255, 228); + } + + :global(.container-tasks) tbody tr:nth-child(8n + 6) { + background-color: rgb(228, 255, 236); + } + + :global(.container-tasks) tbody tr:nth-child(8n + 7) { + background-color: rgb(228, 255, 255); + } +} + +.textLeft { + text-align: left !important; +} + + +.name { + display: inline-block; + margin-right: 4px; +} + +.dot { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + font-size: 12px; + font-weight: 600; + color: #ffffff; + text-transform: uppercase; +} + +.imgCircle { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + margin-right: 4px; +} + +.resourceMoreToggle { + display: inline-block; + margin-left: 4px; + cursor: pointer; +} diff --git a/src/components/Projects/projects.css b/src/components/Projects/projects.module.css similarity index 61% rename from src/components/Projects/projects.css rename to src/components/Projects/projects.module.css index 51fd4b61f8..a15f1f3a5c 100644 --- a/src/components/Projects/projects.css +++ b/src/components/Projects/projects.module.css @@ -1,105 +1,71 @@ -thead { +:global(thead) { background: aliceblue; } -#new_project { +:global(#new_project) { margin-bottom: 10px; } -#projects__order { +:global(#projects__order) { width: 5px; } -#projects__active { +:global(#projects__active) { width: 10px; } -#projects__category { +:global(#projects__category) { width: 15px; } -#projects__members { +:global(#projects__members) { width: 15px; } -#projects__inv { +:global(#projects__inv) { width: 15px; } -#projects__wbs { +:global(#projects__wbs) { width: 15px; } -#projects__delete { +:global(#projects__delete) { width: 20px; } -.projects__tr { - text-align: center; -} - +/* Keep overview layout global for now (likely used in Overview.jsx) */ /* .projects__overview--top { background: gray; } */ -.projects__overview--top .card { +:global(.projects__overview--top .card) { width: 200px; float: left; margin-right: 10px; margin-bottom: 10px; } -#card_project { +:global(#card_project) { background: #1d62f0; color: white; } -#card_active { +:global(#card_active) { background: #fbad4c; color: white; } -.projects__order--input div { - margin-top: 7px; -} - -.projects__active--input i { - margin-top: 10px; - cursor: pointer; -} - -.projects__name--input input { - border: 1px solid white; -} - -.projects__name--input input:hover { - border: 1px solid #dee2e6; -} - -.isActive i { - color: lawngreen; -} - -.isNotActive { - color: #dee2e6; -} - -.isDisabled { - opacity: 0; - transition: opacity 10ms; - display: none; -} - -.project_info_modal_ol { +:global(.project_info_modal_ol) { padding-left: 20px; } -.search-project-person { +:global(.search-project-person) { position: relative; width: 100%; } -.suggestions-list { +:global(.suggestions-list) { position: absolute; background-color: white; border: 1px solid #ccc; @@ -110,36 +76,71 @@ thead { padding: 0; } -.suggestion-item { +:global(.suggestion-item) { padding: 8px; cursor: pointer; } -.suggestion-item:hover { +:global(.suggestion-item:hover) { background-color: #f0f0f0; } -.suggestion-button { +:global(.suggestion-button) { display: block; - width: 100%; - padding: 10px; - text-align: left; - background: none; - border: none; - cursor: pointer; + width: 100%; + padding: 10px; + text-align: left; + background: none; + border: none; + cursor: pointer; } -.suggestion-button:hover { +:global(.suggestion-button:hover) { background-color: #f0f0f0; } -tr:hover { +:global(tr:hover) { background: #e9f6ff; } @media (max-width: 375px) { - #new_project .form-control { + :global(#new_project .form-control) { flex: 1 1 auto; width: 50%; } } + +.projects__tr { + text-align: center; +} + +.projects__order--input div { + margin-top: 7px; +} + +.projects__active--input i { + margin-top: 10px; + cursor: pointer; +} + +.projects__name--input input { + border: 1px solid white; +} + +.projects__name--input input:hover { + border: 1px solid #dee2e6; +} + +.isActive i { + color: lawngreen; +} + +.isNotActive { + color: #dee2e6; +} + +.isDisabled { + opacity: 0; + transition: opacity 10ms; + display: none; +} diff --git a/src/components/TeamMemberTasks/TeamMemberTask.jsx b/src/components/TeamMemberTasks/TeamMemberTask.jsx index 6ed49be836..359a0a1693 100644 --- a/src/components/TeamMemberTasks/TeamMemberTask.jsx +++ b/src/components/TeamMemberTasks/TeamMemberTask.jsx @@ -595,7 +595,7 @@ const TeamMemberTask = React.memo( darkMode ? 'bg-yinmn-blue text-light' : '' }`} > - <> +
- +
{props.name}{props.name} +