371 lines
9.7 KiB
Svelte
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}
|