Files
scan-wave/src/routes/private/events/event/view/+page.svelte
2025-07-14 21:37:05 +02:00

371 lines
9.7 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/state';
// Import components
import EventInformation from './components/EventInformation.svelte';
import EmailTemplate from './components/EmailTemplate.svelte';
import GoogleAuthentication from './components/GoogleAuthentication.svelte';
import ParticipantsTable from './components/ParticipantsTable.svelte';
import EmailSending from './components/EmailSending.svelte';
import EmailResults from './components/EmailResults.svelte';
import Statistics from './components/Statistics.svelte';
import { toast } from '$lib/stores/toast.js';
let { data } = $props();
// Types
interface Event {
id: string;
created_at: string;
created_by: string;
name: string;
date: string;
section_id: string;
email_subject: string;
email_body: string;
sheet_id: string;
name_column: number;
surname_column: number;
email_column: number;
confirmation_column: number;
}
interface Participant {
id: string;
name: string;
surname: string;
email: string;
scanned: boolean;
email_sent: boolean;
}
// State management with Svelte 5 runes
let event = $state<Event | null>(null);
let participants = $state<Participant[]>([]);
let loading = $state(true);
let participantsLoading = $state(true);
let syncingParticipants = $state(false);
let sendingEmails = $state(false);
let updatingEmail = $state(false);
let emailProgress = $state({ sent: 0, total: 0 });
let emailResults = $state<{success: boolean, results: any[], summary: any} | null>(null);
// Get event ID from URL params
let eventId = $derived(page.url.searchParams.get('id'));
onMount(async () => {
if (eventId) {
await loadEvent();
await loadParticipants();
}
});
async function loadEvent() {
loading = true;
try {
const { data: eventData, error: eventError } = await data.supabase
.from('events')
.select('*')
.eq('id', eventId)
.single();
if (eventError) throw eventError;
event = eventData;
} catch (err) {
console.error('Error loading event:', err);
toast.error('Failed to load event');
} finally {
loading = false;
}
}
async function loadParticipants() {
participantsLoading = true;
try {
const { data: participantsData, error: participantsError } = await data.supabase
.from('participants')
.select('id, name, surname, email, scanned, email_sent')
.eq('event', eventId)
.order('scanned', { ascending: true })
.order('email_sent', { ascending: true })
.order('name', { ascending: true });
if (participantsError) throw participantsError;
participants = participantsData || [];
} catch (err) {
console.error('Error loading participants:', err);
toast.error('Failed to load participants');
} finally {
participantsLoading = false;
}
}
async function syncParticipants() {
if (!event || !event.sheet_id) {
toast.error('Cannot sync participants: No Google Sheet is connected to this event');
return;
}
// Check if user has Google authentication
const refreshToken = localStorage.getItem('google_refresh_token');
if (!refreshToken) {
toast.error('Please connect your Google account first to sync participants');
return;
}
syncingParticipants = true;
try {
// Fetch sheet data
const response = await fetch(`/private/api/google/sheets/${event.sheet_id}/data`, {
method: 'GET',
headers: {
Authorization: `Bearer ${refreshToken}`
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Google authentication expired. Please reconnect your Google account.');
}
throw new Error('Failed to fetch sheet data');
}
const sheetData = await response.json();
const rows = sheetData.values || [];
if (rows.length === 0) throw new Error('No data found in sheet');
// Extract participant data based on column mapping
const names: string[] = [];
const surnames: string[] = [];
const emails: string[] = [];
// Skip header row (start from index 1)
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (row.length > 0) {
const name = row[event.name_column - 1] || '';
const surname = row[event.surname_column - 1] || '';
const email = row[event.email_column - 1] || '';
const confirmation = row[event.confirmation_column - 1] || '';
// Only add if the row has meaningful data (not all empty) AND confirmation is TRUE
const isConfirmed =
confirmation.toString().toLowerCase() === 'true' ||
confirmation.toString().toLowerCase() === 'yes' ||
confirmation === '1' ||
confirmation === 'x';
if ((name.trim() || surname.trim() || email.trim()) && isConfirmed) {
names.push(name.trim());
surnames.push(surname.trim());
emails.push(email.trim());
}
}
}
// Call database function to add participants
const { error: syncError } = await data.supabase.rpc('participants_add_bulk', {
p_event: eventId,
p_names: names,
p_surnames: surnames,
p_emails: emails
});
if (syncError) throw syncError;
// Reload participants
await loadParticipants();
// Show success message with count of synced participants
const previousCount = participants.length;
const newCount = names.length;
const addedCount = Math.max(0, participants.length - previousCount);
toast.success(
`Successfully synced participants. ${newCount} entries processed, ${addedCount} new participants added.`,
5000
);
} catch (err) {
console.error('Error syncing participants:', err);
toast.error(`Failed to sync participants: ${err instanceof Error ? err.message : 'Unknown error'}`);
} finally {
syncingParticipants = false;
}
}
async function sendEmailsToUncontacted() {
if (!event) return;
// Check if user has Google authentication
const refreshToken = localStorage.getItem('google_refresh_token');
if (!refreshToken) {
toast.add({
message: 'Please connect your Google account first to send emails',
type: 'error'
});
return;
}
const uncontactedParticipants = participants.filter(p => !p.email_sent);
if (uncontactedParticipants.length === 0) {
toast.add({
message: 'No uncontacted participants found',
type: 'warning'
});
return;
}
sendingEmails = true;
emailProgress = { sent: 0, total: uncontactedParticipants.length };
emailResults = null;
try {
// Send all emails in batch
const response = await fetch('/private/api/google/gmail', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
participants: uncontactedParticipants,
subject: event.email_subject,
text: event.email_body,
eventId: event.id,
refreshToken: refreshToken
})
});
if (response.ok) {
const result = await response.json();
emailProgress.sent = result.summary.success;
emailResults = result;
// Update participants state to reflect email_sent status
participants = participants.map(p => {
const emailedParticipant = result.results.find((r: any) => r.participant.id === p.id);
if (emailedParticipant && emailedParticipant.success) {
return { ...p, email_sent: true };
}
return p;
});
} else {
const errorData = await response.json();
toast.add({
message: errorData.error || 'Failed to send emails',
type: 'error'
});
console.error('Email sending failed:', errorData);
}
} catch (err) {
console.error('Error sending emails:', err);
toast.add({
message: 'Failed to send emails to participants',
type: 'error'
});
} finally {
sendingEmails = false;
}
}
// For Email Template updating
async function handleEmailUpdate(eventId: string, subject: string, body: string) {
updatingEmail = true;
try {
// Call the email_modify RPC function
const { error } = await data.supabase.rpc('email_modify', {
p_event_id: eventId,
p_email_subject: subject,
p_email_body: body
});
if (error) throw error;
// Update the local event data on success
if (event) {
event.email_subject = subject;
event.email_body = body;
}
toast.add({
message: 'Email template updated successfully',
type: 'success'
});
} catch (err) {
console.error('Error updating email template:', err);
toast.add({
message: 'Failed to update email template',
type: 'error'
});
} finally {
updatingEmail = false;
}
}
function handleGoogleAuthSuccess() {
// Success handled by toast in the component
}
function handleGoogleAuthError(errorMsg: string) {
toast.add({
message: errorMsg,
type: 'error'
});
}
</script>
<div class="mt-2 mb-4">
<h1 class="text-center text-2xl font-bold">Event Overview</h1>
</div>
<EventInformation {event} {loading} />
<GoogleAuthentication
{loading}
onSuccess={handleGoogleAuthSuccess}
onError={handleGoogleAuthError}
/>
<ParticipantsTable
{event}
{participants}
{loading}
participantsLoading={participantsLoading}
syncingParticipants={syncingParticipants}
onSyncParticipants={syncParticipants}
/>
<div class="mb-4 rounded-lg border border-gray-300 bg-white p-6">
<h2 class="mb-4 text-xl font-semibold text-gray-900">Statistics</h2>
<Statistics
loading={loading || participantsLoading}
totalCount={participants.length}
scannedCount={participants.filter(p => p.scanned).length}
emailSentCount={participants.filter(p => p.email_sent).length}
pendingCount={participants.filter(p => !p.email_sent).length}
/>
</div>
<EmailTemplate
{event}
{loading}
{updatingEmail}
onUpdateEmail={handleEmailUpdate}
/>
<EmailSending
{loading}
{participants}
{sendingEmails}
{emailProgress}
{event}
onSendEmails={sendEmailsToUncontacted}
/>
{#if emailResults}
<EmailResults {emailResults} />
{/if}