added attendees

This commit is contained in:
chark1es 2025-02-16 05:42:17 -08:00
parent 4f44ead2f6
commit cc99d414a2
2 changed files with 298 additions and 28 deletions

View file

@ -4,6 +4,7 @@ import { Get } from "../pocketbase/Get";
import { Authentication } from "../pocketbase/Authentication"; import { Authentication } from "../pocketbase/Authentication";
import EventEditor from "./Officer_EventManagement/EventEditor"; import EventEditor from "./Officer_EventManagement/EventEditor";
import FilePreview from "./Officer_EventManagement/FilePreview"; import FilePreview from "./Officer_EventManagement/FilePreview";
import Attendees from "./Officer_EventManagement/Attendees";
// Get instances // Get instances
const get = Get.getInstance(); const get = Get.getInstance();
@ -648,7 +649,7 @@ const currentPage = eventResponse.page;
</div> </div>
<div id="attendeesContent" class="space-y-4 hidden"> <div id="attendeesContent" class="space-y-4 hidden">
<!-- Attendees content --> <Attendees client:load eventId="" attendees={[]} />
</div> </div>
</div> </div>
<form method="dialog" class="modal-backdrop"> <form method="dialog" class="modal-backdrop">
@ -688,6 +689,26 @@ const currentPage = eventResponse.page;
</form> </form>
</dialog> </dialog>
<!-- Add a separate Attendees Modal -->
<dialog id="attendeesModal" class="modal">
<div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg" id="attendeesModalTitle"></h3>
<button class="btn btn-circle btn-ghost" onclick="attendeesModal.close()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div id="attendeesModalContent">
<Attendees client:load />
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script> <script>
import { Get } from "../pocketbase/Get"; import { Get } from "../pocketbase/Get";
import { Authentication } from "../pocketbase/Authentication"; import { Authentication } from "../pocketbase/Authentication";
@ -1164,6 +1185,11 @@ const currentPage = eventResponse.page;
<polyline points="14 2 14 8 20 8" /> <polyline points="14 2 14 8 20 8" />
</svg> </svg>
</button> </button>
<button class="btn btn-ghost btn-sm" onclick="window.openAttendeesModal(window['${eventDataId}'])">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
</button>
<button class="btn btn-ghost btn-sm" onclick="window.openEditModal(window['${eventDataId}'])"> <button class="btn btn-ghost btn-sm" onclick="window.openEditModal(window['${eventDataId}'])">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" /> <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
@ -1892,10 +1918,8 @@ const currentPage = eventResponse.page;
} }
// Update the openDetailsModal function to use the universal preview // Update the openDetailsModal function to use the universal preview
window.openDetailsModal = function (event: Event) { window.openDetailsModal = function (event: Event, activeTab: string = 'files') {
const modal = document.getElementById( const modal = document.getElementById("eventDetailsModal") as HTMLDialogElement;
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent"); const filesContent = document.getElementById("filesContent");
const attendeesContent = document.getElementById("attendeesContent"); const attendeesContent = document.getElementById("attendeesContent");
const tabs = document.querySelectorAll('.tabs .tab'); const tabs = document.querySelectorAll('.tabs .tab');
@ -1905,6 +1929,46 @@ const currentPage = eventResponse.page;
// Show modal // Show modal
modal.showModal(); modal.showModal();
// Update files list
if (event.files && event.files.length > 0) {
filesContent.innerHTML = `
<div class="space-y-2">
${updateFilePreviewButtons(event.files, event.id)}
</div>
`;
} else {
filesContent.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<p>No files attached to this event</p>
</div>
`;
}
// Update Attendees component props initially
const attendeesComponent = attendeesContent.querySelector('astro-island');
if (attendeesComponent) {
const component = attendeesComponent.querySelector('[data-astro-cid]');
if (component) {
component.setAttribute('eventId', event.id);
component.setAttribute('attendees', JSON.stringify(event.attendees || []));
}
}
// Show the requested tab
const targetTab = Array.from(tabs).find(tab => (tab as HTMLElement).dataset.tab === activeTab);
if (targetTab) {
tabs.forEach(t => t.classList.remove('tab-active'));
targetTab.classList.add('tab-active');
if (activeTab === 'files') {
filesContent.classList.remove('hidden');
attendeesContent.classList.add('hidden');
} else {
filesContent.classList.add('hidden');
attendeesContent.classList.remove('hidden');
}
}
// Add tab functionality // Add tab functionality
tabs.forEach(tab => { tabs.forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {
@ -1921,29 +1985,6 @@ const currentPage = eventResponse.page;
} }
}); });
}); });
// Update files list
if (event.files && event.files.length > 0) {
filesContent.innerHTML = `
<div class="space-y-2">
${updateFilePreviewButtons(event.files, event.id)}
</div>
`;
} else {
filesContent.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<p>No files attached to this event</p>
</div>
`;
}
// Show files tab by default
const filesTab = Array.from(tabs).find(tab => (tab as HTMLElement).dataset.tab === 'files');
if (filesTab) {
filesTab.classList.add('tab-active');
filesContent.classList.remove('hidden');
attendeesContent.classList.add('hidden');
}
}; };
// Add file input change handler to show selected files // Add file input change handler to show selected files
@ -2352,4 +2393,31 @@ const currentPage = eventResponse.page;
window.previewFileOfficer = previewFileOfficer; window.previewFileOfficer = previewFileOfficer;
window.closeFilePreviewOfficer = closeFilePreviewOfficer; window.closeFilePreviewOfficer = closeFilePreviewOfficer;
window.showFilePreviewOfficer = showFilePreviewOfficer; window.showFilePreviewOfficer = showFilePreviewOfficer;
// Add openAttendeesModal function
window.openAttendeesModal = function (event: Event) {
console.log('Opening attendees modal for event:', event.id);
const modal = document.getElementById("attendeesModal") as HTMLDialogElement;
const modalTitle = document.getElementById("attendeesModalTitle");
if (!modal || !modalTitle) {
console.error('Missing required elements');
return;
}
// Set modal title
modalTitle.textContent = `Attendees - ${event.event_name}`;
// Dispatch custom event with event data
window.dispatchEvent(new CustomEvent('updateAttendees', {
detail: {
eventId: event.id,
eventName: event.event_name
}
}));
// Show modal
modal.showModal();
};
</script> </script>

View file

@ -0,0 +1,202 @@
import { useEffect, useState } from 'react';
import { Get } from '../../pocketbase/Get';
import { Authentication } from '../../pocketbase/Authentication';
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
food: string;
}
interface User {
id: string;
name: string;
email: string;
}
interface Event {
id: string;
attendees: AttendeeEntry[];
}
export default function Attendees() {
const [eventId, setEventId] = 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 get = Get.getInstance();
const auth = Authentication.getInstance();
// Listen for the custom event
useEffect(() => {
const handleUpdateAttendees = (e: CustomEvent<{ eventId: string; eventName: string }>) => {
console.log('Received updateAttendees event:', e.detail);
setEventId(e.detail.eventId);
};
// Add event listener
window.addEventListener('updateAttendees', handleUpdateAttendees as EventListener);
// Cleanup
return () => {
window.removeEventListener('updateAttendees', handleUpdateAttendees as EventListener);
};
}, []);
// Fetch event data when eventId changes
useEffect(() => {
const fetchEventData = async () => {
if (!eventId) {
console.log('No eventId provided');
return;
}
if (!auth.isAuthenticated()) {
console.log('User not authenticated');
setError('Authentication required');
return;
}
try {
setLoading(true);
setError(null);
console.log('Fetching event data for:', eventId);
const event = await get.getOne<Event>('events', eventId);
if (!event.attendees || !Array.isArray(event.attendees)) {
console.log('No attendees found or invalid format');
setAttendeesList([]);
return;
}
console.log('Found attendees:', {
count: event.attendees.length,
sample: event.attendees.slice(0, 2)
});
setAttendeesList(event.attendees);
// Fetch user details
const userIds = [...new Set(event.attendees.map(a => a.user_id))];
console.log('Fetching details for users:', userIds);
const userPromises = userIds.map(async (userId) => {
try {
return await get.getOne<User>('users', userId);
} catch (error) {
console.error(`Failed to fetch user ${userId}:`, error);
return null;
}
});
const userResults = await Promise.all(userPromises);
const userMap = new Map<string, User>();
userResults.forEach(user => {
if (user) {
userMap.set(user.id, user);
}
});
console.log('Fetched user details:', {
totalUsers: userMap.size,
userIds: Array.from(userMap.keys())
});
setUsers(userMap);
} catch (error) {
console.error('Failed to fetch event data:', error);
setError('Failed to load event data');
setAttendeesList([]);
} finally {
setLoading(false);
}
};
fetchEventData();
}, [eventId]); // Re-run when eventId changes
// Reset state when modal is closed
useEffect(() => {
const handleModalClose = () => {
setEventId('');
setAttendeesList([]);
setUsers(new Map());
setError(null);
};
const modal = document.getElementById('attendeesModal');
if (modal) {
modal.addEventListener('close', handleModalClose);
return () => modal.removeEventListener('close', handleModalClose);
}
}, []);
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<span className="loading loading-spinner loading-lg"></span>
</div>
);
}
if (error) {
return (
<div className="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{error}</span>
</div>
);
}
if (!eventId) {
return null;
}
if (!attendeesList || attendeesList.length === 0) {
return (
<div className="text-center py-8 text-base-content/70">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mx-auto mb-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
<p>No attendees yet</p>
</div>
);
}
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();
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>
</div>
);
}