329 lines
11 KiB
Svelte
329 lines
11 KiB
Svelte
<script>
|
|
import { onMount } from 'svelte'
|
|
import { api } from '../api.js'
|
|
import { db } from '../db.js'
|
|
|
|
let loading = $state(true)
|
|
let saving = $state(false)
|
|
let savingEvent = $state(false)
|
|
let testing = $state(false)
|
|
let error = $state('')
|
|
let success = $state('')
|
|
|
|
let smtpHost = $state('')
|
|
let smtpPort = $state(587)
|
|
let smtpUser = $state('')
|
|
let smtpPassword = $state('')
|
|
let smtpFrom = $state('')
|
|
let smtpFromName = $state('')
|
|
let baseURL = $state('')
|
|
let testEmail = $state('')
|
|
let noteLabel = $state('Additional note')
|
|
let noteRequired = $state(false)
|
|
let eventName = $state('')
|
|
let eventVenue = $state('')
|
|
let eventStartDate = $state('')
|
|
let eventEndDate = $state('')
|
|
let eventTimezone = $state('')
|
|
const timezones = Intl.supportedValuesOf('timeZone')
|
|
let shiftSignupsOpen = $state(false)
|
|
let togglingSignups = $state(false)
|
|
|
|
onMount(async () => {
|
|
try {
|
|
const ev = await api.event.get()
|
|
eventName = ev.name ?? ''
|
|
eventVenue = ev.venue ?? ''
|
|
eventStartDate = ev.start_date ?? ''
|
|
eventEndDate = ev.end_date ?? ''
|
|
eventTimezone = ev.timezone ?? ''
|
|
} catch {}
|
|
try {
|
|
const s = await api.settings.get()
|
|
smtpHost = s.smtp_host ?? ''
|
|
smtpPort = s.smtp_port ?? 587
|
|
smtpUser = s.smtp_user ?? ''
|
|
smtpPassword = '' // never pre-filled
|
|
smtpFrom = s.smtp_from ?? ''
|
|
smtpFromName = s.smtp_from_name ?? ''
|
|
baseURL = s.base_url ?? ''
|
|
noteLabel = s.volunteer_note_label ?? 'Additional note'
|
|
noteRequired = s.volunteer_note_required ?? false
|
|
shiftSignupsOpen = s.shift_signups_open ?? false
|
|
} catch (err) {
|
|
error = err.message
|
|
} finally {
|
|
loading = false
|
|
}
|
|
})
|
|
|
|
async function saveEvent(e) {
|
|
e.preventDefault()
|
|
savingEvent = true
|
|
error = ''
|
|
success = ''
|
|
try {
|
|
const updated = await api.event.update({
|
|
name: eventName,
|
|
venue: eventVenue,
|
|
start_date: eventStartDate,
|
|
end_date: eventEndDate,
|
|
timezone: eventTimezone,
|
|
})
|
|
await db.event.put({ ...updated, id: 1 })
|
|
success = 'Event saved.'
|
|
} catch (err) {
|
|
error = err.message
|
|
} finally {
|
|
savingEvent = false
|
|
}
|
|
}
|
|
|
|
async function save(e) {
|
|
e.preventDefault()
|
|
saving = true
|
|
error = ''
|
|
success = ''
|
|
try {
|
|
await api.settings.update({
|
|
smtp_host: smtpHost,
|
|
smtp_port: smtpPort,
|
|
smtp_user: smtpUser,
|
|
smtp_password: smtpPassword, // empty = keep existing
|
|
smtp_from: smtpFrom,
|
|
smtp_from_name: smtpFromName,
|
|
base_url: baseURL,
|
|
volunteer_note_label: noteLabel,
|
|
volunteer_note_required: noteRequired,
|
|
})
|
|
smtpPassword = ''
|
|
success = 'Settings saved.'
|
|
} catch (err) {
|
|
error = err.message
|
|
} finally {
|
|
saving = false
|
|
}
|
|
}
|
|
|
|
async function toggleSignups() {
|
|
const opening = !shiftSignupsOpen
|
|
if (opening && !confirm('This will email all confirmed volunteers their shift signup links. Continue?')) return
|
|
togglingSignups = true
|
|
error = ''
|
|
success = ''
|
|
try {
|
|
const result = await api.settings.toggleShiftSignups(opening)
|
|
shiftSignupsOpen = result.shift_signups_open
|
|
success = opening ? 'Shift signups opened. Emails are being sent.' : 'Shift signups closed.'
|
|
} catch (err) {
|
|
error = err.message
|
|
} finally {
|
|
togglingSignups = false
|
|
}
|
|
}
|
|
|
|
async function resetModel(label, fn) {
|
|
if (!confirm(`PERMANENTLY DELETE all ${label}? This cannot be undone.`)) return
|
|
error = ''
|
|
success = ''
|
|
try {
|
|
const result = await fn()
|
|
success = `Deleted ${result.deleted} ${label}.`
|
|
} catch (err) {
|
|
error = err.message
|
|
}
|
|
}
|
|
|
|
async function sendTest() {
|
|
if (!testEmail) return
|
|
testing = true
|
|
error = ''
|
|
success = ''
|
|
try {
|
|
await api.settings.testEmail(testEmail)
|
|
success = `Test email sent to ${testEmail}.`
|
|
} catch (err) {
|
|
error = err.message
|
|
} finally {
|
|
testing = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="page">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Settings</h1>
|
|
</div>
|
|
|
|
{#if error}
|
|
<div class="alert alert-error">{error}</div>
|
|
{/if}
|
|
{#if success}
|
|
<div class="alert alert-success">{success}</div>
|
|
{/if}
|
|
|
|
{#if loading}
|
|
<div class="text-muted">Loading…</div>
|
|
{:else}
|
|
<form onsubmit={saveEvent}>
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:1rem">Event</h2>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
|
<div class="form-group" style="grid-column:1/-1">
|
|
<label for="e-name">Event Name *</label>
|
|
<input id="e-name" bind:value={eventName} required placeholder="My Event 2026" />
|
|
</div>
|
|
<div class="form-group" style="grid-column:1/-1">
|
|
<label for="e-venue">Venue</label>
|
|
<input id="e-venue" bind:value={eventVenue} placeholder="Location name" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="e-start">Start Date *</label>
|
|
<input id="e-start" type="date" bind:value={eventStartDate} required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="e-end">End Date *</label>
|
|
<input id="e-end" type="date" bind:value={eventEndDate} required />
|
|
</div>
|
|
<div class="form-group" style="grid-column:1/-1">
|
|
<label for="e-tz">Timezone</label>
|
|
<input id="e-tz" bind:value={eventTimezone} placeholder="America/Chicago" list="tz-list" />
|
|
<datalist id="tz-list">
|
|
{#each timezones as tz}
|
|
<option value={tz} />
|
|
{/each}
|
|
</datalist>
|
|
</div>
|
|
</div>
|
|
<div class="actions">
|
|
<button type="submit" class="btn btn-primary" disabled={savingEvent}>
|
|
{savingEvent ? 'Saving…' : 'Save Event'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<form onsubmit={save}>
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:1rem">SMTP Email</h2>
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
|
<div class="form-group" style="grid-column:1">
|
|
<label for="s-host">SMTP Host</label>
|
|
<input id="s-host" bind:value={smtpHost} placeholder="smtp.fastmail.com" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="s-port">Port</label>
|
|
<input id="s-port" type="number" bind:value={smtpPort} placeholder="587" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="s-user">Username</label>
|
|
<input id="s-user" bind:value={smtpUser} placeholder="user@example.com" autocomplete="off" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="s-pass">Password</label>
|
|
<input id="s-pass" type="password" bind:value={smtpPassword}
|
|
placeholder="Leave blank to keep existing" autocomplete="new-password" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="s-from">From Address</label>
|
|
<input id="s-from" type="email" bind:value={smtpFrom} placeholder="events@example.com" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="s-fname">From Name</label>
|
|
<input id="s-fname" bind:value={smtpFromName} placeholder="Event Team" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="s-url">Base URL <span class="text-muted" style="font-weight:400">(for kiosk links in emails)</span></label>
|
|
<input id="s-url" bind:value={baseURL} placeholder="https://events.example.com" />
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<button type="submit" class="btn btn-primary" disabled={saving}>
|
|
{saving ? 'Saving…' : 'Save Settings'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Test email -->
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:1rem">Test Email</h2>
|
|
<div style="display:flex;gap:0.5rem;align-items:flex-end">
|
|
<div class="form-group" style="flex:1;margin-bottom:0">
|
|
<label for="s-test">Send to</label>
|
|
<input id="s-test" type="email" bind:value={testEmail} placeholder="your@email.com" />
|
|
</div>
|
|
<button class="btn btn-ghost" onclick={sendTest} disabled={testing || !testEmail}>
|
|
{testing ? 'Sending…' : 'Send Test'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Volunteer Signup -->
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:1rem">Volunteer Signup</h2>
|
|
<div class="form-group">
|
|
<label for="s-note-label">Note Field Label</label>
|
|
<input id="s-note-label" bind:value={noteLabel} placeholder="Additional note" />
|
|
</div>
|
|
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.875rem;cursor:pointer">
|
|
<input type="checkbox" bind:checked={noteRequired} />
|
|
Note field is required
|
|
</label>
|
|
<p class="text-muted" style="font-size:0.78rem;margin-top:0.75rem">
|
|
Signup form: <a href="/volunteer-signup" target="_blank" style="color:var(--c-accent)">/volunteer-signup</a>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Shift Signups -->
|
|
<div class="card" style="margin-bottom:1.5rem">
|
|
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:1rem">Shift Signups</h2>
|
|
<div style="display:flex;align-items:center;gap:1rem">
|
|
<span style="font-size:0.875rem">
|
|
Status: <strong>{shiftSignupsOpen ? 'Open' : 'Closed'}</strong>
|
|
</span>
|
|
<button class="btn {shiftSignupsOpen ? 'btn-ghost' : 'btn-primary'}"
|
|
onclick={toggleSignups} disabled={togglingSignups}>
|
|
{#if togglingSignups}
|
|
Working…
|
|
{:else}
|
|
{shiftSignupsOpen ? 'Close Signups' : 'Open Signups'}
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{#if !shiftSignupsOpen}
|
|
<p class="text-muted" style="font-size:0.78rem;margin-top:0.75rem">
|
|
Opening signups will email all confirmed volunteers their shift signup links.
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Data Management -->
|
|
<div class="card">
|
|
<h2 style="font-size:0.95rem;font-weight:700;margin-bottom:0.5rem">Data Management</h2>
|
|
<p class="text-muted" style="font-size:0.78rem;margin-bottom:1rem">
|
|
Permanently delete all records of a given type. This cannot be undone.
|
|
</p>
|
|
<div style="display:flex;flex-wrap:wrap;gap:0.5rem">
|
|
<button class="btn btn-danger" onclick={() => resetModel('tickets', api.settings.resetTickets)}>
|
|
Delete All Tickets
|
|
</button>
|
|
<button class="btn btn-danger" onclick={() => resetModel('volunteers', api.settings.resetVolunteers)}>
|
|
Delete All Volunteers
|
|
</button>
|
|
<button class="btn btn-danger" onclick={() => resetModel('shifts', api.settings.resetShifts)}>
|
|
Delete All Shifts
|
|
</button>
|
|
<button class="btn btn-danger" onclick={() => resetModel('departments', api.settings.resetDepartments)}>
|
|
Delete All Departments
|
|
</button>
|
|
<button class="btn btn-danger" onclick={() => resetModel('volunteer shift assignments', api.settings.resetVolunteerShifts)}>
|
|
Delete All Shift Assignments
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|