diff --git a/src/components/CommunityPortal/Reports/Participation/ChartsSection.jsx b/src/components/CommunityPortal/Reports/Participation/ChartsSection.jsx new file mode 100644 index 0000000000..e3f85d5ac2 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/ChartsSection.jsx @@ -0,0 +1,146 @@ +import { useSelector } from 'react-redux'; +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + LineChart, + Line, + PieChart, + Pie, + Cell, + Legend, +} from 'recharts'; + +import mockEvents from './mockData'; +import styles from './ChartsSection.module.css'; + +function ChartsSection() { + const darkMode = useSelector(state => state.theme.darkMode); + + // Group data by event type + const eventTypeStats = []; + const groups = {}; + + mockEvents.forEach(evt => { + const key = evt.eventType; + + if (!groups[key]) { + groups[key] = { count: 0, noShowSum: 0, dropSum: 0 }; + } + + groups[key].count++; + groups[key].noShowSum += parseInt(evt.noShowRate, 10); + groups[key].dropSum += parseInt(evt.dropOffRate, 10); + }); + + for (const key in groups) { + eventTypeStats.push({ + eventType: key, + avgNoShow: Math.round(groups[key].noShowSum / groups[key].count), + avgDrop: Math.round(groups[key].dropSum / groups[key].count), + }); + } + + // Monthly trend + const monthlyTrend = {}; + + mockEvents.forEach(evt => { + const m = new Date(evt.eventDate).getMonth(); + + if (!monthlyTrend[m]) { + monthlyTrend[m] = { count: 0, noShowSum: 0 }; + } + + monthlyTrend[m].count++; + monthlyTrend[m].noShowSum += parseInt(evt.noShowRate, 10); + }); + + const trendData = Object.keys(monthlyTrend).map(m => ({ + month: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][m], + avgNoShow: Math.round(monthlyTrend[m].noShowSum / monthlyTrend[m].count), + })); + + // Location distribution + const locationGroups = {}; + + mockEvents.forEach(evt => { + const loc = evt.location; + if (!locationGroups[loc]) locationGroups[loc] = 0; + locationGroups[loc]++; + }); + + const locationData = Object.keys(locationGroups).map(loc => ({ + name: loc, + value: locationGroups[loc], + })); + + const pieColors = ['#007bff', '#00b894', '#e17055', '#6c5ce7', '#fdcb6e']; + + return ( +
+

Comparative Charts

+ + {/* Row 1 — Bar Charts */} +
+ {/* No-Show Chart */} +
+

No-show rate by event type

+ + + + + + + + +
+ + {/* Drop-Off Chart */} +
+

Drop-off rate by event type

+ + + + + + + + +
+
+ + {/* Row 2 — Line Chart */} +
+

Monthly no-show trend

+ + + + + + + + +
+ + {/* Row 3 — Pie Chart */} +
+

Participation by location

+ + + + {locationData.map((entry, index) => ( + + ))} + + + + +
+
+ ); +} + +export default ChartsSection; diff --git a/src/components/CommunityPortal/Reports/Participation/ChartsSection.module.css b/src/components/CommunityPortal/Reports/Participation/ChartsSection.module.css new file mode 100644 index 0000000000..597f481f78 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/ChartsSection.module.css @@ -0,0 +1,56 @@ +.chartsSection { + margin-top: 30px; + background: #ffffff; + padding: 25px; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.chartsSectionDark { + background: #1c2541; + color: #ffffff; + border: 1px solid #333; +} + +.sectionTitle { + font-size: 1.3rem; + font-weight: 600; + margin-bottom: 20px; +} + +.row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.chartBox { + background: #f8f9fa; + padding: 15px; + border-radius: 10px; + border: 1px solid #ddd; +} + +.chartsSectionDark .chartBox { + background: #3a506b; + border: 1px solid #555; +} + +.chartBox h4 { + margin-bottom: 10px; + font-size: 1rem; + font-weight: 600; +} + +.chartBoxFull { + margin-top: 20px; + background: #f8f9fa; + padding: 15px; + border-radius: 10px; + border: 1px solid #ddd; +} + +.chartsSectionDark .chartBoxFull { + background: #3a506b; + border: 1px solid #555; +} diff --git a/src/components/CommunityPortal/Reports/Participation/Demographics.jsx b/src/components/CommunityPortal/Reports/Participation/Demographics.jsx new file mode 100644 index 0000000000..84f1d2cb63 --- /dev/null +++ b/src/components/CommunityPortal/Reports/Participation/Demographics.jsx @@ -0,0 +1,24 @@ +import { useSelector } from 'react-redux'; +import styles from './Participation.module.css'; + +function Demographics() { + const darkMode = useSelector(state => state.theme.darkMode); + + return ( +
+

+ Demographics Overview +

+ +
+
+

Charts and breakdowns for age, gender, and location demographics will appear here.

+
+
+
+ ); +} + +export default Demographics; diff --git a/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx b/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx index 1b78c10355..b19a19722f 100644 --- a/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx +++ b/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx @@ -7,15 +7,15 @@ function DropOffTracking() { const [selectedEvent, setSelectedEvent] = useState('All Events'); const [selectedTime, setSelectedTime] = useState('All Time'); + const darkMode = useSelector(state => state.theme.darkMode); + const getDateRange = () => { const today = new Date(); let startDate, endDate; if (selectedTime === 'Today') { - startDate = new Date(today); - startDate.setHours(0, 0, 0, 0); - endDate = new Date(today); - endDate.setHours(23, 59, 59, 999); + startDate = new Date(today.setHours(0, 0, 0, 0)); + endDate = new Date(today.setHours(23, 59, 59, 999)); } else if (selectedTime === 'This Week') { startDate = new Date(today); startDate.setDate(today.getDate() - today.getDay()); @@ -33,36 +33,40 @@ function DropOffTracking() { }; const filteredEvents = mockEvents.filter(event => { - if (selectedEvent !== 'All Events' && event.eventType !== selectedEvent) { - return false; - } + if (selectedEvent !== 'All Events' && event.eventType !== selectedEvent) return false; + if (selectedTime !== 'All Time') { const { startDate, endDate } = getDateRange(); const eventDate = new Date(event.eventDate); return eventDate >= startDate && eventDate <= endDate; } + return true; }); - const darkMode = useSelector(state => state.theme.darkMode); - return ( -
+

Drop-off and no-show rate tracking

+
- setSelectedEvent(e.target.value)} + className={darkMode ? 'darkSelect' : ''} + > - setSelectedTime(e.target.value)} + className={darkMode ? 'darkSelect' : ''} + > @@ -77,15 +81,16 @@ function DropOffTracking() { +5% Last week

- Drop-off rate + Drop-off rate

+

+5% Last week

- No-show rate + No-show rate

@@ -95,11 +100,7 @@ function DropOffTracking() { darkMode ? styles.trackingListContainerDark : '' }`} > - +
@@ -108,6 +109,7 @@ function DropOffTracking() { + {filteredEvents.map(event => ( diff --git a/src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx b/src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx index f089462f70..6fddae5344 100644 --- a/src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx +++ b/src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx @@ -1,57 +1,61 @@ /* eslint-disable testing-library/no-node-access */ import { useSelector } from 'react-redux'; import { useRef, useState, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; import MyCases from './MyCases'; import DropOffTracking from './DropOffTracking'; import NoShowInsights from './NoShowInsights'; import styles from './Participation.module.css'; +import ChartsSection from './ChartsSection'; function EventParticipation() { const darkMode = useSelector(state => state.theme.darkMode); + const history = useHistory(); const exportRef = useRef(null); const [exporting, setExporting] = useState(false); const handleSaveAsPDF = useCallback(() => { - if (globalThis.window === undefined || globalThis.document === undefined) return; + if (!window || !document) return; if (exporting) return; - setExporting(true); + setExporting(true); document.documentElement.dataset.exporting = 'true'; - // Expand "More" so all visible items are included - const moreBtn = document.querySelector('.more-btn-global'); - const toggled = moreBtn?.textContent?.toLowerCase().includes('more') ?? false; - if (toggled) moreBtn.click(); + const moreBtn = document.querySelector(`[class*="moreBtn"]`); + const shouldExpand = moreBtn?.textContent?.toLowerCase().includes('more'); + + if (shouldExpand) moreBtn.click(); const prevTitle = document.title; document.title = 'event_participation'; setTimeout(() => { - globalThis.print(); + window.print(); setTimeout(() => { - if (toggled) moreBtn.click(); + if (shouldExpand) moreBtn.click(); delete document.documentElement.dataset.exporting; document.title = prevTitle; setExporting(false); - }, 100); + }, 120); }, 500); }, [exporting]); return (
- {/* Print-only page title header */} -
+ {/* PRINT-ONLY HEADER */} +
Social And Recreational Management
Event Participation
+ {/* PAGE HEADER */}
@@ -60,6 +64,7 @@ function EventParticipation() { > Social And Recreational Management +
+ {/* SUB-PAGE NAVIGATION BUTTONS */} +
+ + +
+ + {/* MY CASES (Top section) */} + {/* ANALYTICS SECTION */}
+ + {/* ACTIONABLE INSIGHTS SECTION */} +
+

Actionable insights

+ +
+
+

High no-show rate detected

+

+ Yoga Class events show an unusual increase in no-show percentage this month. +

+ ↑ 12% +
+ +
+

Weekend events perform better

+

+ Attendance is consistently higher on Saturdays compared to weekdays. +

+ ↑ 8% +
- {/* Print-only footer note */} -
- Generated from Event Participation +
+

Drop-off rate reduction opportunity

+

+ Average event drop-off decreases when host reminders are sent earlier. +

+ ↓ 5% +
+
+ {/* PRINT-ONLY FOOTER */} +
Generated from Event Participation
); } diff --git a/src/components/CommunityPortal/Reports/Participation/MyCases.jsx b/src/components/CommunityPortal/Reports/Participation/MyCases.jsx index a7d4a338b4..107aefaede 100644 --- a/src/components/CommunityPortal/Reports/Participation/MyCases.jsx +++ b/src/components/CommunityPortal/Reports/Participation/MyCases.jsx @@ -1,5 +1,7 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import Calendar from 'react-calendar'; import styles from './MyCases.module.css'; import mockEvents from './mockData'; @@ -7,15 +9,20 @@ function MyCases() { const [view, setView] = useState('card'); const [filter, setFilter] = useState('all'); const [expanded, setExpanded] = useState(false); + const [calendarDate, setCalendarDate] = useState(new Date()); + const history = useHistory(); + + const darkMode = useSelector(state => state.theme.darkMode); const isExporting = - typeof document !== 'undefined' && document.documentElement?.dataset?.exporting === 'true'; // Sonar: prefer .dataset + typeof document !== 'undefined' && document.documentElement?.dataset?.exporting === 'true'; const filterEvents = events => { const now = new Date(); + if (filter === 'today') { return events.filter(event => { - const eventDate = new Date(event.eventTime); + const eventDate = new Date(event.eventDate || event.eventTime); return ( eventDate.getDate() === now.getDate() && eventDate.getMonth() === now.getMonth() && @@ -23,30 +30,51 @@ function MyCases() { ); }); } + if (filter === 'thisWeek') { - const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay())); + const startOfWeek = new Date(now); + startOfWeek.setDate(now.getDate() - now.getDay()); + startOfWeek.setHours(0, 0, 0, 0); + const endOfWeek = new Date(startOfWeek); - endOfWeek.setDate(endOfWeek.getDate() + 6); + endOfWeek.setDate(startOfWeek.getDate() + 6); + endOfWeek.setHours(23, 59, 59, 999); + return events.filter(event => { - const eventDate = new Date(event.eventTime); + const eventDate = new Date(event.eventDate || 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); + endOfMonth.setHours(23, 59, 59, 999); + return events.filter(event => { - const eventDate = new Date(event.eventTime); + const eventDate = new Date(event.eventDate || event.eventTime); return eventDate >= startOfMonth && eventDate <= endOfMonth; }); } + return events; }; - const darkMode = useSelector(state => state.theme.darkMode); const filteredEvents = filterEvents(mockEvents); - // Sonar: extract nested ternary into independent statement + // Group events by YYYY-MM-DD for calendar view + const eventsByDate = useMemo(() => { + const map = {}; + filteredEvents.forEach(event => { + const baseDate = new Date(event.eventDate || event.eventTime); + const key = baseDate.toISOString().slice(0, 10); + if (!map[key]) map[key] = []; + map[key].push(event); + }); + return map; + }, [filteredEvents]); + + // Visible subset for card/list in non-export mode let visibleEvents = filteredEvents; if (!isExporting) { visibleEvents = expanded ? filteredEvents.slice(0, 40) : filteredEvents.slice(0, 10); @@ -87,7 +115,9 @@ function MyCases() {
{`+${event.attendees}`} + > + {`+${event.attendees}`} +
))} @@ -116,11 +146,73 @@ function MyCases() { ); - const renderCalendarView = () => ( -
-

Calendar View is under construction...

-
- ); + // --- Calendar View --- + + const renderCalendarTileContent = ({ date, view }) => { + if (view !== 'month') return null; + + const key = date.toISOString().slice(0, 10); + const dayEvents = eventsByDate[key]; + + if (!dayEvents || dayEvents.length === 0) return null; + + return
{dayEvents.length}
; + }; + + const renderCalendarView = () => { + const selectedKey = calendarDate.toISOString().slice(0, 10); + const selectedEvents = eventsByDate[selectedKey] || []; + + const formattedSelectedDate = calendarDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + return ( +
+
+ + {calendarDate.toLocaleString('en-US', { month: 'long', year: 'numeric' })} + +
+ + + +
+

Events on {formattedSelectedDate}

+ + {selectedEvents.length === 0 && ( +

No events scheduled for this day.

+ )} + + {selectedEvents.map(event => ( +
+
+ {event.eventName} + {event.eventType} +
+
+ {event.eventTime} + {event.location} + {`+${event.attendees} attendees`} +
+
+ ))} +
+
+ ); + }; return (
-
+
- - {filteredEvents.length > 10 && !isExporting && ( + {filteredEvents.length > 10 && !isExporting && view !== 'calendar' && (
Event nameAttendees