added attendees
This commit is contained in:
parent
4f44ead2f6
commit
cc99d414a2
2 changed files with 298 additions and 28 deletions
|
@ -4,6 +4,7 @@ import { Get } from "../pocketbase/Get";
|
|||
import { Authentication } from "../pocketbase/Authentication";
|
||||
import EventEditor from "./Officer_EventManagement/EventEditor";
|
||||
import FilePreview from "./Officer_EventManagement/FilePreview";
|
||||
import Attendees from "./Officer_EventManagement/Attendees";
|
||||
|
||||
// Get instances
|
||||
const get = Get.getInstance();
|
||||
|
@ -648,7 +649,7 @@ const currentPage = eventResponse.page;
|
|||
</div>
|
||||
|
||||
<div id="attendeesContent" class="space-y-4 hidden">
|
||||
<!-- Attendees content -->
|
||||
<Attendees client:load eventId="" attendees={[]} />
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
|
@ -688,6 +689,26 @@ const currentPage = eventResponse.page;
|
|||
</form>
|
||||
</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>
|
||||
import { Get } from "../pocketbase/Get";
|
||||
import { Authentication } from "../pocketbase/Authentication";
|
||||
|
@ -1164,6 +1185,11 @@ const currentPage = eventResponse.page;
|
|||
<polyline points="14 2 14 8 20 8" />
|
||||
</svg>
|
||||
</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}'])">
|
||||
<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" />
|
||||
|
@ -1892,10 +1918,8 @@ const currentPage = eventResponse.page;
|
|||
}
|
||||
|
||||
// Update the openDetailsModal function to use the universal preview
|
||||
window.openDetailsModal = function (event: Event) {
|
||||
const modal = document.getElementById(
|
||||
"eventDetailsModal"
|
||||
) as HTMLDialogElement;
|
||||
window.openDetailsModal = function (event: Event, activeTab: string = 'files') {
|
||||
const modal = document.getElementById("eventDetailsModal") as HTMLDialogElement;
|
||||
const filesContent = document.getElementById("filesContent");
|
||||
const attendeesContent = document.getElementById("attendeesContent");
|
||||
const tabs = document.querySelectorAll('.tabs .tab');
|
||||
|
@ -1905,6 +1929,46 @@ const currentPage = eventResponse.page;
|
|||
// Show modal
|
||||
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
|
||||
tabs.forEach(tab => {
|
||||
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
|
||||
|
@ -2352,4 +2393,31 @@ const currentPage = eventResponse.page;
|
|||
window.previewFileOfficer = previewFileOfficer;
|
||||
window.closeFilePreviewOfficer = closeFilePreviewOfficer;
|
||||
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>
|
||||
|
|
202
src/components/dashboard/Officer_EventManagement/Attendees.tsx
Normal file
202
src/components/dashboard/Officer_EventManagement/Attendees.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue