From 7b59ebd27dac6d5fe625f4f7979aa7fe78806267 Mon Sep 17 00:00:00 2001 From: Sheetal Mangate Date: Sat, 23 Nov 2024 19:25:58 -0800 Subject: [PATCH 01/37] Added test cases for monthlyDashboardDataReducer --- .../monthlyDashboardDataReducer.test.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/reducers/__tests__/monthlyDashboardDataReducer.test.js diff --git a/src/reducers/__tests__/monthlyDashboardDataReducer.test.js b/src/reducers/__tests__/monthlyDashboardDataReducer.test.js new file mode 100644 index 0000000000..059404ce58 --- /dev/null +++ b/src/reducers/__tests__/monthlyDashboardDataReducer.test.js @@ -0,0 +1,31 @@ +import { monthlyDashboardDataReducer } from '../monthlyDashboardDataReducer'; + +const monthlyDashboardData = { + projectName: "", + timeSpent_hrs: 0, +} + +describe('Monthly Dashboard Data Reducer', () => { + + it('get monthly dashboard data', () => { + + const newPayload = { projectName: 'Project Name', timeSpent_hrs : '0' }; + + const action = { + type: 'GET_MONTHLY_DASHBOARD_DATA', + payload: newPayload, + }; + + const result = monthlyDashboardDataReducer(monthlyDashboardData, action ); + expect(result).toEqual(newPayload); + + }); + + it('should return the initial state when an action type is not passed', () => { + + const result = monthlyDashboardDataReducer(monthlyDashboardData, {} ); + expect(result).toEqual(monthlyDashboardData); + + }); + +}) \ No newline at end of file From 7ac56eb6e6f9b58b477840270b65306b65451690 Mon Sep 17 00:00:00 2001 From: Sheetal Mangate Date: Sat, 21 Dec 2024 15:46:47 -0800 Subject: [PATCH 02/37] Added test cases for teamsTeamMembersReducer --- .../__tests__/teamsTeamMembersReducer.test.js | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/reducers/__tests__/teamsTeamMembersReducer.test.js diff --git a/src/reducers/__tests__/teamsTeamMembersReducer.test.js b/src/reducers/__tests__/teamsTeamMembersReducer.test.js new file mode 100644 index 0000000000..2b9d6d9934 --- /dev/null +++ b/src/reducers/__tests__/teamsTeamMembersReducer.test.js @@ -0,0 +1,91 @@ +import { RECEIVE_TEAM_USERS, TEAM_MEMBER_ADD, TEAM_MEMBER_DELETE, FETCH_TEAM_USERS_START } from '../../constants/allTeamsConstants'; +import { teamUsersReducer, updateObject } from '../teamsTeamMembersReducer' + +const teamUsersInitial = { + fetching: false, + fetched: false, + teamMembers: [], + status: 404 +}; + +const teamMembers = { + teamMembers: [{ + _id: '1', + firstName: 'First name', + lastName: 'Last name', + jobTitle: [''], + teams: [], + projects: ['p1', 'p2'] + }] +}; + +describe('Teams Team Members Reducer', () => { + + it('Should receive team users', () => { + + const expectedState = { teamMembers:{}, fetching: false, fetched: true, status: '200'}; + const action = { + payload: {}, + type:RECEIVE_TEAM_USERS + }; + + const result = teamUsersReducer( teamMembers, action ); + expect(result).toEqual(expectedState); + + }); + + it('Should add team member to team', () => { + + const action = { + member: { _id: '2', firstName: 'First name', lastName: 'Last name' }, + type:TEAM_MEMBER_ADD + }; + + const expectedResult = { + teamMembers:Object.assign([...teamMembers.teamMembers, action.member]), + fetching: false, + fetched: true, + status: '200' + }; + + const result = teamUsersReducer(teamMembers, action); + expect(result).toEqual(expectedResult); + + }); + + it('Should delete member from team', () => { + + const action = { + member: { _id: '2', firstName: 'First name', lastName: 'Last name' }, + type: TEAM_MEMBER_DELETE + }; + + const expectedResult = { + teamMembers:Object.assign(teamMembers.teamMembers.filter(item => item._id !== action.member)), + fetching: false, + fetched: true, + status: '200' + }; + + const result = teamUsersReducer(teamMembers, action); + expect(result).toEqual(expectedResult); + + }); + + it('Should fetch teams user start', () => { + + const expectedResult = updateObject(teamMembers, {fetching: true, fetched: false}); + + const result = teamUsersReducer( teamMembers,{ type: FETCH_TEAM_USERS_START } ); + expect(result).toEqual(expectedResult); + + }); + + it('Should return initial teamMemebres in default case ', () =>{ + + const result = teamUsersReducer( teamMembers, {} ); + expect(result).toEqual(teamMembers); + + }); + +}) \ No newline at end of file From a4d7edda9949971045adb14e0d3394b49c834fd2 Mon Sep 17 00:00:00 2001 From: nishitag5 Date: Sat, 18 Jan 2025 01:02:58 -0500 Subject: [PATCH 03/37] frontend for event participation analysis --- .../Reports/Participation/DropOffTracking.jsx | 120 ++++++++ .../Participation/EventParticipation.jsx | 21 ++ .../Reports/Participation/MyCases.css | 211 +++++++++++++ .../Reports/Participation/MyCases.jsx | 134 +++++++++ .../Reports/Participation/NoShowInsights.jsx | 121 ++++++++ .../Reports/Participation/Participation.css | 279 ++++++++++++++++++ .../Reports/Participation/mockData.jsx | 74 +++++ src/routes.js | 4 +- 8 files changed, 963 insertions(+), 1 deletion(-) create mode 100644 src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx create mode 100644 src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx create mode 100644 src/components/CommunityPortal/Reports/Participation/MyCases.css create mode 100644 src/components/CommunityPortal/Reports/Participation/MyCases.jsx create mode 100644 src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx create mode 100644 src/components/CommunityPortal/Reports/Participation/Participation.css create mode 100644 src/components/CommunityPortal/Reports/Participation/mockData.jsx diff --git a/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx b/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx new file mode 100644 index 0000000000..d730519a21 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx @@ -0,0 +1,120 @@ +import { useState } from 'react'; +import './Participation.css'; +import mockEvents from './mockData'; + +function DropOffTracking() { + const [selectedEvent, setSelectedEvent] = useState('All Events'); + const [selectedTime, setSelectedTime] = useState('All Time'); + + const getDateRange = () => { + const today = new Date(); + let startDate; + let endDate; + + if (selectedTime === 'Today') { + startDate = new Date(today); + // Start of the day + startDate.setHours(0, 0, 0, 0); + endDate = new Date(today); + // End of the day + endDate.setHours(23, 59, 59, 999); + } else if (selectedTime === 'This Week') { + startDate = new Date(today); + startDate.setDate(today.getDate() - today.getDay()); + // Start of the week + startDate.setHours(0, 0, 0, 0); + endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + 6); + // End of the week + endDate.setHours(23, 59, 59, 999); + } else if (selectedTime === 'This Month') { + // Start of the month + startDate = new Date(today.getFullYear(), today.getMonth(), 1); + // End of the month + endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0); + endDate.setHours(23, 59, 59, 999); + } + + return { startDate, endDate }; + }; + + // Filter events based on selected filters + const filteredEvents = mockEvents.filter(event => { + // Filter by event type + if (selectedEvent !== 'All Events' && event.eventType !== selectedEvent) { + return false; + } + + // Filter by date range + if (selectedTime !== 'All Time') { + const { startDate, endDate } = getDateRange(); + const eventDate = new Date(event.eventTime.split(' pm ')[1]); + if (startDate && endDate) { + return eventDate >= startDate && eventDate <= endDate; + } + } + + return true; + }); + + return ( +
+
+

Drop-off and no-show rate tracking

+
+ + +
+
+
+
+

+ +5% Last week +

+

Drop-off rate

+
+
+

+ +5% Last week +

+

No-show rate

+
+
+
+ + + + + + + + + + + {filteredEvents.map(event => ( + + + + + + + ))} + +
Event nameNo-show rateDrop-off rateAttendees
{event.eventName}{event.noShowRate}{event.dropOffRate}{event.attendees}
+
+
+ ); +} + +export default DropOffTracking; diff --git a/src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx b/src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx new file mode 100644 index 0000000000..4b1af71b17 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx @@ -0,0 +1,21 @@ +import MyCases from './MyCases'; +import DropOffTracking from './DropOffTracking'; +import NoShowInsights from './NoShowInsights'; +import './Participation.css'; + +function LandingPage() { + return ( +
+
+

HGN Management System

+
+ +
+ + +
+
+ ); +} + +export default LandingPage; diff --git a/src/components/CommunityPortal/Reports/Participation/MyCases.css b/src/components/CommunityPortal/Reports/Participation/MyCases.css new file mode 100644 index 0000000000..e8f59111cc --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/MyCases.css @@ -0,0 +1,211 @@ +/* General Styles */ +body { + font-family: 'Arial', sans-serif; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.my-cases-page { + padding: 20px; + background-color: #ffffff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.my-cases-page .header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.section-title { + font-size: 24px; + font-weight: bold; + color: #333; + margin: 0; +} + +.header-actions { + display: flex; + align-items: center; + gap: 15px; +} + +.view-switcher { + border: 1px solid #ddd; + border-radius: 4px; +} + +.view-switcher button, +.create-new { + background-color: #fff; + border-radius: 4px; + padding: 8px 12px; + cursor: pointer; + font-size: 14px; + color: #333; +} + +.filter-wrapper { + margin: 10px 0; +} + +.filter-dropdown { + padding: 8px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; +} + +/* Hover and focus effects for the dropdown */ +.filter-dropdown:hover, +.filter-dropdown:focus { + border-color: #007bff; + outline: none; +} + +.view-switcher button.active, +.create-new:hover { + background-color: #007bff; + color: #fff; + border-color: #007bff; +} + +.create-new { + font-weight: bold; + border: 1px solid #ccc; + border-radius: 4px; +} + +.content { + margin-top: 10px; +} + +/* Card View Styles */ +.case-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; +} + +.case-card { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.event-badge { + display: inline-block; + font-size: 12px; + font-weight: bold; + padding: 5px 10px; + border-radius: 20px; + margin-bottom: 10px; + background-color: #e9f7ef; + color: #28a745; +} + +.event-badge[data-type='Yoga Class'] { + background-color: #e9f7ef; + color: #28a745; +} + +.event-badge[data-type='Cooking Workshop'] { + background-color: #fdecea; + color: #e74c3c; +} + +.event-badge[data-type='Dance Class'] { + background-color: #fff5e6; + color: #f39c12; +} + +.event-badge[data-type='Fitness Bootcamp'] { + background-color: #f5e6ff; + color: #9b59b6; +} + +.event-time { + font-size: 14px; + color: #555; + margin-bottom: 10px; +} + +.event-name { + font-size: 16px; + font-weight: bold; + margin-bottom: 15px; + color: #333; +} + +.attendees-info { + display: flex; + align-items: center; + justify-content: space-between; +} + +.avatars img { + width: 24px; + height: 24px; + border-radius: 50%; + margin-right: -8px; + border: 2px solid #fff; +} + +.attendees-count { + font-size: 14px; + color: #555; +} + +.rates { + margin-top: 10px; + font-size: 14px; + color: #555; +} + +/* List View Styles */ +.case-list { + list-style-type: none; + padding: 0; + margin: 0; +} + +.case-list-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px; + background-color: #fff; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.case-list-item span { + margin-right: 15px; + font-size: 14px; + color: #333; +} + +.event-type { + font-weight: bold; + font-size: 14px; +} + +/* Calendar View Styles */ +.calendar-view { + padding: 20px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 8px; + text-align: center; + font-size: 16px; + color: #555; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/src/components/CommunityPortal/Reports/Participation/MyCases.jsx b/src/components/CommunityPortal/Reports/Participation/MyCases.jsx new file mode 100644 index 0000000000..3db824d6f6 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/MyCases.jsx @@ -0,0 +1,134 @@ +import { useState } from 'react'; +import './MyCases.css'; +import mockEvents from './mockData'; + +function MyCases() { + const [view, setView] = useState('card'); + const [filter, setFilter] = useState('all'); + + const filterEvents = events => { + const now = new Date(); + if (filter === 'today') { + return events.filter(event => { + const eventDate = new Date(event.eventTime); + return ( + eventDate.getDate() === now.getDate() && + eventDate.getMonth() === now.getMonth() && + eventDate.getFullYear() === now.getFullYear() + ); + }); + } + if (filter === 'thisWeek') { + const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(endOfWeek.getDate() + 6); + return events.filter(event => { + const eventDate = new Date(event.eventTime); + return eventDate >= startOfWeek && eventDate <= endOfWeek; + }); + } + if (filter === 'thisMonth') { + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); + return events.filter(event => { + const eventDate = new Date(event.eventTime); + return eventDate >= startOfMonth && eventDate <= endOfMonth; + }); + } + return events; + }; + + const filteredEvents = filterEvents(mockEvents); + + const renderCardView = () => ( +
+ {filteredEvents.map(event => ( +
+ {event.eventType} + {event.eventTime} + {event.eventName} +
+
+ profile img +
+ {`+${event.attendees}`} +
+
+ ))} +
+ ); + + const renderListView = () => ( +
    + {filteredEvents.map(event => ( +
  • + {event.eventType} + {event.eventTime} + {event.eventName} + {`+${event.attendees}`} +
  • + ))} +
+ ); + + const renderCalendarView = () => ( +
+

Calendar View is under construction...

+
+ ); + + return ( +
+
+

My Cases

+
+
+ + + +
+
+ +
+ +
+
+
+ {view === 'card' && renderCardView()} + {view === 'list' && renderListView()} + {view === 'calendar' && renderCalendarView()} +
+
+ ); +} + +export default MyCases; diff --git a/src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx b/src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx new file mode 100644 index 0000000000..2d68261953 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/NoShowInsights.jsx @@ -0,0 +1,121 @@ +import { useState } from 'react'; +import mockEvents from './mockData'; // Import mock data +import './Participation.css'; + +function NoShowInsights() { + // State for the selected date filter and tab + const [dateFilter, setDateFilter] = useState('All'); + const [activeTab, setActiveTab] = useState('Event type'); + + // Function to filter events based on the date filter + const filterByDate = events => { + const today = new Date(); + return events.filter(event => { + const eventDate = new Date(event.eventTime.split(' pm ')[1]); + switch (dateFilter) { + case 'Today': + return eventDate.toDateString() === today.toDateString(); + case 'This Week': { + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - today.getDay()); + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + return eventDate >= startOfWeek && eventDate <= endOfWeek; + } + case 'This Month': + return ( + eventDate.getMonth() === today.getMonth() && + eventDate.getFullYear() === today.getFullYear() + ); + default: + return true; // All Time + } + }); + }; + + // Function to aggregate stats based on the active tab + const calculateStats = filteredEvents => { + const statsMap = new Map(); + + filteredEvents.forEach(event => { + let key; // Initialize the key variable + + // Determine the key based on activeTab + if (activeTab === 'Event type') { + key = event.eventType; + } else if (activeTab === 'Time') { + const [timeRange] = event.eventTime.split(' '); + key = activeTab === 'Time' ? timeRange : event.location; + } else if (activeTab === 'Location') { + key = event.location; + } + + const percentage = parseInt(event.noShowRate, 10); + + if (statsMap.has(key)) { + const existing = statsMap.get(key); + statsMap.set(key, { + totalPercentage: existing.totalPercentage + percentage, + count: existing.count + 1, + }); + } else { + statsMap.set(key, { totalPercentage: percentage, count: 1 }); + } + }); + + // Transform the map into an array of stats + return Array.from(statsMap.entries()).map(([key, value]) => ({ + label: key, + percentage: Math.round(value.totalPercentage / value.count), + })); + }; + + // Function to render stats dynamically for the active tab + const renderStats = () => { + const filteredEvents = filterByDate(mockEvents); + const stats = calculateStats(filteredEvents); + + return stats.map(item => ( +
+
{item.label}
+
+
+
+
{item.percentage}%
+
+ )); + }; + + return ( +
+
+

No-show rate insights

+
+ +
+
+ +
+ {['Event type', 'Time', 'Location'].map(tab => ( + + ))} +
+ +
{renderStats()}
+
+ ); +} + +export default NoShowInsights; diff --git a/src/components/CommunityPortal/Reports/Participation/Participation.css b/src/components/CommunityPortal/Reports/Participation/Participation.css new file mode 100644 index 0000000000..625f260dd3 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/Participation.css @@ -0,0 +1,279 @@ +/* General Styling */ +body { + font-family: 'Arial', sans-serif; + margin: 0; + padding: 0; + background-color: #f5f5f5; +} + +.participation-landing-page { + max-width: 90%; + margin: 0 auto; +} + +header h1 { + font-size: 24px; + color: #333; + margin-bottom: 20px; + text-align: center; +} + +header { + margin-bottom: 30px; +} + +/* My Cases Section */ +.my-cases { + margin-bottom: 40px; +} + +.my-cases-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.my-cases-header h2 { + font-size: 20px; + color: #333; +} + +.my-cases-header button { + padding: 8px 15px; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.my-cases-header button:hover { + background-color: #0056b3; +} + +.my-cases-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.my-case-item { + padding: 15px; + background: white; + border: 1px solid #ddd; + border-radius: 5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Analytics Section */ +.analytics-section { + display: flex; + flex-wrap: nowrap; + gap: 20px; + justify-content: space-between; + align-items: flex-start; + padding-top: 20px; +} + +.tracking-container, +.insights { + flex: 1; + min-width: 48%; + background-color: #ffffff; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 20px; + box-sizing: border-box; +} + +@media (max-width: 768px) { + .analytics-section { + flex-wrap: wrap; + } + + .tracking-container, + .insights { + width: 100%; + } +} + +/* Tracking Section */ +.tracking-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.tracking-header h3 { + font-size: 1.3rem; + color: #333; + margin: 0; +} + +.tracking-filters select { + margin-left: 10px; + padding: 8px; + font-size: 0.8rem; + border: 1px solid #ccc; + border-radius: 5px; +} + +.tracking-summary { + display: flex; + justify-content: space-between; + margin-bottom: 20px; +} + +.tracking-rate { + background: #f8f9fa; + border-radius: 10px; + width: 48%; + padding: 10px; +} + +.tracking-rate-value { + font-size: 0.9rem; + color: #f44336; + display: inline-block; +} + +.tracking-rate-value span { + font-size: 0.9rem; + color: #777; + display: inline-block; +} + +.tracking-list-container { + max-height: 600px; + overflow-y: auto; + border: 1px solid #ccc; + border-radius: 10px; + padding: 10px; + background: #fff; +} + +.tracking-table { + width: 100%; + border-collapse: collapse; +} + +.tracking-table thead th { + text-align: left; + padding: 10px; + background: #f4f4f4; +} + +.tracking-table tbody td { + padding: 5px; + border-bottom: 1px solid #eee; +} + +.tracking-rate-green { + color: green; +} + +.tracking-rate-red { + color: red; +} + +/* Insights Section */ +.insights-header { + font-weight: bold; + color: #444; + margin-bottom: 15px; + display: flex; + justify-content: space-between; +} + +.insights-header h3 { + font-size: 1.3rem; + color: #333; + margin: 0; +} + +.insights-filters select { + margin-left: 10px; + padding: 8px; + font-size: 0.8rem; + border: 1px solid #ccc; + border-radius: 5px; +} + +.insights-tabs { + display: flex; + border: 1px solid #ccc; + border-radius: 5px; + overflow: hidden; + max-width: 60%; +} + +.insights-tab { + flex: 1; + text-align: center; + padding: 10px 0; + font-size: 0.9rem; + color: #555; + background-color: #f8f9fa; + border: none; + cursor: pointer; + transition: background-color 0.3s, color 0.3s; +} + +.insights-tab:not(:last-child) { + border-right: 1px solid #ccc; +} + +.insights-tab.active-tab { + border-radius: 4px; + background-color: #007bff; + color: #fff; + font-weight: bold; +} + +.insights-tab:hover { + background-color: #e0e0e0; +} + +.insights-content { + display: flex; + flex-direction: column; + padding-top: 20px; + gap: 15px; +} + +.insight-item { + display: flex; + align-items: center; + justify-content: space-between; +} + +.insight-label { + flex: 1; + font-size: 0.9rem; + color: #555; + margin-right: 10px; +} + +.insight-bar { + flex: 2; + height: 8px; + background: #e9ecef; + border-radius: 4px; + overflow: hidden; + position: relative; + margin-right: 10px; +} + +.insight-fill { + height: 100%; + background: #007bff; + border-radius: 4px; + transition: width 0.3s ease-in-out; +} + +.insight-percentage { + font-size: 0.9rem; + font-weight: bold; + color: #333; +} \ No newline at end of file diff --git a/src/components/CommunityPortal/Reports/Participation/mockData.jsx b/src/components/CommunityPortal/Reports/Participation/mockData.jsx new file mode 100644 index 0000000000..4c5c4ae8eb --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/mockData.jsx @@ -0,0 +1,74 @@ +const mockEvents = [ + { + id: 1, + eventType: 'Yoga Class', + eventTime: '7:30-8:20 pm Jan 17, 2025', + eventName: 'Event 1', + attendees: 30, + noShowRate: '12%', + dropOffRate: '54%', + location: 'New York', + }, + { + id: 2, + eventType: 'Cooking Workshop', + eventTime: '5:00-6:30 pm Jan 20, 2025', + eventName: 'Event 2', + attendees: 25, + noShowRate: '10%', + dropOffRate: '50%', + location: 'San Francisco', + }, + { + id: 3, + eventType: 'Dance Class', + eventTime: '6:00-7:00 pm Feb 12, 2025', + eventName: 'Event 3', + attendees: 40, + noShowRate: '15%', + dropOffRate: '60%', + location: 'Los Angeles', + }, + { + id: 4, + eventType: 'Fitness Bootcamp', + eventTime: '8:00-9:00 am Feb 04, 2025', + eventName: 'Event 4', + attendees: 20, + noShowRate: '8%', + dropOffRate: '45%', + location: 'Chicago', + }, + { + id: 5, + eventType: 'Fitness Bootcamp', + eventTime: '8:00-9:00 am Feb 01, 2025', + eventName: 'Event 5', + attendees: 55, + noShowRate: '36%', + dropOffRate: '19%', + location: 'Chicago', + }, + { + id: 6, + eventType: 'Fitness Bootcamp', + eventTime: '8:00-9:00 am Jan 30, 2025', + eventName: 'Event 6', + attendees: 41, + noShowRate: '85%', + dropOffRate: '25%', + location: 'Chicago', + }, + { + id: 7, + eventType: 'Fitness Bootcamp', + eventTime: '8:00-9:00 am Jan 21, 2025', + eventName: 'Event 7', + attendees: 50, + noShowRate: '18%', + dropOffRate: '40%', + location: 'Chicago', + }, +]; + +export default mockEvents; diff --git a/src/routes.js b/src/routes.js index d22fdd3443..24ce6d18b0 100644 --- a/src/routes.js +++ b/src/routes.js @@ -52,7 +52,7 @@ import ActivityList from './components/CommunityPortal/Activities/ActivityList'; // import AddActivities from './components/CommunityPortal/Activities/AddActivities'; // import ActvityDetailPage from './components/CommunityPortal/Activities/ActivityDetailPage'; - +import EventParticipation from './components/CommunityPortal/Reports/Participation/EventParticipation'; // eslint-disable-next-line import/order, import/no-unresolved @@ -362,6 +362,8 @@ export default ( + + {/* */} From 274acaa8afec33786b12fc0854322b8f9f253239 Mon Sep 17 00:00:00 2001 From: Sheetal Mangate Date: Sat, 18 Jan 2025 12:42:21 -0800 Subject: [PATCH 04/37] test cases for action weeklySummaries --- .../__tests__/weeklySummariesAction.tests.js | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/actions/__tests__/weeklySummariesAction.tests.js diff --git a/src/actions/__tests__/weeklySummariesAction.tests.js b/src/actions/__tests__/weeklySummariesAction.tests.js new file mode 100644 index 0000000000..59b8e359c3 --- /dev/null +++ b/src/actions/__tests__/weeklySummariesAction.tests.js @@ -0,0 +1,207 @@ +import axios from 'axios'; +import * as actions from '../../constants/weeklySummaries'; +import { ENDPOINTS } from '../../utils/URL'; +import { fetchWeeklySummariesBegin, fetchWeeklySummariesSuccess, fetchWeeklySummariesError, getWeeklySummaries, updateWeeklySummaries } from '../weeklySummaries' +import { getUserProfileActionCreator } from '../../actions/userProfile'; + + +jest.mock('axios'); + + +describe('Weekly Summaries Action',() => { + + it('Should return action FETCH_WEEKLY_SUMMARIES_BEGIN', () => { + + const data = fetchWeeklySummariesBegin(); + + expect(data).toEqual( {type: actions.FETCH_WEEKLY_SUMMARIES_BEGIN }); + + }); + + it('Should fetch weekly summaries success', () => { + + const weeklySummariesData = { + id: 1, + dueDate: "2024-12-29", + summary: "Weekly Summary" + }; + + const result = fetchWeeklySummariesSuccess(weeklySummariesData); + + const expecteResult = { + type: actions.FETCH_WEEKLY_SUMMARIES_SUCCESS, + payload : {weeklySummariesData} + } + + expect(result).toEqual(expecteResult); + + }); + + it('Should return action FETCH_WEEKLY_SUMMARIES_ERROR ', () => { + + const error = {}; + const result = fetchWeeklySummariesError(error); + + expect(result).toEqual({ + type: actions.FETCH_WEEKLY_SUMMARIES_ERROR, + payload: {error} + }); + }) + +}); + +describe('Weekly Summaries', () => { + + jest.mock('axios'); + const dispatch = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Get Weekly Summaries', () => { + + it('Should dispatch actions and return status 200 on successful API call', async() => { + + const mockData = { + weeklySummariesCount : 1, + weeklySummaries: [ + { + id: "1", + dueDate: "2024-12-29", + summary: "Weekly Summary" + } + ], + mediaUrl: 'http://media.com', + adminLinks: [{ Name: 'Media Folder', Link: 'http://newmedia.com' }] + } + + axios.get.mockResolvedValue({ data: mockData, status:200 }); + + const result = await getWeeklySummaries(1)(dispatch); + + expect(dispatch).toHaveBeenCalledWith(fetchWeeklySummariesBegin()); + expect(dispatch).toHaveBeenCalledWith(fetchWeeklySummariesSuccess({ + weeklySummariesCount:1, + weeklySummaries:[ + { + id: "1", + dueDate: "2024-12-29", + summary: "Weekly Summary" + } + ], + mediaUrl: 'http://newmedia.com' + })); + + expect(dispatch).toHaveBeenCalledWith(getUserProfileActionCreator(mockData)); + expect(result).toBe(200); + + }); + + it('Should dispatch an error action when GET request fails', async() => { + + const mockError = { response : { status: 500 } }; + axios.get.mockRejectedValueOnce(mockError); + + const result = await getWeeklySummaries(1)(dispatch); + + expect(dispatch).toHaveBeenCalledWith(fetchWeeklySummariesBegin()); + expect(dispatch).toHaveBeenCalledWith(fetchWeeklySummariesError(mockError)); + expect(result).toBe(500); + }) + + }); + + describe('Update Weekly Summaries', () => { + + it('Should successfully update weekly summaries and return status 200', async() => { + + const mockUserProfile = { + firstName: 'User First Name', + lastName: 'User Last Name', + weeklySummariesCount : 0, + weeklySummaries:[] + } + + const weeklySummariesData = { + weeklySummariesCount : 1, + weeklySummaries: [ + { + id: "1", + dueDate: "2025-01-05", + summary: "Weekly Summary Week1" + }, + ], + mediaUrl: 'http://media.com' + } + + axios.get.mockResolvedValue({ data: mockUserProfile }); + axios.put.mockResolvedValue({ status: 200 }); + + const result = await updateWeeklySummaries(1, weeklySummariesData )(dispatch); + expect(result).toBe(200); + expect(axios.get).toHaveBeenCalledWith(ENDPOINTS.USER_PROFILE(1)); + expect(axios.put).toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalledWith(getUserProfileActionCreator({ + ...mockUserProfile, + 'adminLinks' : [{ Name: 'Media Folder', Link: 'http://media.com' }] , + ...weeklySummariesData + })); + + }); + + it('Ensure that if the "Media Folder" link already exists, it should be updated correctly.', async() => { + + const mockUserProfile = { + firstName: 'User First Name', + lastName: 'User Last Name', + weeklySummariesCount : 0, + weeklySummaries:[], + 'adminLinks' : [{ Name: 'Media Folder', Link: 'http://media.com' }] + } + + const weeklySummariesData = { + weeklySummariesCount : 1, + weeklySummaries: [ + { + id: "1", + dueDate: "2025-01-05", + summary: "Weekly Summary Week1" + }, + ], + mediaUrl: 'http://newmedia.com' + } + + axios.get.mockResolvedValue({ data: mockUserProfile }); + axios.put.mockResolvedValue({ status: 200 }); + + const result = await updateWeeklySummaries(1, weeklySummariesData )(dispatch); + expect(result).toBe(200); + expect(axios.get).toHaveBeenCalledWith(ENDPOINTS.USER_PROFILE(1)); + expect(axios.put).toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalledWith(getUserProfileActionCreator({ + ...mockUserProfile, + 'adminLinks' : [{ Name: 'Media Folder', Link: 'http://newmedia.com' }] , + ...weeklySummariesData + })); + + }); + + it('Should throw error when API request fails', async() => { + + const mockError = { response : { status: 500 } }; + axios.get.mockRejectedValueOnce(mockError); + + const result = await updateWeeklySummaries(1, { + mediaUrl: 'http://newmedia.com', + weeklySummaries: [], + weeklySummariesCount: 2 })(dispatch); + + expect(dispatch).not.toHaveBeenCalled(); + expect(result).toBe(500); + + }); + + }); + +}); From 64beaf25bd07f848bd03350ead35569bb14fb70e Mon Sep 17 00:00:00 2001 From: Sheetal Mangate Date: Sat, 18 Jan 2025 17:43:48 -0800 Subject: [PATCH 05/37] file name changes --- ...eklySummariesAction.tests.js => weeklySummariesAction.test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/actions/__tests__/{weeklySummariesAction.tests.js => weeklySummariesAction.test.js} (100%) diff --git a/src/actions/__tests__/weeklySummariesAction.tests.js b/src/actions/__tests__/weeklySummariesAction.test.js similarity index 100% rename from src/actions/__tests__/weeklySummariesAction.tests.js rename to src/actions/__tests__/weeklySummariesAction.test.js From 363a6092f5c4015c4318c005d3973340fd7572f9 Mon Sep 17 00:00:00 2001 From: Sheetal Mangate Date: Sat, 18 Jan 2025 21:46:04 -0800 Subject: [PATCH 06/37] Added suggested review changes --- .../monthlyDashboardDataReducer.test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/reducers/__tests__/monthlyDashboardDataReducer.test.js b/src/reducers/__tests__/monthlyDashboardDataReducer.test.js index 059404ce58..02eab6cb97 100644 --- a/src/reducers/__tests__/monthlyDashboardDataReducer.test.js +++ b/src/reducers/__tests__/monthlyDashboardDataReducer.test.js @@ -28,4 +28,21 @@ describe('Monthly Dashboard Data Reducer', () => { }); + it('should retrun null as the initial state', () => { + + const result = monthlyDashboardDataReducer(undefined, {} ); + expect(result).toBeNull(); + + }); + + it('should return previous state if action type is unknown', () => { + const action = { + type: 'UNKNOWN_ACTION', + payload: { projectName: 'Project Name', timeSpent_hrs : '0' } + }; + + const result = monthlyDashboardDataReducer(monthlyDashboardData, action); + expect(result).toEqual(monthlyDashboardData); + }); + }) \ No newline at end of file From bbda4bd85ec47c4283f495256f64baad11222d4a Mon Sep 17 00:00:00 2001 From: Sheetal Mangate Date: Sat, 18 Jan 2025 22:15:01 -0800 Subject: [PATCH 07/37] spelling correction for return --- src/reducers/__tests__/monthlyDashboardDataReducer.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reducers/__tests__/monthlyDashboardDataReducer.test.js b/src/reducers/__tests__/monthlyDashboardDataReducer.test.js index 02eab6cb97..f858098e07 100644 --- a/src/reducers/__tests__/monthlyDashboardDataReducer.test.js +++ b/src/reducers/__tests__/monthlyDashboardDataReducer.test.js @@ -28,7 +28,7 @@ describe('Monthly Dashboard Data Reducer', () => { }); - it('should retrun null as the initial state', () => { + it('should return null as the initial state', () => { const result = monthlyDashboardDataReducer(undefined, {} ); expect(result).toBeNull(); @@ -40,7 +40,7 @@ describe('Monthly Dashboard Data Reducer', () => { type: 'UNKNOWN_ACTION', payload: { projectName: 'Project Name', timeSpent_hrs : '0' } }; - + const result = monthlyDashboardDataReducer(monthlyDashboardData, action); expect(result).toEqual(monthlyDashboardData); }); From f7f8dd81a421ca99e0cb37f02e454ee6eef1506d Mon Sep 17 00:00:00 2001 From: nishitag5 Date: Sat, 25 Jan 2025 00:40:27 -0500 Subject: [PATCH 08/37] fixing the alignment issue in colimns of list view --- .../CommunityPortal/Reports/Participation/MyCases.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CommunityPortal/Reports/Participation/MyCases.css b/src/components/CommunityPortal/Reports/Participation/MyCases.css index e8f59111cc..ee2b9f43b4 100644 --- a/src/components/CommunityPortal/Reports/Participation/MyCases.css +++ b/src/components/CommunityPortal/Reports/Participation/MyCases.css @@ -179,8 +179,8 @@ body { } .case-list-item { - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: 2fr 3fr 1fr 1fr; align-items: center; padding: 15px; background-color: #fff; From 45dc43a0bfc172b81ac0b6730dee7c77dc0f4bd8 Mon Sep 17 00:00:00 2001 From: Sheetal Mangate Date: Sat, 1 Feb 2025 21:44:09 -0800 Subject: [PATCH 09/37] Test cases for weeklySummariesReportAction --- .../weeklySummariesReportActions.test.js | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/actions/__tests__/weeklySummariesReportActions.test.js diff --git a/src/actions/__tests__/weeklySummariesReportActions.test.js b/src/actions/__tests__/weeklySummariesReportActions.test.js new file mode 100644 index 0000000000..5999f81c9b --- /dev/null +++ b/src/actions/__tests__/weeklySummariesReportActions.test.js @@ -0,0 +1,169 @@ +import axios from "axios"; +import * as actions from '../../constants/weeklySummariesReport'; +import * as weeklySummaryReport from '../weeklySummariesReport'; +import { ENDPOINTS } from '../../utils/URL'; + +jest.mock('axios'); + +describe('Weekly Summaries Report Actions',() => { + + it('Should return action FETCH_SUMMARIES_REPORT_BEGIN', () => { + + const data = weeklySummaryReport.fetchWeeklySummariesReportBegin(); + expect(data).toEqual({ type: actions.FETCH_SUMMARIES_REPORT_BEGIN }); + + }); + + it('Should return action FETCH_SUMMARIES_REPORT_SUCCESS',() => { + + const weeklySummariesData = { + id: 1, + dueDate: "2024-12-29", + summary: "Weekly Summary" + }; + + const data = weeklySummaryReport.fetchWeeklySummariesReportSuccess(weeklySummariesData); + + const expectedResult = { + type: actions.FETCH_SUMMARIES_REPORT_SUCCESS, + payload: { weeklySummariesData } + } + + expect(data).toEqual(expectedResult); + + }); + + it('Should return action FETCH_SUMMARIES_REPORT_ERROR',() => { + + const error = {} + + const data = weeklySummaryReport.fetchWeeklySummariesReportError(error); + + expect(data).toEqual({ + type:actions.FETCH_SUMMARIES_REPORT_ERROR, + payload: {error} + }); + + }) + +}); + +describe('Weekly Summary Report', () => { + + const dispatch = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Get Weekly Summaries Report', () => { + + it('Should dispatch actions and return 200 on successful API call ', async() => { + + const mockData = { + 'weeklySummaries' : [ + { + id: "1", + dueDate: "2024-12-29", + summary: "Week1 Summary" + } + ] + } + + axios.get.mockResolvedValue({ data: mockData, status:200 }); + + const result = await weeklySummaryReport.getWeeklySummariesReport()(dispatch); + + expect(dispatch).toHaveBeenCalledWith(weeklySummaryReport.fetchWeeklySummariesReportBegin()); + expect(dispatch).toHaveBeenCalledWith(weeklySummaryReport.fetchWeeklySummariesReportSuccess(result.data)); + expect(result.status).toBe(200); + + }); + + it('Should dispatch error action when GET request fails', async() => { + + const mockError = { response : { status: 500 } }; + axios.get.mockRejectedValueOnce(mockError); + + const result = await weeklySummaryReport.getWeeklySummariesReport()(dispatch); + + expect(dispatch).toHaveBeenCalledWith(weeklySummaryReport.fetchWeeklySummariesReportBegin()); + expect(dispatch).toHaveBeenCalledWith(weeklySummaryReport.fetchWeeklySummariesReportError(mockError)) + expect(result).toBe(500); + + }); + + }); + + + describe('Update One Summary Report', () => { + + let mockUserProfile, updatedField; + + beforeEach(() => { + + mockUserProfile = { + firstName: 'User First Name', + lastName: 'User Last Name', + weeklySummariesCount : 0, + weeklySummaries:[ + { + id: "1", + dueDate: "2024-12-29", + summary: "Weekly Summary Week1" + } + ] + } + + updatedField = { + + weeklySummaries: [ + { + id: "2", + dueDate: "2025-01-05", + summary: "Weekly Summary Week2" + }, + ] + + }; + + }); + + it('Should successfully update user profile with weekly summary and dispatch UPDATE_SUMMARY_REPORT action ', async() => { + + axios.get.mockResolvedValue({ data: mockUserProfile }); + axios.put.mockResolvedValue({ + status: 200 + }); + + const result = await weeklySummaryReport.updateOneSummaryReport(1, updatedField )(dispatch); + expect(result.status).toBe(200); + expect(axios.get).toHaveBeenCalledWith(ENDPOINTS.USER_PROFILE(1)); + expect(axios.put).toHaveBeenCalled(); + expect(dispatch).toHaveBeenCalledWith(weeklySummaryReport.updateSummaryReport({ _id: 1, updatedField })); + + }); + + it('Should throw error when PUT request fails', async() => { + + axios.get.mockResolvedValue({ data: mockUserProfile }); + axios.put.mockResolvedValue({ + status: 500 + }); + + const result = weeklySummaryReport.updateOneSummaryReport(1, updatedField ); + await expect(result(dispatch)).rejects.toThrow( new Error ('An error occurred while attempting to save the changes to the profile.')); + + }); + + it('Should throw error when GET request fails', async() => { + + axios.get.mockRejectedValue(new Error('Failed to fetch user profile')); + + const result = weeklySummaryReport.updateOneSummaryReport(1, updatedField ); + await expect(result(dispatch)).rejects.toThrow( new Error('Failed to fetch user profile') ); + + }); + + }); +}); \ No newline at end of file From d6aba00559293cb2f3a2cd5a02c5117bd12392b5 Mon Sep 17 00:00:00 2001 From: Shraddha Shahari Date: Mon, 10 Feb 2025 21:19:36 -0800 Subject: [PATCH 10/37] Add validation check for file upload before sending email --- src/components/Announcements/index.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index 2236bf7714..cad3f558c4 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -16,6 +16,7 @@ function Announcements({ title, email }) { const [emailSubject, setEmailSubject] = useState(''); const [testEmail, setTestEmail] = useState(''); const [showEditor, setShowEditor] = useState(true); // State to control rendering of the editor + const [isFileUploaded, setIsFileUploaded] = useState(false); useEffect(() => { // Toggle the showEditor state to force re-render when dark mode changes @@ -120,6 +121,8 @@ function Announcements({ title, email }) { const addImageToEmailContent = e => { const imageFile = document.querySelector('input[type="file"]').files[0]; + setIsFileUploaded(true); + convertImageToBase64(imageFile, base64Image => { const imageTag = `Header Image`; setHeaderContent(prevContent => `${imageTag}${prevContent}`); @@ -146,6 +149,11 @@ function Announcements({ title, email }) { return; } + if (!isFileUploaded) { + toast.error('Error: Please upload a file.'); + return; + } + const invalidEmails = emailList.filter(email => !validateEmail(email.trim())); if (invalidEmails.length > 0) { @@ -240,7 +248,7 @@ function Announcements({ title, email }) { From 64e53dbb81ba82afbaf20e7a59ce0600b100247a Mon Sep 17 00:00:00 2001 From: Shraddha Shahari Date: Mon, 10 Feb 2025 21:21:37 -0800 Subject: [PATCH 11/37] Add validation check for file upload before sending email [remove extra space] --- src/components/Announcements/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index cad3f558c4..d27a290670 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -248,7 +248,7 @@ function Announcements({ title, email }) { From a82359da6036b92bf26064b0257d7c764c007a50 Mon Sep 17 00:00:00 2001 From: ziddigsm <158247382+ziddigsm@users.noreply.github.com> Date: Fri, 14 Mar 2025 15:49:17 -0400 Subject: [PATCH 12/37] Added asterisk for the users to identify them as mandatory fields. Added warning messages in some forms to let the user know that a field is required. --- src/components/Announcements/index.jsx | 5 ++-- .../BMDashboard/Lesson/LessonForm.jsx | 2 ++ src/components/Badge/AssignBadge.jsx | 2 ++ src/components/Badge/CreateNewBadgePopup.jsx | 3 +++ src/components/Header/DarkMode.css | 5 ++++ .../PermissionsManagement/NewRolePopUp.jsx | 8 ++++-- .../UserPermissionsPopUp.jsx | 4 ++- .../Projects/AddProject/AddProject.jsx | 3 ++- .../WBS/WBSDetail/AddTask/AddTaskModal.jsx | 2 +- .../WBS/WBSDetail/EditTask/EditTaskModal.jsx | 2 +- .../Reports/LostTime/AddLostTime.jsx | 19 +++++++++++--- src/components/Reports/reportsPage.css | 4 +++ .../TeamLocations/AddOrEditPopup.jsx | 26 ++++++++++++++++--- src/components/Teams/CreateNewTeamPopup.jsx | 3 ++- .../UserManagement/setupNewUserPopup.jsx | 2 +- .../ScheduleReasonModal.jsx | 1 + 16 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/components/Announcements/index.jsx b/src/components/Announcements/index.jsx index 2236bf7714..cfff11f412 100644 --- a/src/components/Announcements/index.jsx +++ b/src/components/Announcements/index.jsx @@ -202,10 +202,11 @@ function Announcements({ title, email }) { title ? (

Email

) : ( - + + ) } diff --git a/src/components/BMDashboard/Lesson/LessonForm.jsx b/src/components/BMDashboard/Lesson/LessonForm.jsx index f24da1c6cf..d847fe6c28 100644 --- a/src/components/BMDashboard/Lesson/LessonForm.jsx +++ b/src/components/BMDashboard/Lesson/LessonForm.jsx @@ -259,6 +259,7 @@ function LessonForm() {
Lesson Title + * Write a Lesson + * Search by Full Name + * + Name + * Image URL + * Description + * - + - + -

User name:

+

+ User name* : +