monacousa-portal/src/routes/(app)/admin/bulk-email/+page.svelte

237 lines
7.2 KiB
Svelte

<script lang="ts">
import { Send, Mail, Users, Shield, CheckCircle, XCircle, Clock, AlertTriangle } from 'lucide-svelte';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { enhance } from '$app/forms';
import { invalidateAll } from '$app/navigation';
let { data, form } = $props();
const { broadcasts, recipientCounts } = data;
let isSending = $state(false);
let selectedTarget = $state('all');
let showConfirm = $state(false);
const targetOptions = [
{ value: 'all', label: 'All Members', count: recipientCounts.all },
{ value: 'active', label: 'Active Members Only', count: recipientCounts.active },
{ value: 'board', label: 'Board & Admins', count: recipientCounts.board },
{ value: 'admin', label: 'Admins Only', count: recipientCounts.admin }
];
const currentRecipientCount = $derived(
targetOptions.find(o => o.value === selectedTarget)?.count || 0
);
function getStatusBadge(status: string) {
switch (status) {
case 'completed':
return { color: 'text-green-600', bg: 'bg-green-50', label: 'Sent' };
case 'failed':
return { color: 'text-red-600', bg: 'bg-red-50', label: 'Failed' };
case 'sending':
return { color: 'text-blue-600', bg: 'bg-blue-50', label: 'Sending' };
default:
return { color: 'text-slate-500', bg: 'bg-slate-50', label: status };
}
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
</script>
<svelte:head>
<title>Bulk Email | Monaco USA</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-slate-900">Bulk Email Broadcast</h1>
<p class="text-slate-500">Send announcements and newsletters to members</p>
</div>
{#if form?.error}
<div class="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
<div class="flex items-center gap-2">
<AlertTriangle class="h-4 w-4" />
{form.error}
</div>
</div>
{/if}
{#if form?.success}
<div class="rounded-lg bg-green-50 border border-green-200 p-4 text-sm text-green-600">
<div class="flex items-center gap-2">
<CheckCircle class="h-4 w-4" />
{form.success}
</div>
</div>
{/if}
<!-- Compose Email -->
<div class="glass-card p-6">
<h2 class="mb-4 flex items-center gap-2 text-lg font-semibold text-slate-900">
<Mail class="h-5 w-5 text-monaco-600" />
Compose Broadcast
</h2>
<form
method="POST"
action="?/send"
use:enhance={() => {
isSending = true;
showConfirm = false;
return async ({ update }) => {
await invalidateAll();
await update();
isSending = false;
};
}}
class="space-y-4"
>
<!-- Recipients -->
<div>
<Label for="target">Recipients</Label>
<select
id="target"
name="target"
bind:value={selectedTarget}
class="mt-1 w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm"
>
{#each targetOptions as option}
<option value={option.value}>{option.label} ({option.count})</option>
{/each}
</select>
<p class="mt-1 text-xs text-slate-500">
This will send to <strong>{currentRecipientCount}</strong> recipients.
Email preferences will be respected (members can opt out of announcements).
</p>
</div>
<!-- Subject -->
<div>
<Label for="subject">Subject *</Label>
<Input
type="text"
id="subject"
name="subject"
required
placeholder="e.g., Monaco USA Monthly Newsletter - February 2026"
class="mt-1"
/>
</div>
<!-- Body -->
<div>
<Label for="body">Email Body *</Label>
<textarea
id="body"
name="body"
required
rows="10"
placeholder="Write your email content here. Use variables like first_name and last_name for personalization."
class="mt-1 w-full rounded-lg border border-slate-200 px-3 py-2 text-sm"
></textarea>
<p class="mt-1 text-xs text-slate-500">
Plain text with line breaks. Use {'{{first_name}}'} and {'{{last_name}}'} for personalization.
</p>
</div>
<!-- Send button -->
<div class="flex items-center gap-4 border-t border-slate-200 pt-4">
{#if !showConfirm}
<button
type="button"
onclick={() => (showConfirm = true)}
class="flex items-center gap-2 rounded-lg bg-monaco-600 px-6 py-2 text-sm font-medium text-white hover:bg-monaco-700"
>
<Send class="h-4 w-4" />
Send Broadcast
</button>
{:else}
<div class="flex items-center gap-3 rounded-lg bg-amber-50 border border-amber-200 px-4 py-3">
<AlertTriangle class="h-5 w-5 text-amber-600 shrink-0" />
<p class="text-sm text-amber-700">
Send to <strong>{currentRecipientCount}</strong> recipients? This cannot be undone.
</p>
<button
type="submit"
disabled={isSending}
class="shrink-0 rounded-lg bg-monaco-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-monaco-700 disabled:opacity-50"
>
{isSending ? 'Sending...' : 'Confirm Send'}
</button>
<button
type="button"
onclick={() => (showConfirm = false)}
class="shrink-0 text-sm text-slate-500 hover:text-slate-700"
>
Cancel
</button>
</div>
{/if}
</div>
</form>
</div>
<!-- Broadcast History -->
<div class="glass-card p-6">
<h2 class="mb-4 text-lg font-semibold text-slate-900">Broadcast History</h2>
{#if broadcasts.length === 0}
<div class="py-12 text-center">
<Mail class="mx-auto h-12 w-12 text-slate-300" />
<p class="mt-4 text-slate-500">No broadcasts sent yet.</p>
</div>
{:else}
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-200 text-left">
<th class="pb-3 pr-4 font-medium text-slate-600">Subject</th>
<th class="pb-3 pr-4 font-medium text-slate-600">Status</th>
<th class="pb-3 pr-4 font-medium text-slate-600">Recipients</th>
<th class="pb-3 pr-4 font-medium text-slate-600">Sent By</th>
<th class="pb-3 font-medium text-slate-600">Date</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{#each broadcasts as broadcast}
{@const statusBadge = getStatusBadge(broadcast.status)}
<tr class="hover:bg-slate-50">
<td class="py-3 pr-4 font-medium text-slate-900 max-w-xs truncate">
{broadcast.subject}
</td>
<td class="py-3 pr-4">
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {statusBadge.bg} {statusBadge.color}">
{statusBadge.label}
</span>
</td>
<td class="py-3 pr-4 text-slate-600">
{broadcast.sent_count}/{broadcast.total_recipients}
{#if broadcast.failed_count > 0}
<span class="text-red-500 text-xs">({broadcast.failed_count} failed)</span>
{/if}
</td>
<td class="py-3 pr-4 text-slate-500">
{broadcast.sent_by_name || 'Unknown'}
</td>
<td class="py-3 text-slate-500 whitespace-nowrap">
{broadcast.sent_at ? formatDate(broadcast.sent_at) : formatDate(broadcast.created_at)}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>