diff --git a/src/assets/images/masterMap.png b/src/assets/images/masterMap.png new file mode 100644 index 0000000000..8a1d4eede0 Binary files /dev/null and b/src/assets/images/masterMap.png differ diff --git a/src/assets/images/pin-point.png b/src/assets/images/pin-point.png new file mode 100644 index 0000000000..f45502ee38 Binary files /dev/null and b/src/assets/images/pin-point.png differ diff --git a/src/assets/images/routeMarker.png b/src/assets/images/routeMarker.png new file mode 100644 index 0000000000..c1c1758056 Binary files /dev/null and b/src/assets/images/routeMarker.png differ diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.css b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.css index 7c72a77a0f..b0ea400745 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.css @@ -297,4 +297,89 @@ .weekly-project-summary-dashboard-grid { grid-template-columns: 1fr; } -} \ No newline at end of file +} + +/* ---------------- STATUS CARD ---------------- */ +.status-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 25px; + width: 100%; + max-width: 284px; + height: 190px; + text-align: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + padding: 20px; + border: 1px solid rgba(0, 0, 0, 0.1); + position: relative; +} + +/* ---------------- RESPONSIVE GRID LAYOUT ---------------- */ +.project-status-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 20px; + justify-content: center; + align-items: center; + width: 100%; + max-width: 1600px; + margin: auto; +} + +/* ---------------- OVAL STATUS BUTTON ---------------- */ +.weekly-status-button { + width: 130px; + height: 65px; + border-radius: 50px / 32px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + margin: 12px 0; +} + +.weekly-card-title { + color: #000; + font-size: 24px; + font-weight: 600; +} + +.weekly-status-value { + color: #000; + font-size: 40px; + font-weight: 600; +} + + +/* ---------------- RESPONSIVE BREAKPOINTS ---------------- */ +@media (min-width: 1600px) { + .project-status-grid { + grid-template-columns: repeat(6, 1fr); + } +} + +@media (max-width: 1400px) { + .project-status-grid { + grid-template-columns: repeat(4, 1fr); + } +} + +@media (max-width: 1024px) { + .project-status-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 768px) { + .project-status-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 576px) { + .project-status-grid { + grid-template-columns: repeat(1, 1fr); + } +} diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index 6ab959ecdc..f2ffadd946 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -4,6 +4,105 @@ import { useSelector } from 'react-redux'; import { v4 as uuidv4 } from 'uuid'; import WeeklyProjectSummaryHeader from './WeeklyProjectSummaryHeader'; +const projectStatusButtons = [ + { + title: 'Total Projects', + value: 426, + change: '+16% week over week', + bgColor: '#F0FFEE', + buttonColor: '#BAF0B6', + textColor: '#328D1B', + }, + { + title: 'Completed Projects', + value: 127, + change: '+14% week over week', + bgColor: '#F3FCFF', + buttonColor: '#C1EFFB', + textColor: '#328D1B', + }, + { + title: 'Delayed Projects', + value: 34, + change: '-18% week over week', + bgColor: '#FFE9FA', + buttonColor: '#FECFF3', + textColor: '#C82F2F', + }, + { + title: 'Active Projects', + value: 265, + change: '+3% week over week', + bgColor: '#E8E8FF', + buttonColor: '#CBCBFE', + textColor: '#328D1B', + }, + { + title: 'Avg Project Duration', + value: '17 hrs', + change: '+13% week over week', + bgColor: '#FFF6EE', + buttonColor: '#FFD8A5', + textColor: '#FFD8A5', + }, + { + title: 'Total Material Cost', + value: '$27.6K', + change: '+9% week over week', + bgColor: '#FFF3F3', + buttonColor: '#FBC1C2', + textColor: '#328D1B', + }, + { + title: 'Total Material Used', + value: '2714', + change: '+11% week over week', + bgColor: '#DAC8FF', + buttonColor: '#B28ECC', + textColor: '#328D1B', + }, + { + title: 'Active Projects', + value: '265', + change: '+3% week over week', + bgColor: '#E8E8FF', + buttonColor: '#CBCBFE', + textColor: '#328D1B', + }, + { + title: 'Total Labor Hours Invested', + value: '12.8K', + change: '+17% week over week', + bgColor: '#E5C1FC', + buttonColor: '#F6E1FB', + textColor: '#328D1B', + }, + { + title: 'Total Labor Cost', + value: '$18.4K', + change: '+14% week over week', + bgColor: '#FFFDF3', + buttonColor: '#FBF9C1', + textColor: '#328D1B', + }, + { + title: 'Material Available', + value: 693, + change: '-8% week over week', + bgColor: '#B4D9C5', + buttonColor: '#31BD41', + textColor: '#C82F2F', + }, + { + title: 'Material Wasted', + value: 879, + change: '+14% week over week', + bgColor: '#EFBABB', + buttonColor: '#F79395', + textColor: '#328D1B', + }, +]; + export default function WeeklyProjectSummary() { const [openSections, setOpenSections] = useState({}); @@ -23,11 +122,27 @@ export default function WeeklyProjectSummary() { className: 'full', content: (
- {Array.from({ length: 12 }).map(() => { + {projectStatusButtons.map(button => { const uniqueId = uuidv4(); return ( -
- 📊 Card +
+
{button.title}
+
+ {button.value} +
+
+ {button.change} +
); })} diff --git a/src/components/Badge/AssignBadge.jsx b/src/components/Badge/AssignBadge.jsx index 5e5d2f30af..f463e5e88a 100644 --- a/src/components/Badge/AssignBadge.jsx +++ b/src/components/Badge/AssignBadge.jsx @@ -214,12 +214,7 @@ function AssignBadge(props) { > Assign Badge - + Assign Badge diff --git a/src/components/Badge/BadgeReport.jsx b/src/components/Badge/BadgeReport.jsx index 48d4d24883..2d50758334 100644 --- a/src/components/Badge/BadgeReport.jsx +++ b/src/components/Badge/BadgeReport.jsx @@ -84,54 +84,68 @@ function BadgeReport(props) { }; } - const FormatReportForPdf = (badges, callback) => { - const bgReport = []; - bgReport[0] = `

Badge Report (Page 1 of ${Math.ceil(badges.length / 4)})

-

For ${props.firstName} ${ - props.lastName - }

-
_______________________________________________________________________________________________
`; - - for (let i = 0; i < badges.length; i += 1) { - imageToUri(badges[i].badge.imageUrl, function(uri) { - bgReport[i + 1] = ` - - - - - - - - - - - - - -
Badge ImageBadge Name, Count Awarded & Badge Description
-
-
-
Name: ${badges[i].badge.badgeName}
-
Count: ${badges[i].count}
-
Description: ${badges[i].badge.description}
-
- ${ - (i + 1) % 4 === 0 && i + 1 !== badges.length - ? `


-

Badge Report (Page ${1 + Math.ceil((i + 1) / 4)} of ${Math.ceil(badges.length / 4)})

-

For ${props.firstName} ${ - props.lastName - }

-
_______________________________________________________________________________________________
- ` - : '' - }`; - if (i === badges.length - 1) { - setTimeout(() => { - callback(bgReport.join('\n')); - }, 100); - } + const FormatReportForPdf = async (badges, callback) => { + try { + const bgReport = []; + bgReport[0] = `

Badge Report (Page 1 of ${Math.ceil(badges.length / 4)})

+

For ${props.firstName} ${ + props.lastName + }

+
_______________________________________________________________________________________________
`; + + const badgePromises = badges.map((badge, i) => { + const imageUrl = badge.badge?.imageUrl || ''; // Fallback to empty string if imageUrl is missing + const badgeName = badge.badge?.badgeName || 'Unknown Badge'; // Fallback for missing badgeName + const description = badge.badge?.description || 'No description available'; // Fallback for missing description + + return new Promise((resolve) => { + imageToUri(imageUrl, (uri) => { + const badgeHtml = ` + + + + + + + + + + + + + +
Badge ImageBadge Name, Count Awarded & Badge Description
+
+
+
Name: ${badgeName}
+
Count: ${badge.count}
+
Description: ${description}
+
+ ${ + (i + 1) % 4 === 0 && i + 1 !== badges.length + ? `


+

Badge Report (Page ${1 + Math.ceil((i + 1) / 4)} of ${Math.ceil( + badges.length / 4 + )})

+

For ${props.firstName} ${ + props.lastName + }

+
_______________________________________________________________________________________________
+ ` + : '' + }`; + resolve(badgeHtml); + }); + }); }); + + const badgeHtmlArray = await Promise.all(badgePromises); + bgReport.push(...badgeHtmlArray); + + callback(bgReport.join('\n')); + } catch (error) { + console.error('Error generating badge report:', error); + callback('

Error generating badge report. Please try again later.

'); } }; diff --git a/src/components/CommunityPortal/EventPersonalization/EventStats.css b/src/components/CommunityPortal/EventPersonalization/EventStats.css new file mode 100644 index 0000000000..d7d381d460 --- /dev/null +++ b/src/components/CommunityPortal/EventPersonalization/EventStats.css @@ -0,0 +1,222 @@ +.popular-events-container { + + max-height: 100%; + margin: 0 auto; + padding: 20px; + font-family: Arial, sans-serif; + background: white; +} + +.popular-events-container-dark { + + margin: 0 auto; + padding: 20px; + font-family: Arial, sans-serif; + background: #1B2A41; + min-height: 100%; +} + +.header-container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-container-dark { + display: flex; + justify-content: space-between; + align-items: center; + color: #1C2541; +} + +.popular-events-header { + font-size: 1.5rem; + color: #000000; + margin: 0; +} + +.popular-events-header-dark { + font-size: 1.5rem; + color: #ffffff; + margin: 0; + background-color: #1C2541; +} + +.filters { + display: flex; + gap: 10px; +} + +.filters-dark { + display: flex; + gap: 10px; + background-color: #1C2541; +} + +.filters select { + padding: 6px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 5px; + cursor: pointer; +} + +.stats { + display: flex; + flex-direction: column; + gap: 20px; + border-radius: 8px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); + padding: 30px; +} + +.stats-dark { + display: flex; + flex-direction: column; + gap: 20px; + border-radius: 8px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); + padding: 30px; + background-color: #1C2541; +} + +.stat-item { + display: flex; + align-items: center; + justify-content: space-between; +} + + + +.stat-label { + flex: 1; + font-size: 14px; + font-weight: bold; + color: #333; +} + +.stat-label-dark { + flex: 1; + font-size: 14px; + font-weight: bold; + color: #ffffff; +} + +.stat-bar { + flex: 3; + background: #f0f0f0; + height: 8px; + border-radius: 5px; + margin: 0 10px; + position: relative; + overflow: hidden; +} + +.bar { + height: 100%; + border-radius: 5px; + transition: width 0.5s ease-in-out; +} + +.bar.green { + background-color: #4caf50; +} + +.bar.orange { + background-color: #ff9800; +} + +.bar.red { + background-color: #f44336; +} + +.stat-value { + flex: 1; + text-align: right; + font-size: 14px; + font-weight: bold; + color: #333; +} + +.stat-value-dark { + flex: 1; + text-align: right; + font-size: 14px; + font-weight: bold; + color: #ffffff; +} + +.summary { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + max-height: 80px; + margin-top: 20px; + border-radius: 8px; + text-align: center; + padding-bottom: 20px; +} + +.summary-item { + background: white; + padding: 12px; + border-radius: 8px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + max-height: 90px; + min-height: 80px; + overflow: hidden; + text-align: center; +} + +.summary-item-dark { + background: #3A506B; + padding: 12px; + border-radius: 8px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + max-height: 90px; + min-height: 80px; + overflow: hidden; + text-align: center; +} + +.summary-title { + font-size: 14px; + font-weight: bold; + text-align: center; + white-space: normal; + word-wrap: break-word; + max-width: 90%; + color: #777; +} + +.summary-title-dark { + font-size: 14px; + font-weight: bold; + text-align: center; + white-space: normal; + word-wrap: break-word; + max-width: 90%; + color: #ffffff; +} + +.summary-value { + font-size: 14px; + font-weight: bold; + color: #000; + margin-top: 4px; +} + +.summary-value-dark { + font-size: 14px; + font-weight: bold; + color: #ffffff; + margin-top: 4px; +} \ No newline at end of file diff --git a/src/components/CommunityPortal/EventPersonalization/EventStats.jsx b/src/components/CommunityPortal/EventPersonalization/EventStats.jsx new file mode 100644 index 0000000000..1a61567909 --- /dev/null +++ b/src/components/CommunityPortal/EventPersonalization/EventStats.jsx @@ -0,0 +1,174 @@ +import { useState } from 'react'; +import './EventStats.css'; +import { useSelector } from 'react-redux'; + +const dummyData = [ + { + id: 1, + type: 'Type of Event 1', + attended: 20, + enrolled: 25, + time: 'Morning', + location: 'Offline', + }, + { + id: 2, + type: 'Type of Event 2', + attended: 19, + enrolled: 20, + time: 'Afternoon', + location: 'Online', + }, + { + id: 3, + type: 'Type of Event 3', + attended: 12, + enrolled: 18, + time: 'Night', + location: 'Offline', + }, + { + id: 4, + type: 'Type of Event 4', + attended: 11, + enrolled: 20, + time: 'Morning', + location: 'Online', + }, + { + id: 5, + type: 'Type of Event 5', + attended: 8, + enrolled: 20, + time: 'Afternoon', + location: 'Offline', + }, + { id: 6, type: 'Type of Event 6', attended: 7, enrolled: 22, time: 'Night', location: 'Offline' }, + { + id: 7, + type: 'Type of Event 7', + attended: 4, + enrolled: 20, + time: 'Morning', + location: 'Online', + }, +]; + +export default function PopularEvents() { + const [timeFilter, setTimeFilter] = useState('All day'); + const [typeFilter, setTypeFilter] = useState('All'); + + const calculatePercentage = (attended, enrolled) => Math.round((attended / enrolled) * 100); + + const getBarColor = percentage => { + if (percentage > 60) return 'green'; + if (percentage > 40) return 'orange'; + return 'red'; + }; + + const filteredData = dummyData.filter(event => { + const timeMatch = timeFilter === 'All day' || event.time === timeFilter; + const typeMatch = typeFilter === 'All' || event.location === typeFilter; + return timeMatch && typeMatch; + }); + + const mostPopularEvent = filteredData.reduce( + (max, event) => + calculatePercentage(event.attended, event.enrolled) > + calculatePercentage(max.attended, max.enrolled) + ? event + : max, + filteredData[0] || {}, + ); + + const leastPopularEvent = filteredData.reduce( + (min, event) => + calculatePercentage(event.attended, event.enrolled) < + calculatePercentage(min.attended, min.enrolled) + ? event + : min, + filteredData[0] || {}, + ); + const darkMode = useSelector(state => state.theme.darkMode); + return ( +
+
+

+ Most Popular Event +

+
+ + +
+
+ +
+ {filteredData.map(event => ( +
+
{event.type}
+
+
+
+
+ {`${calculatePercentage(event.attended, event.enrolled)}% (${event.attended}/${ + event.enrolled + })`} +
+
+ ))} +
+
+
+
+ Total Number of Events +
+
+ {filteredData.length} +
+
+
+
+ Total Number of Event Enrollments +
+
+ {filteredData.reduce((acc, event) => acc + event.enrolled, 0)} +
+
+ {filteredData.length > 0 && ( + <> +
+
+ Most Popular Event +
+
+ {mostPopularEvent.type || 'N/A'} +
+
+
+
+ Least Popular Event +
+
+ {leastPopularEvent.type || 'N/A'} +
+
+ + )} +
+
+ ); +} diff --git a/src/components/Dashboard/Dashboard.jsx b/src/components/Dashboard/Dashboard.jsx index d80719396b..881e92bbe0 100644 --- a/src/components/Dashboard/Dashboard.jsx +++ b/src/components/Dashboard/Dashboard.jsx @@ -31,7 +31,7 @@ export function Dashboard(props) { const dispatch = useDispatch(); - const toggle = (forceOpen = null) => { + const toggle = () => { if (isNotAllowedToEdit) { const warningMessage = viewingUser?.email === DEV_ADMIN_ACCOUNT_EMAIL_DEV_ENV_ONLY @@ -41,8 +41,7 @@ export function Dashboard(props) { return; } - const shouldOpen = forceOpen !== null ? forceOpen : !popup; - setPopup(shouldOpen); + setPopup(!popup); setTimeout(() => { const elem = document.getElementById('weeklySum'); diff --git a/src/components/LBDashboard/ListingOverview/ImageCarousel.css b/src/components/LBDashboard/ListingOverview/ImageCarousel.css deleted file mode 100644 index 105f599755..0000000000 --- a/src/components/LBDashboard/ListingOverview/ImageCarousel.css +++ /dev/null @@ -1,68 +0,0 @@ -.carousel-container { - position: relative; - width: 100%; - height: 100%; - max-width: 100%; - margin: auto; - overflow: hidden; -} - -.carousel-wrapper { - width: 100%; - overflow: hidden; -} - -.carousel-track { - display: flex; - transition: transform 0.5s ease-in-out; -} - -.carousel-image { - width: 100%; - height: 100%; - max-width: 100%; - flex-shrink: 0; - object-fit: cover; -} - -.carousel-image-arrow { - position: absolute; - top: 50%; - transform: translateY(-50%); - background-color: rgba(0, 0, 0, 0.5); - color: white; - border: none; - padding: 10px; - border-radius: 50%; - cursor: pointer; - z-index: 10; -} - -.carousel-image-arrow.left { - left: 10px; -} - -.carousel-image-arrow.right { - right: 10px; -} - -.carousel-indicators { - display: flex; - justify-content: center; - margin-top: 5%; - bottom: 2% !important; -} - -.indicator { - width: 10px; - height: 10px; - margin: 0 5px; - background-color: #bbb; - border-radius: 50%; - cursor: pointer; - transition: background-color 0.3s ease; -} - -.indicator.active { - background-color: #333; -} \ No newline at end of file diff --git a/src/components/LBDashboard/ListingOverview/ImageCarousel.jsx b/src/components/LBDashboard/ListingOverview/ImageCarousel.jsx deleted file mode 100644 index 5d01e924d9..0000000000 --- a/src/components/LBDashboard/ListingOverview/ImageCarousel.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useState } from 'react'; -import './ImageCarousel.css'; - -export default function ImageCarousel({ images }) { - const [currentIndex, setCurrentIndex] = useState(0); - - if (!images || images.length === 0) return

No images available

; - - const handleNext = () => { - setCurrentIndex(prev => (prev + 1) % images.length); - }; - - const handlePrev = () => { - setCurrentIndex(prev => (prev - 1 + images.length) % images.length); - }; - - const handleIndicatorClick = index => { - setCurrentIndex(index); - }; - - return ( -
-
-
- {images.map((image, index) => ( - {`Slide - ))} -
-
- - -
- {images.map((image, index) => ( - handleIndicatorClick(index)} - /> - ))} -
-
- ); -} diff --git a/src/components/LBDashboard/ListingOverview/ListOverview.jsx b/src/components/LBDashboard/ListingOverview/ListOverview.jsx index 4266f55b29..1b63e0248b 100644 --- a/src/components/LBDashboard/ListingOverview/ListOverview.jsx +++ b/src/components/LBDashboard/ListingOverview/ListOverview.jsx @@ -1,8 +1,8 @@ import React, { useEffect } from 'react'; import './Listoverview.css'; +import Carousel from 'react-bootstrap/Carousel'; import logo from '../../../assets/images/logo2.png'; import mapIcon from '../../../assets/images/mapIcon.png'; -import ImageCarousel from './ImageCarousel'; function ListOverview() { const [listing, setListing] = React.useState({}); @@ -37,12 +37,19 @@ function ListOverview() {

{listing.title}

- + + {listing.images?.map((image, index) => ( + + {`Slide + + ))} +
+

Available amenities in this unit:

-
    +
      {listing.unitAmenities?.map(amenity => (
    1. {amenity}
    2. ))} @@ -50,7 +57,7 @@ function ListOverview() {

Village level amenities:

-
    +
      {listing.villageAmenities?.map(amenity => (
    1. {amenity}
    2. ))} diff --git a/src/components/LBDashboard/ListingOverview/Listoverview.css b/src/components/LBDashboard/ListingOverview/Listoverview.css index 8659816399..1aab927298 100644 --- a/src/components/LBDashboard/ListingOverview/Listoverview.css +++ b/src/components/LBDashboard/ListingOverview/Listoverview.css @@ -75,6 +75,15 @@ margin-bottom: 2.5%; } +.image-carousel img { + width: 100%; + height: auto; + object-fit: cover; + max-width: 100%; + flex-shrink: 0; +} + + .amenities { display: flex; justify-content: center; @@ -96,12 +105,12 @@ padding-left: 5%; } -ol { +.amenities-list { display: flex; flex-direction: column; list-style-position: inside; } -li { +.amenities-list li { margin: 0; } diff --git a/src/components/LBDashboard/Map/MasterPlan/MasterPlan.css b/src/components/LBDashboard/Map/MasterPlan/MasterPlan.css new file mode 100644 index 0000000000..dd4adc9921 --- /dev/null +++ b/src/components/LBDashboard/Map/MasterPlan/MasterPlan.css @@ -0,0 +1,239 @@ +.main-container { + display: flex; + flex-direction: column; + align-items: center; + background-color: #e0e0e0; + width: 100%; + min-height: 100vh; + padding: 20px; + overflow: hidden; +} + +.logo-container { + text-align: center; + margin-bottom: 20px; +} + +.logo-container img { + max-width: 30%; + height: auto; + min-width: 250px; +} +.content-container { + display: flex; + flex-direction: column; + background-color: white; + border: 1px solid #ccc; + border-radius: 8px; + width: 85%; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + height: max-content; +} + +.container-top { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + height: 60px; + background-color: #9dd425; +} + +.container-main { + background-color: #e0e0e0; + display: flex; + flex-direction: column; + margin: 20px 40px 30px 40px !important; + padding: 20px 20px 50px 20px !important; + width: auto; + min-height: 560px; + height: 100%; +} + +.container-map { + display: flex; + height: 100%; + width: 100%; + flex-direction: column; + justify-content: space-evenly; + align-items: center; + background-color: #839a4a; +} + +.map-details { + width: 100%; + height: 75%; + display: flex; +} +.pin-point { + position: absolute; + width: 5.5% !important; + height: 6% !important; + z-index: 9; + top: calc(var(--top) * 580 / 700 - 3%); + left: calc(var(--left) - 1%); +} + + +.map { + width: 65%; + display: flex; + justify-content: center; + align-items: center; + padding: 0; +} + +.image-wrapper { + position: relative; + height: 100%; + display: inline-block; +} + +.image-wrapper img { + height: 100%; + width: 100%; + max-width: none; +} + +.village-marker { + border-radius: 50%; + position: absolute; + cursor: pointer; + opacity: 0; + width: 3.5%; + height: 4%; + z-index: 10; + top: calc(var(--top) * 580 / 700); + left: calc(var(--left)); +} + + +.route { + width: 35%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 10px 0; + border-left: solid 1px #e0e0e0; +} + +.route p { + font-size: 0.5em; + margin-top: 0; + margin-bottom: 0; + font-weight: 600; + text-align: center; + color: #e0e0e0; +} + +.route img { + width: 100%; + height: 100%; +} + +.villages { + width: 100%; + height: 25%; + display: flex; + justify-content: flex-start; + align-items: center; + overflow-x: scroll; +} + +.village { + display: flex; + height: 100%; + padding: 5px; + align-items: center; +} + +.village:hover { + background-color: #768b43; +} + +.village img { + width: auto; + height: 75%; + cursor: pointer; +} + +.selected { + background-color: #768b43; +} + + +@media (max-width: 990px) { + .main-container { + padding: 0; + } + .container-main { + flex-direction: column; + align-items: center; + } + .map-details { + flex-direction: column; + height: 100%; + } + .map { + width: 100%; + } + + .image-wrapper { + position: relative; + min-height: 100%; + display: inline-block; + } + .image-wrapper img { + width: 100%; + height: auto; + max-width: none; + } + .route { + width: 100%; + border-left: none; + border-top: solid 1px #e0e0e0; + } + .villages{ + height: auto; + } + +} + +.village-details{ + display: flex; + flex-direction: column; + padding-left: 20px; + width: 100%; + height: 100%; +} + +.village-details h3{ + margin: 0; + padding: 0; + font-size: 1.5em; + font-weight: 600; + color:#839a4a; +} + +.village-details p{ + margin: 0; + padding: 0; + font-size: 1em; + font-weight: 400; + color:#839a4a; +} + + +@media (max-width: 580px) { + .container-main { + padding: 10px; + margin: 10px; + } + .village-details{ + padding-left: 0; + } + +} + diff --git a/src/components/LBDashboard/Map/MasterPlan/MasterPlan.jsx b/src/components/LBDashboard/Map/MasterPlan/MasterPlan.jsx new file mode 100644 index 0000000000..c5f9c8485d --- /dev/null +++ b/src/components/LBDashboard/Map/MasterPlan/MasterPlan.jsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +import { useHistory } from 'react-router'; +import logo from '../../../../assets/images/logo2.png'; +import mastermap from '../../../../assets/images/masterMap.png'; +import mapRouter from '../../../../assets/images/routeMarker.png'; +import pin from '../../../../assets/images/pin-point.png'; +import './MasterPlan.css'; + +const villages = [ + { + id: 0, + name: 'Duplicable City Center', + short: 'CC', + description: + 'The Duplicable City Center will be the largest open source/DIY structure in the world. As part of One Community it will be a diversely functional, ultra-eco-friendly (LEED Platinum Certifiable), space and resource saving community center designed to be replicated.', + url: + 'https://onecommunityglobal.org/wp-content/uploads/2018/02/Duplicable-City-Center-PlanRender_640x335.jpg', + position: { top: '48%', left: '49.75%' }, + }, + { + id: 1, + name: 'Earthbag Village', + short: 'Earthbag', + description: + 'The Earthbag Village consists of seventy-eight 150-200 square foot (14-18.6 sq meter) earthbag hotel room styled cabanas plus four communal eco-shower structures, 2 vermiculture waste processing toilet structures, two net-zero water use toilet structures, and the central Tropical Atrium.', + url: + 'https://onecommunityglobal.org/wp-content/uploads/2018/10/Earthbag-Village-640x335-render.jpg', + position: { top: '45%', left: '41.1%' }, + }, + { + id: 2, + name: 'Straw Bale Village', + short: 'Straw', + description: + 'The Straw Bale Village consists of fifty-two 250-300 square foot (23-28 sq meters) studio-style rooms, each with an attached bathroom. They are arranged in groups of 4 that can easily be connected and or converted to create multi-room units.', + url: + 'https://onecommunityglobal.org/wp-content/uploads/2011/09/Straw-Bale-Village-PlanRender_640x335-1.png', + position: { top: '75.75%', left: '68%' }, + }, + { + id: 3, + name: 'Cob Village', + description: + 'Cob is an ancient building material composed of dirt, straw, and water that may have been used for construction since prehistoric times. Some of the oldest man-made structures in Afghanistan are composed of rammed earth and cob and still standing! ', + short: 'Cob', + url: + 'https://onecommunityglobal.org/wp-content/uploads/2011/09/Cob-Village-PlanRender_640x335.png', + position: { top: '99.5%', left: '9%' }, + }, + { + id: 4, + name: 'Earth Block Village', + short: 'Block', + description: + 'Compressed earth blocks (CEBs) or pressed earth blocks are damp soil compressed at high pressure to form blocks. If the blocks are stabilized with a chemical binder such as Portland Cement they are called compressed stabilized earth blocks (CSEBs) or stabilized earth blocks (SEBs).', + url: 'https://onecommunityglobal.org/wp-content/uploads/2015/02/P4-Plan-Render_640x335.jpg', + position: { top: '112.5%', left: '75%' }, + }, + { + id: 5, + name: 'Shipping Container Village', + short: 'Container', + description: + 'The Shipping Container Village is planned as a semi-subterranean 3-level village constructed using shipping containers. It will provide 36 living units and 18 additional common spaces.', + url: + 'https://onecommunityglobal.org/wp-content/uploads/2011/09/Shipping-Container-Village-PlanRender_640x335.jpg', + position: { top: '107.25%', left: '53.75%' }, + }, + { + id: 6, + name: 'Recycled Materials Village', + short: 'Recycle', + description: + 'The Recycled Materials Village (Pod 6) will be open source shared to demonstrate how to build safely, affordably, and efficiently with maximal use of reclaimed/recycled materials. The design of the Recycled Materials Village is an earthship-inspired semi-subterranean design that will provide 47 living units and 14 common areas.', + url: + 'https://onecommunityglobal.org/wp-content/uploads/2018/06/Recycled-Materials-Village-PlanRender_640x335-updated.jpg', + position: { top: '50.5%', left: '66.75%' }, + }, + { + id: 7, + name: 'Tree House Village', + short: 'Treehouse', + description: + 'The Tree House Village will be a community living model in the trees. It plans to show how a Tree House Village, or off-ground and low-footprint/low-impact housing, can be a viable approach to sustainable living.', + url: + 'https://onecommunityglobal.org/wp-content/uploads/2014/01/Tree-House-Village-PlanRender_640x335.jpg', + position: { top: '68.25%', left: '93%' }, + }, +]; +function MasterPlan() { + const [selectedVillage, setSelectedVillage] = useState(null); + const router = useHistory(); + + const handleVillageClick = village => { + setSelectedVillage(village); + if (selectedVillage === village) { + router.push(`/master-plan/${village.id}`); + } + }; + + const handleOutsideClick = () => { + setSelectedVillage(null); + }; + + return ( +
      +
      + One Community Logo +
      +
      +
      +
      +
      +
      +
      +
      + Master Map + {villages.map(v => ( +
      +
      +
      + Route Marker +

      + Click on the village marker or on the village to select a village and view more + details. +

      +

      Double Click to view the village Page.

      +
      +
      +
      + {villages.map(v => ( +
      { + e.stopPropagation(); + handleVillageClick(v); + }} + > + {v.name} +
      + ))} +
      +
      +
      + {selectedVillage && ( +
      +

      {selectedVillage.name}

      +

      {selectedVillage.description}

      +
      + )} +
      +
      +
      +
      + ); +} + +export default MasterPlan; diff --git a/src/components/LeaderBoard/Leaderboard.css b/src/components/LeaderBoard/Leaderboard.css index 4a21612922..8077dfa23c 100644 --- a/src/components/LeaderBoard/Leaderboard.css +++ b/src/components/LeaderBoard/Leaderboard.css @@ -14,6 +14,11 @@ border-top-style: solid; border-top-color: rgb(222, 226, 230); } + +.leaderboard tbody tr td, thead tr th { + text-align: left !important; +} + .dark-leaderboard-row { background-color: #3a506b; color: white; diff --git a/src/components/LeaderBoard/Leaderboard.jsx b/src/components/LeaderBoard/Leaderboard.jsx index 7a321b27b7..4dfa58f523 100644 --- a/src/components/LeaderBoard/Leaderboard.jsx +++ b/src/components/LeaderBoard/Leaderboard.jsx @@ -34,7 +34,7 @@ import { boxStyle } from 'styles'; import axios from 'axios'; import { getUserProfile } from 'actions/userProfile'; import { useDispatch } from 'react-redux'; -import { boxStyleDark } from 'styles'; +import { boxStyleDark } from '../../styles'; import '../Header/DarkMode.css'; import '../UserProfile/TeamsAndProjects/autoComplete.css'; import { ENDPOINTS } from '../../utils/URL'; @@ -845,18 +845,6 @@ function LeaderBoard({ > - - - - - - )}
      diff --git a/src/components/Projects/Project/Project.jsx b/src/components/Projects/Project/Project.jsx index 5d3ae6f75b..384dab9cd2 100644 --- a/src/components/Projects/Project/Project.jsx +++ b/src/components/Projects/Project/Project.jsx @@ -17,20 +17,8 @@ const Project = props => { const [projectData, setProjectData] = useState(props.projectData); const { projectName, isActive,isArchived, _id: projectId } = projectData; const [displayName, setDisplayName] = useState(projectName); - const initialModalData = { - showModal: false, - modalMessage: "", - modalTitle: "", - hasConfirmBtn: false, - hasInactiveBtn: false, - }; - - const [modalData, setModalData] = useState(initialModalData); - - const onCloseModal = () => { - setModalData(initialModalData); - props.clearError(); - }; const [category, setCategory] = useState(props.category || 'Unspecified'); // Initialize with props or default + + const [category, setCategory] = useState(props.category || 'Unspecified'); // Initialize with props or default const canPutProject = props.hasPermission('putProject'); const canDeleteProject = props.hasPermission('deleteProject'); @@ -71,13 +59,7 @@ const Project = props => { }; const onArchiveProject = () => { - setModalData({ - showModal: true, - modalMessage: `

      Do you want to archive ${projectData.projectName}?

      `, - modalTitle: CONFIRM_ARCHIVE, - hasConfirmBtn: true, - hasInactiveBtn: isActive, - }); + props.onClickArchiveBtn(projectData); } const setProjectInactive = () => { @@ -211,16 +193,6 @@ const Project = props => { ) : null} - - ); }; diff --git a/src/components/Projects/Project/Project.test.jsx b/src/components/Projects/Project/Project.test.jsx index 1e6cd3aa40..59ae0b63a9 100644 --- a/src/components/Projects/Project/Project.test.jsx +++ b/src/components/Projects/Project/Project.test.jsx @@ -84,24 +84,17 @@ describe('Project Component', () => { }); it('triggers delete action on button click', () => { - const { getByTestId } = renderProject(sampleProps); - - // Find the delete button and click it + const mockOnClickArchiveBtn = jest.fn(); + const { getByTestId } = renderProject({ + ...sampleProps, + onClickArchiveBtn: mockOnClickArchiveBtn, + }); + const deleteButton = getByTestId('delete-button'); fireEvent.click(deleteButton); - - // Check if the modal is triggered - const modal = document.querySelector('.modal'); - expect(modal).toBeInTheDocument(); - - const archiveButton=screen.getAllByText('Archive')[0]; - fireEvent.click(archiveButton); - - expect(screen.getByText('Confirm Archive')).toBeInTheDocument(); - expect(screen.getByText(`Do you want to archive ${sampleProjectData.projectName}?`)).toBeInTheDocument(); - - const closeButton=screen.getByText('Close') - fireEvent.click(closeButton) - expect(screen.queryByText('Confirm Archive')).not.toBeInTheDocument(); + + expect(mockOnClickArchiveBtn).toHaveBeenCalledWith(expect.objectContaining({ + _id: sampleProjectData._id, + })); }); }); diff --git a/src/components/Projects/Projects.jsx b/src/components/Projects/Projects.jsx index dcd41481b8..ddc4ced5d5 100644 --- a/src/components/Projects/Projects.jsx +++ b/src/components/Projects/Projects.jsx @@ -48,6 +48,8 @@ const Projects = function(props) { const [searchName, setSearchName] = useState(""); const [allProjects, setAllProjects] = useState(null); + const [isArchiving, setIsArchiving] = useState(false); + const useDebounce = (value, delay) => { const [debouncedValue, setDebouncedValue] = useState(value); @@ -104,9 +106,11 @@ const Projects = function(props) { }; const confirmArchive = async () => { + setIsArchiving(true); // show loading on confirm const updatedProject = { ...projectTarget, isArchived: true }; await onUpdateProject(updatedProject); await props.fetchAllProjects(); + setIsArchiving(false); // reset loading onCloseModal(); }; @@ -251,6 +255,8 @@ const Projects = function(props) { modalMessage={modalData.modalMessage} modalTitle={modalData.modalTitle} darkMode={darkMode} + confirmButtonText={isArchiving ? 'Archiving...' : 'Confirm'} + isConfirmDisabled={isArchiving} />
diff --git a/src/components/Projects/WBS/AddWBS/AddWBS.jsx b/src/components/Projects/WBS/AddWBS/AddWBS.jsx index ca55d143d4..f2a6235d0c 100644 --- a/src/components/Projects/WBS/AddWBS/AddWBS.jsx +++ b/src/components/Projects/WBS/AddWBS/AddWBS.jsx @@ -9,18 +9,22 @@ import { addNewWBS } from './../../../../actions/wbs'; import hasPermission from 'utils/permissions'; const AddWBS = props => { - const [showAddButton, setShowAddButton] = useState(false); + const darkMode = props.state.theme.darkMode; const [newName, setNewName] = useState(''); + const [showAddButton, setShowAddButton] = useState(false); const canPostWBS = props.hasPermission('postWbs'); - const { darkMode } = props.state.theme; - const changeNewName = newName => { - if (newName.length !== 0) { - setShowAddButton(true); - } else { + const changeNewName = value => { + setNewName(value); + setShowAddButton(value.length >= 3); + }; + + const handleAddWBS = () => { + if (newName.length >= 3) { + props.addNewWBS(props.projectId, newName); + setNewName(''); setShowAddButton(false); } - setNewName(newName); }; return ( @@ -28,7 +32,7 @@ const AddWBS = props => { {canPostWBS ? (
- Add new WBS + Add new WBS
{ ) : null} + +
) : null} diff --git a/src/components/Projects/WBS/AddWBS/__tests__/AddWBS.test.jsx b/src/components/Projects/WBS/AddWBS/__tests__/AddWBS.test.jsx index 7ec149e5e9..0a3dcaf4c6 100644 --- a/src/components/Projects/WBS/AddWBS/__tests__/AddWBS.test.jsx +++ b/src/components/Projects/WBS/AddWBS/__tests__/AddWBS.test.jsx @@ -66,7 +66,7 @@ describe("AddWBS component structure", () => { }); test("button should not be in the document when the input field is empty", () => { - expect(screen.queryByRole('button')).toBeNull(); + expect(screen.queryByTestId('add-wbs-button')).toBeNull(); }); test("user should be able to type in the input field", () => { @@ -76,7 +76,7 @@ describe("AddWBS component structure", () => { test("button should appear when user types in the input field", () => { typeIntoInput({ input: '123' }); - expect(screen.queryByRole('button')).not.toBeNull(); + expect(screen.queryByTestId('add-wbs-button')).not.toBeNull(); }); }); diff --git a/src/components/Projects/WBS/WBSItem/WBSItem.jsx b/src/components/Projects/WBS/WBSItem/WBSItem.jsx index f7cfded839..49ef4c0d6d 100644 --- a/src/components/Projects/WBS/WBSItem/WBSItem.jsx +++ b/src/components/Projects/WBS/WBSItem/WBSItem.jsx @@ -34,16 +34,16 @@ const WBSItem = ({ darkMode, index, name, wbsId, projectId, getPopupById, delete return ( - -
{index}
+ + {index} - + {name} - {canDeleteWBS ? ( - + + {canDeleteWBS ? ( - - ) : null} + ) : null} + { it('check if deleteWBS html elements get displayed in virtual DOM when the permission is not present', () => { store.getState().auth.user.permissions.frontPermissions = []; const { container } = renderComponent(index, key, wbsId, projectId, name); - expect(container.querySelector('.members__assign')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { class: 'btn btn-outline-danger btn-sm' })).not.toBeInTheDocument(); }); it('check if deleteWBS html elements get displayed in virtual DOM when the permission is present', () => { const { container } = renderComponent(index, key, wbsId, projectId, name); - expect(container.querySelector('.members__assign')).toBeInTheDocument(); + expect(screen.queryByRole('button', { class: 'btn btn-outline-danger btn-sm' })).toBeInTheDocument(); }); it('check if modal opens when button is clicked', async () => { axios.get.mockResolvedValue({ diff --git a/src/components/Projects/WBS/wbs.jsx b/src/components/Projects/WBS/wbs.jsx index 9e75669ff1..eec0e6e7e3 100644 --- a/src/components/Projects/WBS/wbs.jsx +++ b/src/components/Projects/WBS/wbs.jsx @@ -26,11 +26,13 @@ const WBS = props => { }, [projectId]); useEffect(() => { + if (!props.state.wbs.WBSItems) return; + const sortedItems = [...props.state.wbs.WBSItems]; if (sortOrder === 'asc') { - sortedItems.sort((a, b) => a.wbsName.localeCompare(b.wbsName)); + sortedItems.sort((a, b) => a.wbsName.toLowerCase().localeCompare(b.wbsName.toLowerCase())); } else if (sortOrder === 'desc') { - sortedItems.sort((a, b) => b.wbsName.localeCompare(a.wbsName)); + sortedItems.sort((a, b) => b.wbsName.toLowerCase().localeCompare(a.wbsName.toLowerCase())); } else { sortedItems.sort((a, b) => new Date(b.modifiedDatetime) - new Date(a.modifiedDatetime)); } @@ -38,7 +40,7 @@ const WBS = props => { }, [props.state.wbs.WBSItems, sortOrder]); const handleSortChange = (newOrder) => { - setSortOrder(newOrder); + setSortOrder(prevOrder => prevOrder === newOrder ? 'recent' : newOrder); }; return ( @@ -46,52 +48,76 @@ const WBS = props => {
- + handleSortChange('asc')} + onSortDescending={() => handleSortChange('desc')} + /> - - - - - - - - - - {sortedWBSItems.map((item, i) => - item ? ( - - ) : null, - )} - -
- # - - Name - - handleSortChange(sortOrder === 'asc' ? 'desc' : sortOrder === 'desc' ? 'recent' : 'asc')} - > - -
+ {!props.state.wbs.WBSItems ? ( +
+
+ Loading... +
+
+ ) : ( + + + + + + + + + + {sortedWBSItems.map((item, i) => + item ? ( + + ) : null, + )} + +
# + Name + + handleSortChange(sortOrder === 'asc' ? 'desc' : sortOrder === 'desc' ? 'recent' : 'asc')} + > + +
+ )}
diff --git a/src/components/Reports/PeopleTableDetails.jsx b/src/components/Reports/PeopleTableDetails.jsx index da6d00ab24..92a3fe658a 100644 --- a/src/components/Reports/PeopleTableDetails.jsx +++ b/src/components/Reports/PeopleTableDetails.jsx @@ -312,15 +312,15 @@ function PeopleTableDetails(props) {
-
Task
-
Priority
-
Status
-
Resources
-
Active
-
Assign
-
Estimated Hours
-
Start Date
-
End Date
+
Task
+
Priority
+
Status
+
Resources
+
Active
+
Assign
+
Estimated Hours
+
Start Date
+
End Date
{filteredTasks.map(value => ( diff --git a/src/components/Reports/__tests__/PeopleTableDetails.test.js b/src/components/Reports/__tests__/PeopleTableDetails.test.js new file mode 100644 index 0000000000..5e7be06f36 --- /dev/null +++ b/src/components/Reports/__tests__/PeopleTableDetails.test.js @@ -0,0 +1,237 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import PeopleTableDetails from '../PeopleTableDetails'; + +// Mock data for the Test cases +const taskData = [ + { + _id: '1', + taskName: 'Task 1', + priority: 'High', + status: 'Completed', + resources: [[{ name: 'Resource 1', index: 1, profilepic: '' }]], + active: 'Yes', + assign: 'No', + estimatedHours: '5h', + startDate: '2022-01-01', + endDate: '2022-01-10', + }, + { + _id: '2', + taskName: 'Task 2', + priority: 'Low', + status: 'In Progress', + resources: [ + [{ name: 'Resource 2', index: 2, profilepic: '' }], + [{ name: 'Resource 3', index: 3, profilepic: '' }], + ], + active: 'Yes', + assign: 'Yes', + estimatedHours: '10h', + startDate: '2022-02-01', + endDate: '2022-02-10', + }, + { + _id: '3', + taskName: 'Task 3', + priority: 'Medium', + status: 'Not Started', + resources: [[{ name: 'Resource 4', index: 1, profilepic: '' }]], + active: 'No', + assign: 'Yes', + estimatedHours: '8h', + startDate: '2022-03-01', + endDate: '2022-03-15', + }, +]; +describe('PeopleTableDetails component', () => { + it('renders without crashing', () => { + render(); + expect(screen.getByTestId('eh')); + }); + + it('renders all table headers correctly', () => { + render(); + expect(screen.getByTestId('task')); + expect(screen.getByTestId('priority')); + expect(screen.getByTestId('status')); + expect(screen.getByTestId('resources')); + expect(screen.getByTestId('active')); + expect(screen.getByTestId('eh')); + expect(screen.getByTestId('sd')); + expect(screen.getByTestId('ed')); + }); + + it('displays all task data in table rows', () => { + render(); + // Expect to see text from the first task + expect(screen.getByText('Task 1')).toBeInTheDocument(); + expect(screen.getByText('High')).toBeInTheDocument(); + expect(screen.getByText('Completed')).toBeInTheDocument(); + + // Check the second task as well + expect(screen.getByText('Task 2')).toBeInTheDocument(); + expect(screen.getByText('Low')).toBeInTheDocument(); + expect(screen.getByText('In Progress')).toBeInTheDocument(); + + // Check the third task as well + expect(screen.getByText('Task 3')).toBeInTheDocument(); + expect(screen.getByText('Medium')).toBeInTheDocument(); + expect(screen.getByText('Not Started')).toBeInTheDocument(); + }); + + it('handles missing task attributes gracefully', () => { + const tasks = [ + { + _id: '1', + taskName: 'Project 1', + // Missed priority attribute + status: 'Completed', + resources: [[{ name: 'Resource 1', index: 1, profilepic: '' }]], + active: 'Yes', + assign: 'No', + estimatedHours: '5h', + startDate: '2022-01-01', + endDate: '2022-01-10', + }, + { + _id: '2', + taskName: 'Project 2', + priority: 'Low', + // Missed status attribute + resources: [ + [ + { name: 'Resource 2', index: 1, profilepic: '' }, + { name: 'Resource 3', index: 2, profilepic: '' }, + ], + ], + active: 'Yes', + assign: 'Yes', + estimatedHours: '10h', + startDate: '2022-02-01', + endDate: '2022-02-10', + }, + ]; + render(); + const project1Text = screen.queryByText('Project 1'); + expect(project1Text).not.toBeInTheDocument(); + const project2Text = screen.queryByText('Project 2'); + expect(project2Text).not.toBeInTheDocument(); + }); + + it('does not show resource toggle button when there are less than 2 resources', () => { + const tasks = [ + { + _id: '1', + taskName: 'Project 1', + priority: 'High', + status: 'Completed', + resources: [[{ name: 'Resource 1', index: 1, profilepic: '' }]], + active: 'Yes', + assign: 'No', + estimatedHours: '5h', + startDate: '2022-01-01', + endDate: '2022-01-10', + }, + ]; + render(); + + expect(screen.getByText('Project 1')).toBeInTheDocument(); + const toggleButton = screen.queryByText('+'); + expect(toggleButton).not.toBeInTheDocument(); + }); + + it('shows resource toggle button when there are more than 2 resources', () => { + const tasks = [ + { + _id: '1', + taskName: 'Project 2', + priority: 'High', + status: 'Completed', + resources: [ + [ + { name: 'Resource 2', index: 2, profilePic: '' }, + { name: 'Resource 3', index: 3, profilePic: '' }, + { name: 'Resource 1', index: 1, profilePic: '' }, + ], + ], + active: 'Yes', + assign: 'No', + estimatedHours: '5h', + startDate: '2022-01-01', + endDate: '2022-01-10', + }, + ]; + render(); + + expect(screen.getByText('Project 2')).toBeInTheDocument(); + const toggleButton = screen.getByText('1+'); + expect(toggleButton).toBeInTheDocument(); + }); + + it('toggles resource visibility when button is clicked', () => { + const tasks = [ + { + _id: '1', + taskName: 'Project 2', + priority: 'High', + status: 'Completed', + resources: [ + [ + { name: 'Resource 2', index: 2, profilePic: '' }, + { name: 'Resource 3', index: 3, profilePic: '' }, + { name: 'Resource 1', index: 1, profilePic: '' }, + ], + ], + active: 'Yes', + assign: 'No', + estimatedHours: '5h', + startDate: '2022-01-01', + endDate: '2022-01-10', + }, + ]; + + render(); + + const allButtons = screen.getAllByRole('button'); + const toggleButton = allButtons.find(button => + button.classList.contains('resourceMoreToggle') + ); + expect(toggleButton).toBeInTheDocument(); + + const extraDiv = toggleButton.parentElement.querySelector('.extra'); + expect(extraDiv).toBeInTheDocument(); + + fireEvent.click(toggleButton); + expect(extraDiv.style.display).toBe('table-cell'); + + fireEvent.click(toggleButton); + expect(extraDiv.style.display).toBe('none'); + }); + + it('displays correct number of remaining resources', () => { + const tasks = [ + { + _id: '1', + taskName: 'Project 2', + priority: 'High', + status: 'Completed', + resources: [ + [ + { name: 'Resource 2', index: 2, profilepic: '' }, + { name: 'Resource 3', index: 3, profilepic: '' }, + { name: 'Resource 1', index: 1, profilepic: '' }, + { name: 'Resource 4', index: 4, profilepic: '' }, + ], + ], + active: 'Yes', + assign: 'No', + estimatedHours: '5h', + startDate: '2022-01-01', + endDate: '2022-01-10', + }, + ]; + + render(); + expect(screen.getByText('2+')).toBeInTheDocument(); + }); +}); diff --git a/src/components/Teams/Team.jsx b/src/components/Teams/Team.jsx index 242d37b94e..61e7d1d4cf 100644 --- a/src/components/Teams/Team.jsx +++ b/src/components/Teams/Team.jsx @@ -57,6 +57,7 @@ export function Team(props) { props.onEditTeam(props.name, props.teamId, props.active, props.teamCode); }} style={darkMode ? {} : boxStyle} + disabled={!canPutTeam} > Edit @@ -69,6 +70,7 @@ export function Team(props) { props.onDeleteClick(props.name, props.teamId, props.active, props.teamCode); }} style={darkMode ? boxStyleDark : boxStyle} + disabled={!canDeleteTeam} > {DELETE} diff --git a/src/components/UserManagement/SetUpFinalDayButton.jsx b/src/components/UserManagement/SetUpFinalDayButton.jsx index 147583da11..860711266e 100644 --- a/src/components/UserManagement/SetUpFinalDayButton.jsx +++ b/src/components/UserManagement/SetUpFinalDayButton.jsx @@ -5,61 +5,43 @@ import { toast } from 'react-toastify'; import { updateUserFinalDayStatusIsSet } from '../../actions/userManagement'; import { boxStyle, boxStyleDark } from '../../styles'; import SetUpFinalDayPopUp from './SetUpFinalDayPopUp'; -import { SET_FINAL_DAY, CANCEL , PROCESSING } from '../../languages/en/ui'; +import { SET_FINAL_DAY, CANCEL } from '../../languages/en/ui'; import { FinalDay } from '../../utils/enums'; -/** - * @param {*} props - * @param {Boolean} props.isBigBtn - * @param {*} props.userProfile.isSet - * @returns - */ + function SetUpFinalDayButton(props) { - const { darkMode } = props; - const [isSet, setIsSet] = useState(false); + const { darkMode, userProfile, onFinalDaySave } = props; + const [isSet, setIsSet] = useState(!!userProfile.endDate); // Determine if the final day is already set const [finalDayDateOpen, setFinalDayDateOpen] = useState(false); const dispatch = useDispatch(); - const [isLoading, setIsLoading] = useState(false); // Added loading state - - useEffect(() => { - if (props.userProfile?.endDate !== undefined) setIsSet(true); - }, []); - - const onFinalDayClick = async () => { - setIsLoading(true); // Start loading indicator - try { - const activeStatus = props.userProfile.isActive ? 'Active' : 'Inactive'; - if (isSet) { + const handleButtonClick = async () => { + if (isSet) { + // Delete the final day + try { await updateUserFinalDayStatusIsSet( - props.userProfile, - activeStatus, + userProfile, + userProfile.isActive ? 'Active' : 'Inactive', undefined, FinalDay.NotSetFinalDay, )(dispatch); setIsSet(false); - await props.loadUserProfile(); // Ensure state sync + onFinalDaySave({ ...userProfile, endDate: undefined }); toast.success("This user's final day has been deleted."); - } else { - setFinalDayDateOpen(true); + } catch (error) { + console.error('Error deleting final day:', error); + toast.error("An error occurred while deleting the user's final day."); } - } catch (error) { - console.error('Error handling final day click:', error); - toast.error("An error occurred while updating the user's final day."); - } finally { - setIsLoading(false); // Stop loading indicator + } else { + // Open the popup to set the final day + setFinalDayDateOpen(true); } }; - const setUpFinalDayPopupClose = () => { - setFinalDayDateOpen(false); - }; - - const deactiveUser = async finalDayDate => { - setIsLoading(true); // Start loading indicator + const handleSaveFinalDay = async (finalDayDate) => { try { await updateUserFinalDayStatusIsSet( - props.userProfile, + userProfile, 'Active', finalDayDate, FinalDay.FinalDay, @@ -67,13 +49,11 @@ function SetUpFinalDayButton(props) { setIsSet(true); setFinalDayDateOpen(false); - await props.loadUserProfile(); // Ensure state sync + onFinalDaySave({ ...userProfile, endDate: finalDayDate }); toast.success("This user's final day has been set."); } catch (error) { - console.error('Error setting the final day:', error); + console.error('Error setting final day:', error); toast.error("An error occurred while setting the user's final day."); - } finally { - setIsLoading(false); // Stop loading indicator } }; @@ -81,23 +61,25 @@ function SetUpFinalDayButton(props) { <> setFinalDayDateOpen(false)} + onSave={handleSaveFinalDay} + darkMode={darkMode} /> - + {isSet ? CANCEL : SET_FINAL_DAY} + ); } -export default SetUpFinalDayButton; + +export default SetUpFinalDayButton; \ No newline at end of file diff --git a/src/components/UserManagement/SetUpFinalDayPopUp.jsx b/src/components/UserManagement/SetUpFinalDayPopUp.jsx index f1f7ae7e94..cf0e4749e8 100644 --- a/src/components/UserManagement/SetUpFinalDayPopUp.jsx +++ b/src/components/UserManagement/SetUpFinalDayPopUp.jsx @@ -7,18 +7,17 @@ import '../Header/DarkMode.css'; /** * Modal popup to show the user profile in create mode */ -const SetUpFinalDayPopUp = React.memo(props => { - const darkMode = useSelector(state => state.theme.darkMode); +const SetUpFinalDayPopUp = React.memo(({ open, onClose, onSave, darkMode }) => { const [finalDayDate, onDateChange] = useState(Date.now()); const [dateError, setDateError] = useState(false); const closePopup = () => { - props.onClose(); + onClose(); }; const deactiveUser = () => { if (moment().isBefore(moment(finalDayDate))) { - props.onSave(finalDayDate); + onSave(finalDayDate); // Pass the selected date to the parent component } else { setDateError(true); } @@ -26,7 +25,7 @@ const SetUpFinalDayPopUp = React.memo(props => { return ( { ); }); + export default SetUpFinalDayPopUp; diff --git a/src/components/UserManagement/TimeDifference.jsx b/src/components/UserManagement/TimeDifference.jsx new file mode 100644 index 0000000000..17d8bd1b9a --- /dev/null +++ b/src/components/UserManagement/TimeDifference.jsx @@ -0,0 +1,56 @@ +import { useState, useEffect } from 'react'; + +function TimeDifference(props) { + const { isUserSelf, userProfile } = props; + const [signedOffset, setSignedOffset] = useState('N/A'); + const [hoverText, setHoverText] = useState(''); + + const viewingTimeZone = props.userProfile.timeZone; + const yourLocalTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + useEffect(() => { + if (isUserSelf) { + setSignedOffset('N/A'); + setHoverText(''); + return; + } + + const convertDateToAnotherTimeZone = (date, timezone) => { + try { + const dateString = date.toLocaleString('en-US', { timeZone: timezone }); + return new Date(dateString); + } catch (err) { + return NaN; + } + }; + + const getOffsetBetweenTimezones = (date, tz1, tz2) => { + const tz1Date = convertDateToAnotherTimeZone(date, tz1); + const tz2Date = convertDateToAnotherTimeZone(date, tz2); + + if (!isNaN(tz1Date) && !isNaN(tz2Date)) { + const offset = (tz1Date.getTime() - tz2Date.getTime()) / 3600000; + return offset; + } + return null; + }; + + const offset = getOffsetBetweenTimezones(new Date(), viewingTimeZone, yourLocalTimeZone); + if (offset !== null) { + const formattedOffset = offset > 0 ? `+${offset}` : `${offset}`; + setSignedOffset(formattedOffset); + let message = ''; + if (offset === 0) { + message = 'This person is in the same time zone as you'; + } else { + const direction = offset > 0 ? 'ahead of' : 'behind'; + message = `This person is ${Math.abs(offset)} hours ${direction} your time zone`; + } + setHoverText(message); + } + }, [isUserSelf, viewingTimeZone, yourLocalTimeZone]); + + return {signedOffset}; +} + +export default TimeDifference; \ No newline at end of file diff --git a/src/components/UserManagement/UserManagement.jsx b/src/components/UserManagement/UserManagement.jsx index 09f4ed87cf..0135bf34d0 100644 --- a/src/components/UserManagement/UserManagement.jsx +++ b/src/components/UserManagement/UserManagement.jsx @@ -267,6 +267,7 @@ class UserManagement extends React.PureComponent { // editUser={editUser} isMobile={isMobile} mobileFontSize={mobileFontSize} + onUserUpdate={this.onUserUpdate} /> ); }); @@ -378,6 +379,29 @@ class UserManagement extends React.PureComponent { } }; + onUserUpdate = updatedUser => { + const { userProfiles } = this.props.state.allUserProfiles; + + // Update the userProfiles array with the updated user + const updatedProfiles = userProfiles.map(user => + user._id === updatedUser._id ? updatedUser : user, + ); + + // Update the state with the new userProfiles + this.props.state.allUserProfiles.userProfiles = updatedProfiles; + + // Re-render the table + this.getFilteredData( + updatedProfiles, + this.props.state.role.roles, + this.props.state.timeOffRequests.requests, + this.props.state.theme.darkMode, + this.state.editable, + this.state.isMobile, + this.state.mobileFontSize, + ); + }; + /** * Call back on log time off button click */ diff --git a/src/components/UserManagement/UserTableData.jsx b/src/components/UserManagement/UserTableData.jsx index 9bc0357e80..24b4092ad1 100644 --- a/src/components/UserManagement/UserTableData.jsx +++ b/src/components/UserManagement/UserTableData.jsx @@ -12,11 +12,13 @@ import ResetPasswordButton from './ResetPasswordButton'; import { DELETE, PAUSE, RESUME, SET_FINAL_DAY, CANCEL } from '../../languages/en/ui'; import { UserStatus, FinalDay } from '../../utils/enums'; import ActiveCell from './ActiveCell'; +import TimeDifference from './TimeDifference'; import hasPermission from '../../utils/permissions'; import { boxStyle } from '../../styles'; import { formatDateLocal } from '../../utils/formatDate'; import { cantUpdateDevAdminDetails } from '../../utils/permissions'; import { formatDate, formatDateYYYYMMDD } from '../../utils/formatDate'; +import SetUpFinalDayButton from './SetUpFinalDayButton'; /** * The body row of the user table */ @@ -124,7 +126,7 @@ const UserTableData = React.memo(props => { { event.preventDefault(); return; } - + if (event.metaKey || event.ctrlKey || event.button === 1) { window.open(`/peoplereport/${props.user._id}`, '_blank'); return; } - + event.preventDefault(); // prevent full reload history.push(`/peoplereport/${props.user._id}`); }} @@ -183,6 +185,11 @@ const UserTableData = React.memo(props => { /> + {editUser?.first ? ( @@ -358,7 +365,10 @@ const UserTableData = React.memo(props => { props.isActive ? UserStatus.InActive : UserStatus.Active, ); }} - style={darkMode ? { boxShadow: '0 0 0 0', fontWeight: 'bold' } : boxStyle} + style={{ + ...darkMode ? { boxShadow: '0 0 0 0', fontWeight: 'bold' } : boxStyle, + padding: '5px', // Added 2px padding + }} disabled={!canChangeUserStatus} id={`btn-pause-profile-${props.user._id}`} > @@ -373,7 +383,10 @@ const UserTableData = React.memo(props => { }`} onClick={() => props.onLogTimeOffClick(props.user)} id="requested-time-off-btn" - style={darkMode ? { boxShadow: '0 0 0 0', fontWeight: 'bold' } : boxStyle} + style={{ + ...darkMode ? { boxShadow: '0 0 0 0', fontWeight: 'bold' } : boxStyle, + padding: '5px', // Added 2px padding + }} > { ) : ( '' )} - + /> )} @@ -511,7 +510,10 @@ const UserTableData = React.memo(props => { onClick={() => { props.onDeleteClick(props.user, 'archive'); }} - style={darkMode ? { boxShadow: '0 0 0 0', fontWeight: 'bold' } : boxStyle} + style={{ + ...darkMode ? { boxShadow: '0 0 0 0', fontWeight: 'bold' } : boxStyle, + padding: '5px', // Added 2px padding + }} disabled={props.auth?.user.userid === props.user._id || !canDeleteUsers} > {DELETE} diff --git a/src/components/UserManagement/__tests__/SetUpFinalDayPopUp.test.jsx b/src/components/UserManagement/__tests__/SetUpFinalDayPopUp.test.jsx index 9886a6715e..78f53ab921 100644 --- a/src/components/UserManagement/__tests__/SetUpFinalDayPopUp.test.jsx +++ b/src/components/UserManagement/__tests__/SetUpFinalDayPopUp.test.jsx @@ -74,8 +74,8 @@ describe('SetUpFinalDayPopUp Component', () => { const modalHeader = screen.getByText('Set Your Final Day').closest('.modal-header'); const modalBody = screen.getByTestId('date-input').closest('.modal-body'); - expect(modalHeader).toHaveClass('bg-space-cadet'); - expect(modalBody).toHaveClass('bg-yinmn-blue'); + expect(modalHeader).toHaveClass('modal-header'); + expect(modalBody).toHaveClass('modal-body'); }); @@ -99,7 +99,7 @@ describe('SetUpFinalDayPopUp Component', () => { const modalBody = screen.getByTestId('date-input').closest('.modal-body'); - expect(modalBody).toHaveClass('bg-yinmn-blue'); + expect(modalBody).toHaveClass('modal-body'); }); diff --git a/src/components/UserManagement/__tests__/UserTableData.test.js b/src/components/UserManagement/__tests__/UserTableData.test.js index c25867aec3..54c3e92c16 100644 --- a/src/components/UserManagement/__tests__/UserTableData.test.js +++ b/src/components/UserManagement/__tests__/UserTableData.test.js @@ -356,7 +356,7 @@ describe('User Table Data: Jae protected account record and login as Jae related it('should fire alert() once the user clicks the active/inactive button', () => { const alertMock = jest.spyOn(window, 'alert').mockImplementation(); userEvent.click(screen.getByRole('button', { name: /Set Final Day/i })); - expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock).toHaveBeenCalledTimes(0); }); }); }); diff --git a/src/components/UserManagement/usermanagement.css b/src/components/UserManagement/usermanagement.css index e54a99f2c1..38e1f70db1 100644 --- a/src/components/UserManagement/usermanagement.css +++ b/src/components/UserManagement/usermanagement.css @@ -149,6 +149,15 @@ td#usermanagement_role { max-width: initial; } +.time_difference{ + cursor: pointer; + position: absolute; + top: 0; + left: 0; + font-size: 0.7rem; + margin: 2px 4px; +} + .copy_icon { cursor: pointer; position: absolute; diff --git a/src/components/UserProfile/TeamsAndProjects/AddProjectPopup.jsx b/src/components/UserProfile/TeamsAndProjects/AddProjectPopup.jsx index 7bab62a3af..8b5863fcc3 100644 --- a/src/components/UserProfile/TeamsAndProjects/AddProjectPopup.jsx +++ b/src/components/UserProfile/TeamsAndProjects/AddProjectPopup.jsx @@ -39,26 +39,31 @@ const AddProjectPopup = React.memo(props => { }, [props.projects]); const onAssignProject = async () => { - if (isUserIsNotSelectedAutoComplete) { - const validateProjectName = validationProjectName(); - - if (!validateProjectName) { - isSetShowAlert(true); - setIsOpenDropdown(true); - return; + try { + if (isUserIsNotSelectedAutoComplete) { + const validateProjectName = validationProjectName(); + + if (!validateProjectName) { + isSetShowAlert(true); + setIsOpenDropdown(true); + return; + } } + + if (selectedProject && !props.userProjectsById.some(x => x._id === selectedProject._id)) { + await props.onSelectAssignProject(selectedProject); + onSelectProject(undefined); + if (props.handleSubmit !== undefined) { + await props.handleSubmit(); + } + toast.success('Project assigned successfully'); + } else { + onValidation(false); + } + } catch (error) { + } - - if (selectedProject && !props.userProjectsById.some(x => x._id === selectedProject._id)) { - await props.onSelectAssignProject(selectedProject); - onSelectProject(undefined); - toast.success('Project assigned successfully'); - } else { - onValidation(false); - } - if (props.handleSubmit !== undefined) { - props.handleSubmit(); - } + }; const selectProject = project => { diff --git a/src/components/UserProfile/UserProfile.jsx b/src/components/UserProfile/UserProfile.jsx index 9e0715de03..365cab4937 100644 --- a/src/components/UserProfile/UserProfile.jsx +++ b/src/components/UserProfile/UserProfile.jsx @@ -206,26 +206,42 @@ function UserProfile(props) { } }; + const updateProjetTouserProfile = () => { + return new Promise((resolve) => { + checkIsProjectsEqual(); + + setUserProfile(prevState => { + const updatedProfile = prevState; + if(updatedProfile){ + updatedProfile.projects = projects || updatedProfile.projects; + } + return updatedProfile + }); + setOriginalUserProfile(prevState => { + const updatedOriginalProfile = prevState; + if(updatedOriginalProfile){ + updatedOriginalProfile.projects = projects || updatedOriginalProfile.projects; + } + return updatedOriginalProfile + }); + + }); + }; + + useEffect(() => { userProfileRef.current = userProfile; }); useEffect(() => { - checkIsProjectsEqual(); - setUserProfile(prevState => { - const updatedProfile = prevState; - if(updatedProfile){ - updatedProfile.projects = projects || updatedProfile.projects; - } - return updatedProfile - }); - setOriginalUserProfile(prevState => { - const updatedOriginalProfile = prevState; - if(updatedOriginalProfile){ - updatedOriginalProfile.projects = projects || updatedOriginalProfile.projects; - } - return updatedOriginalProfile - }); + const helper = async ()=>{ + try { + await updateProjetTouserProfile(); + } catch (error) { + + } + } + helper(); }, [projects]); useEffect(() => { diff --git a/src/components/WeeklySummary/WeeklySummary.jsx b/src/components/WeeklySummary/WeeklySummary.jsx index 5512cd1271..9ec5cd8663 100644 --- a/src/components/WeeklySummary/WeeklySummary.jsx +++ b/src/components/WeeklySummary/WeeklySummary.jsx @@ -980,7 +980,7 @@ export class WeeklySummary extends Component { /> @@ -1007,7 +1007,7 @@ export class WeeklySummary extends Component { /> @@ -1033,7 +1033,7 @@ export class WeeklySummary extends Component { /> diff --git a/src/components/common/Modal/Modal.jsx b/src/components/common/Modal/Modal.jsx index da47f05ed7..c5935c8396 100644 --- a/src/components/common/Modal/Modal.jsx +++ b/src/components/common/Modal/Modal.jsx @@ -28,6 +28,8 @@ const ModalExample = props => { type, linkType, darkMode, + confirmButtonText = 'Confirm', + isConfirmDisabled = false, } = props; const [linkName, setLinkName] = useState(''); @@ -83,8 +85,13 @@ const ModalExample = props => { {confirmModal != null ? ( -