From 5697000b7ae57750553b46ebb5e2b37df752444e Mon Sep 17 00:00:00 2001
From: sachinkg12 <61034758+sachinkg12@users.noreply.github.com>
Date: Sun, 29 Mar 2026 14:26:32 -0700
Subject: [PATCH] Add CSV export and print support for admin volunteer tables
Adds export-to-CSV and print buttons to the volunteer management admin
page for all participant types (mentors, judges, volunteers, hackers,
sponsors). Also fixes a bug where participationCount crashes when the
value is numeric instead of a string.
Closes #229
---
src/components/admin/VolunteerTable.js | 171 ++++++++++++++++++++++++-
src/pages/admin/volunteer/index.js | 45 +++++++
2 files changed, 214 insertions(+), 2 deletions(-)
diff --git a/src/components/admin/VolunteerTable.js b/src/components/admin/VolunteerTable.js
index da6cb5d..5c0f635 100644
--- a/src/components/admin/VolunteerTable.js
+++ b/src/components/admin/VolunteerTable.js
@@ -39,7 +39,12 @@ import { styled } from "@mui/system";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import CancelIcon from "@mui/icons-material/Cancel";
import EditIcon from "@mui/icons-material/Edit";
-import { Email as EmailIcon, VolunteerActivism as CertificateIcon } from '@mui/icons-material';
+import {
+ Email as EmailIcon,
+ VolunteerActivism as CertificateIcon,
+ GetApp as DownloadIcon,
+ Print as PrintIcon,
+} from '@mui/icons-material';
import { FaPaperPlane, FaSlack, FaLinkedin } from 'react-icons/fa';
const StyledTableContainer = styled(TableContainer)(({ theme }) => ({
@@ -52,6 +57,20 @@ const StyledTableContainer = styled(TableContainer)(({ theme }) => ({
tableLayout: "auto", // Allow flexible column sizing
whiteSpace: "nowrap", // Prevent text wrapping in cells
},
+ // Print-friendly styles
+ "@media print": {
+ overflow: "visible",
+ maxWidth: "none",
+ "& .MuiTable-root": {
+ minWidth: "auto",
+ fontSize: "10px",
+ },
+ "& .MuiTableCell-root": {
+ padding: "4px 6px",
+ fontSize: "10px",
+ border: "1px solid #ccc",
+ },
+ },
// Better mobile scrolling
WebkitOverflowScrolling: 'touch', // Smooth scrolling on iOS
// Enhanced scrollbar for better touch interaction
@@ -374,6 +393,94 @@ const VolunteerTable = ({
setCopyFeedback({ open: false, message: '' });
};
+ // Extract plain text value from a volunteer for a given column (used for CSV export)
+ const getPlainCellValue = useCallback((volunteer, columnId) => {
+ switch (columnId) {
+ case "id":
+ return volunteer.id || "";
+ case "name":
+ return volunteer.name || "";
+ case "email":
+ return volunteer.email || "";
+ case "pronouns":
+ return volunteer.pronouns || "";
+ case "company":
+ return volunteer.company || "";
+ case "isInPerson":
+ return volunteer.isInPerson ? "Yes" : "No";
+ case "isSelected":
+ return volunteer.isSelected ? "Yes" : "No";
+ case "checkedIn":
+ return volunteer.checkedIn === true ? "Yes" : volunteer.checkedIn === false ? "No" : "Not set";
+ case "slack_user_id":
+ return volunteer.slack_user_id || "";
+ case "status":
+ return volunteer.status || "pending";
+ case "title":
+ return volunteer.title || "";
+ case "background":
+ return volunteer.background || "";
+ case "availability":
+ return volunteer.availability || "";
+ case "participationCount":
+ return volunteer.participationCount != null ? String(volunteer.participationCount) : "";
+ case "linkedin":
+ return volunteer.linkedin || "";
+ case "expertise":
+ return volunteer.expertise || "";
+ case "country":
+ return volunteer.country || "";
+ case "state":
+ return volunteer.state || "";
+ case "teamCode":
+ return volunteer.teamCode || "";
+ case "participantType":
+ return volunteer.participantType || "";
+ case "experienceLevel":
+ return volunteer.experienceLevel || "";
+ case "teamStatus":
+ return volunteer.teamStatus || "";
+ case "primaryRoles":
+ return Array.isArray(volunteer.primaryRoles) ? volunteer.primaryRoles.join("; ") : (volunteer.primaryRoles || "");
+ case "availableDays":
+ return Array.isArray(volunteer.availableDays) ? volunteer.availableDays.join("; ") : "";
+ case "socialCauses":
+ return volunteer.socialCauses || volunteer.otherSocialCause || "";
+ case "artifacts":
+ return Array.isArray(volunteer.artifacts) ? volunteer.artifacts.map(a => a.label).join("; ") : "";
+ case "messages_sent":
+ const emails = getSentEmails(volunteer);
+ return String(emails.length);
+ case "created_timestamp":
+ return volunteer.created_timestamp ? new Date(volunteer.created_timestamp).toLocaleDateString() : "";
+ case "sponsorshipTypes":
+ return volunteer.sponsorshipTypes || volunteer.sponsorshipTier || volunteer.sponsorshipLevel || "";
+ case "phoneNumber":
+ return volunteer.phoneNumber || "";
+ case "preferredContact":
+ return volunteer.preferredContact || "";
+ case "useLogo":
+ return volunteer.useLogo || "";
+ case "volunteerType":
+ return volunteer.volunteerType || "";
+ case "volunteerCount":
+ return volunteer.volunteerCount || "0";
+ case "volunteerHours":
+ return volunteer.volunteerHours || "0";
+ default:
+ return volunteer[columnId] != null ? String(volunteer[columnId]) : "";
+ }
+ }, [getSentEmails]);
+
+ // Escape a value for CSV (handle commas, quotes, newlines)
+ const escapeCsvValue = (value) => {
+ const str = String(value);
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
+ return `"${str.replace(/"/g, '""')}"`;
+ }
+ return str;
+ };
+
const columns = useMemo(() => {
const baseColumns = [
{ id: "id", label: "ID", minWidth: 60 }, // Reduced from 100
@@ -504,6 +611,42 @@ const VolunteerTable = ({
return filtered;
}, [volunteers, checkedInFilter]);
+ // Export filtered volunteers to CSV
+ const exportToCsv = useCallback(() => {
+ if (filteredVolunteers.length === 0) {
+ setCopyFeedback({ open: true, message: 'No data to export' });
+ return;
+ }
+
+ const headers = columns.map(col => col.label);
+ const rows = filteredVolunteers.map(volunteer =>
+ columns.map(col => escapeCsvValue(getPlainCellValue(volunteer, col.id)))
+ );
+
+ const csvContent = [
+ headers.map(escapeCsvValue).join(','),
+ ...rows.map(row => row.join(','))
+ ].join('\n');
+
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ const url = URL.createObjectURL(blob);
+ link.setAttribute('href', url);
+ link.setAttribute('download', `${type}-export.csv`);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+
+ setCopyFeedback({ open: true, message: `Exported ${filteredVolunteers.length} ${type} to CSV` });
+ }, [filteredVolunteers, columns, type, getPlainCellValue]);
+
+ // Print the current table
+ const handlePrint = useCallback(() => {
+ window.print();
+ }, []);
+
const renderCellContent = (volunteer, column) => {
switch (column.id) {
case "id":
@@ -1324,7 +1467,7 @@ const VolunteerTable = ({
);
case "participationCount":
- const participation = volunteer.participationCount || "";
+ const participation = String(volunteer.participationCount || "");
if (!participation) return