added search and csv

This commit is contained in:
chark1es 2025-02-16 05:54:34 -08:00
parent cc99d414a2
commit 2712522bf6

View file

@ -2,6 +2,39 @@ import { useEffect, useState } from 'react';
import { Get } from '../../pocketbase/Get';
import { Authentication } from '../../pocketbase/Authentication';
// Add HighlightText component
const HighlightText = ({ text, searchTerms }: { text: string | number | null | undefined, searchTerms: string[] }) => {
// Convert input to string and handle null/undefined
const textStr = String(text ?? '');
if (!searchTerms.length || !textStr) return <>{textStr}</>;
try {
const escapedTerms = searchTerms.map(term =>
term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
);
const parts = textStr.split(new RegExp(`(${escapedTerms.join('|')})`, 'gi'));
return (
<>
{parts.map((part, i) => {
const isMatch = searchTerms.some(term =>
part.toLowerCase().includes(term.toLowerCase())
);
return isMatch ? (
<mark key={i} className="bg-primary/20 rounded px-1">{part}</mark>
) : (
<span key={i}>{part}</span>
);
})}
</>
);
} catch (error) {
console.error('Error in HighlightText:', error);
return <>{textStr}</>;
}
};
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
@ -12,19 +45,29 @@ interface User {
id: string;
name: string;
email: string;
pid: string;
member_id: string;
member_type: string;
graduation_year: string;
major: string;
}
interface Event {
id: string;
event_name: string;
attendees: AttendeeEntry[];
}
export default function Attendees() {
const [eventId, setEventId] = useState<string>('');
const [eventName, setEventName] = useState<string>('');
const [users, setUsers] = useState<Map<string, User>>(new Map());
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [attendeesList, setAttendeesList] = useState<AttendeeEntry[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [filteredAttendees, setFilteredAttendees] = useState<AttendeeEntry[]>([]);
const [processedSearchTerms, setProcessedSearchTerms] = useState<string[]>([]);
const get = Get.getInstance();
const auth = Authentication.getInstance();
@ -34,6 +77,7 @@ export default function Attendees() {
const handleUpdateAttendees = (e: CustomEvent<{ eventId: string; eventName: string }>) => {
console.log('Received updateAttendees event:', e.detail);
setEventId(e.detail.eventId);
setEventName(e.detail.eventName);
};
// Add event listener
@ -45,6 +89,92 @@ export default function Attendees() {
};
}, []);
// Filter attendees when search term or attendees list changes
useEffect(() => {
if (!searchTerm.trim()) {
setFilteredAttendees(attendeesList);
setProcessedSearchTerms([]);
return;
}
const terms = searchTerm.toLowerCase().split(/\s+/).filter(Boolean);
setProcessedSearchTerms(terms);
const filtered = attendeesList.filter(attendee => {
const user = users.get(attendee.user_id);
if (!user) return false;
const searchableValues = [
user.name,
user.email,
user.pid,
user.member_id,
user.member_type,
user.graduation_year,
user.major,
attendee.food,
new Date(attendee.time_checked_in).toLocaleString(),
].map(value => (value || '').toString().toLowerCase());
return terms.every(term =>
searchableValues.some(value => value.includes(term))
);
});
setFilteredAttendees(filtered);
}, [searchTerm, attendeesList, users]);
// Function to download attendees as CSV
const downloadAttendeesCSV = () => {
// Create CSV headers
const headers = [
'Name',
'Email',
'PID',
'Member ID',
'Member Type',
'Graduation Year',
'Major',
'Check-in Time',
'Food Choice'
];
// Create CSV rows
const rows = attendeesList.map(attendee => {
const user = users.get(attendee.user_id);
const checkInTime = new Date(attendee.time_checked_in).toLocaleString();
return [
user?.name || 'Unknown User',
user?.email || 'N/A',
user?.pid || 'N/A',
user?.member_id || 'N/A',
user?.member_type || 'N/A',
user?.graduation_year || 'N/A',
user?.major || 'N/A',
checkInTime,
attendee.food || 'N/A'
];
});
// Combine headers and rows
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');
// Create blob and download
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', `${eventName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_attendees.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Fetch event data when eventId changes
useEffect(() => {
const fetchEventData = async () => {
@ -168,35 +298,88 @@ export default function Attendees() {
}
return (
<div className="overflow-x-auto">
<div className="mb-4 text-sm opacity-70">
Total Attendees: {attendeesList.length}
</div>
<table className="table table-zebra w-full">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Check-in Time</th>
<th>Food Choice</th>
</tr>
</thead>
<tbody>
{attendeesList.map((attendee, index) => {
const user = users.get(attendee.user_id);
const checkInTime = new Date(attendee.time_checked_in).toLocaleString();
<div className="flex flex-col h-[calc(100vh-16rem)]">
<div className="flex flex-col gap-4 mb-4">
{/* Search and Actions Row */}
<div className="flex justify-between items-center gap-4">
<div className="flex-1 flex gap-2">
<div className="join flex-1">
<div className="join-item bg-base-200 flex items-center px-3">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 opacity-70" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" />
</svg>
</div>
<input
type="text"
placeholder="Search attendees..."
className="input input-bordered join-item w-full"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<button
className="btn btn-primary btn-sm gap-2"
onClick={downloadAttendeesCSV}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
Download CSV
</button>
</div>
return (
<tr key={`${attendee.user_id}-${index}`}>
<td>{user?.name || 'Unknown User'}</td>
<td>{user?.email || 'N/A'}</td>
<td>{checkInTime}</td>
<td>{attendee.food || 'N/A'}</td>
</tr>
);
})}
</tbody>
</table>
{/* Stats Row */}
<div className="flex justify-between items-center">
<div className="text-sm opacity-70">
Total Attendees: {attendeesList.length}
</div>
{searchTerm && (
<div className="text-sm opacity-70">
Showing: {filteredAttendees.length} matches
</div>
)}
</div>
</div>
{/* Updated table with highlighting */}
<div className="overflow-x-auto flex-1">
<table className="table table-zebra w-full">
<thead className="sticky top-0 bg-base-100">
<tr>
<th className="bg-base-100">Name</th>
<th className="bg-base-100">Email</th>
<th className="bg-base-100">PID</th>
<th className="bg-base-100">Member ID</th>
<th className="bg-base-100">Member Type</th>
<th className="bg-base-100">Graduation Year</th>
<th className="bg-base-100">Major</th>
<th className="bg-base-100">Check-in Time</th>
<th className="bg-base-100">Food Choice</th>
</tr>
</thead>
<tbody>
{filteredAttendees.map((attendee, index) => {
const user = users.get(attendee.user_id);
const checkInTime = new Date(attendee.time_checked_in).toLocaleString();
return (
<tr key={`${attendee.user_id}-${index}`}>
<td><HighlightText text={user?.name || 'Unknown User'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.email || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.pid || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.member_id || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.member_type || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.graduation_year || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.major || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={checkInTime} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={attendee.food || 'N/A'} searchTerms={processedSearchTerms} /></td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}