added search and csv
This commit is contained in:
parent
cc99d414a2
commit
2712522bf6
1 changed files with 211 additions and 28 deletions
|
@ -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">
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<thead className="sticky top-0 bg-base-100">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Check-in Time</th>
|
||||
<th>Food Choice</th>
|
||||
<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>
|
||||
{attendeesList.map((attendee, index) => {
|
||||
{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>{user?.name || 'Unknown User'}</td>
|
||||
<td>{user?.email || 'N/A'}</td>
|
||||
<td>{checkInTime}</td>
|
||||
<td>{attendee.food || 'N/A'}</td>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue