Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions src/components/CommunityPortal/Reports/Participation/ChartsSection.jsx
Original file line number Diff line number Diff line change
@@ -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 += Number.parseInt(evt.noShowRate, 10);
groups[key].dropSum += Number.parseInt(evt.dropOffRate, 10);
});

Object.entries(groups).forEach(([key, stats]) => {
eventTypeStats.push({
eventType: key,
avgNoShow: Math.round(stats.noShowSum / stats.count),
avgDrop: Math.round(stats.dropSum / stats.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 += Number.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 (
<div className={`${styles.chartsSection} ${darkMode ? styles.chartsSectionDark : ''}`}>
<h3 className={styles.sectionTitle}>Comparative Charts</h3>

{/* Row 1 — Bar Charts */}
<div className={styles.row}>
{/* No-Show Chart */}
<div className={styles.chartBox}>
<h4>No-show rate by event type</h4>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={eventTypeStats}>
<XAxis dataKey="eventType" stroke={darkMode ? '#fff' : '#333'} />
<YAxis stroke={darkMode ? '#fff' : '#333'} />
<Tooltip />
<Bar dataKey="avgNoShow" fill="#FF6B6B" />
</BarChart>
</ResponsiveContainer>
</div>

{/* Drop-Off Chart */}
<div className={styles.chartBox}>
<h4>Drop-off rate by event type</h4>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={eventTypeStats}>
<XAxis dataKey="eventType" stroke={darkMode ? '#fff' : '#333'} />
<YAxis stroke={darkMode ? '#fff' : '#333'} />
<Tooltip />
<Bar dataKey="avgDrop" fill="#4C89FF" />
</BarChart>
</ResponsiveContainer>
</div>
</div>

{/* Row 2 — Line Chart */}
<div className={styles.chartBoxFull}>
<h5>Monthly no-show trend</h5>
<ResponsiveContainer width="100%" height={280}>
<LineChart data={trendData}>
<XAxis dataKey="month" stroke={darkMode ? '#fff' : '#333'} />
<YAxis stroke={darkMode ? '#fff' : '#333'} />
<Tooltip />
<Line type="monotone" dataKey="avgNoShow" stroke="#FF6B6B" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
</div>

{/* Row 3 — Pie Chart */}
<div className={styles.chartBoxFull}>
<h5>Participation by location</h5>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie data={locationData} dataKey="value" nameKey="name" outerRadius={110} label>
{locationData.map((entry, index) => (
<Cell key={entry.name} fill={pieColors[index % pieColors.length]} />
))}
</Pie>
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
</div>
);
}

export default ChartsSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.chartsSection {
margin-top: 30px;
background: #fff;
padding: 25px;
border-radius: 10px;
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
}

.chartsSectionDark {
background: #1c2541;
color: #fff;
border: 1px solid #333;
}

.sectionTitle {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 20px;
color: #333;
}

.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;
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`${styles.demographicsPage} ${darkMode ? styles.demographicsPageDark : ''}`}>
<h2
className={`${styles.demographicsHeader} ${darkMode ? styles.demographicsHeaderDark : ''}`}
>
Demographics Overview
</h2>

<div className={styles.demographicsContent}>
<div className={styles.placeholderBox}>
<p>Charts and breakdowns for age, gender, and location demographics will appear here.</p>
</div>
</div>
</div>
);
}

export default Demographics;
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
/* 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFilePdf } from '@fortawesome/free-solid-svg-icons';
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 (typeof globalThis.window === 'undefined' || typeof document === 'undefined') return;

Check warning on line 20 in src/components/CommunityPortal/Reports/Participation/EventParticipation.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Compare with `undefined` directly instead of using `typeof`.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ6_NRqUj7SaPbKUtA5l&open=AZ6_NRqUj7SaPbKUtA5l&pullRequest=5304
if (exporting) return;
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 shouldExpand = moreBtn?.textContent?.toLowerCase().includes('more');
if (shouldExpand) moreBtn.click();

const prevTitle = document.title;
document.title = 'event_participation';

setTimeout(() => {
globalThis.print();
globalThis.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]);

Expand Down Expand Up @@ -76,13 +79,63 @@
</button>
</header>

<div className={styles.subPageNav}>
<button
className={`${styles.subPageBtn} ${darkMode ? styles.subPageBtnDark : ''}`}
onClick={() => history.push('/communityportal/reports/participation/demographics')}
>
Demographics
</button>
<button
className={`${styles.subPageBtn} ${darkMode ? styles.subPageBtnDark : ''}`}
onClick={() => history.push('/communityportal/reports/participation/personalization')}
>
Personalization
</button>
</div>

<MyCases />
<div className={`${styles.analyticsSection}`}>
<DropOffTracking />
<NoShowInsights />
</div>
<ChartsSection />

{/* ACTIONABLE INSIGHTS SECTION */}
<div
className={`${styles.actionableSection} ${darkMode ? styles.actionableSectionDark : ''}`}
>
<h3 className={styles.actionableHeader}>Actionable insights</h3>

{/* Print-only footer note */}
<div className={styles.actionableGrid}>
<div className={styles.actionCard}>
<h4 className={styles.actionTitle}>High no-show rate detected</h4>
<p className={styles.actionDescription}>
Yoga Class events show an unusual increase in no-show percentage this month.
</p>
<span className={styles.actionTrendUp}>↑ 12%</span>
</div>

<div className={styles.actionCard}>
<h4 className={styles.actionTitle}>Weekend events perform better</h4>
<p className={styles.actionDescription}>
Attendance is consistently higher on Saturdays compared to weekdays.
</p>
<span className={styles.actionTrendUp}>↑ 8%</span>
</div>

<div className={`${styles.printOnly} ${styles.printFooter}`}>
Generated from Event Participation
<div className={styles.actionCard}>
<h4 className={styles.actionTitle}>Drop-off rate reduction opportunity</h4>
<p className={styles.actionDescription}>
Average event drop-off decreases when host reminders are sent earlier.
</p>
<span className={styles.actionTrendDown}>↓ 5%</span>
</div>
</div>
</div>
</div>
</div>
);
}
Expand Down
Loading
Loading