events(settings): add modules config page and settings link

This commit is contained in:
Scott Idem
2026-04-02 20:01:15 -04:00
parent 0e0fc071c7
commit 055bbd9ffd
2 changed files with 281 additions and 0 deletions

View File

@@ -209,6 +209,22 @@ async function handle_save(field_name: string, data: any) {
</div>
</details>
<details class="details">
<summary class="summary">Module Access</summary>
<div class="p-4 space-y-3">
<p class="text-sm text-surface-500">
Control which module buttons appear on the Event Hub landing page.
Each module defaults to <strong>off</strong> — enable only the modules
available for this event.
</p>
<a
href="/events/{event_id}/settings/modules"
class="btn preset-tonal-primary">
Go to Module Config →
</a>
</div>
</details>
<details class="details">
<summary class="summary">General Config (cfg_json)</summary>
<div class="p-4">

View File

@@ -0,0 +1,265 @@
<script lang="ts">
/**
* Event Modules Config Page
* Route: /events/[event_id]/settings/modules
*
* Admin UI for managing event.cfg_json.modules_enabled.
* Controls which module hub cards appear on the Event landing page.
*
* Default for each module is FALSE — admins must explicitly enable the
* modules available for that event. This prevents accidental exposure
* of modules a client hasn't been licensed/configured for.
*
* Save pattern: load → merge draft → PATCH cfg_json via V3 API.
*/
import { untrack } from 'svelte';
import { page } from '$app/state';
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { api } from '$lib/api/api';
import {
AlertTriangle,
ArrowLeft,
Check,
IdCard,
Contact,
Lock,
Plane,
Presentation,
Save,
Settings
} from '@lucide/svelte';
interface Props {
data: any;
}
let { data }: Props = $props();
let event_id = $derived(page.params.event_id ?? '');
let lq__event_obj = $derived(
liveQuery(async () => {
if (!event_id) return null;
return await db_events.event.get(event_id);
})
);
// ---------------------------------------------------------------------------
// Module definitions (matches the hub card list in [event_id]/+page.svelte)
// ---------------------------------------------------------------------------
const module_list = [
{
key: 'pres_mgmt',
label: 'Presentation Management',
description: 'Manage sessions, presentations, and presenters.',
icon: Presentation
},
{
key: 'launcher',
label: 'Launcher',
description: 'Launch presentations and manage live session display.',
icon: Plane
},
{
key: 'badges',
label: 'Badges',
description: 'Manage and print event badges.',
icon: IdCard
},
{
key: 'leads',
label: 'Leads',
description: 'Exhibitor lead retrieval and management.',
icon: Contact
}
];
// ---------------------------------------------------------------------------
// Draft state — initialized from the live event config
// All modules default to false. Admin must explicitly enable each one.
// ---------------------------------------------------------------------------
const cfg_defaults: Record<string, boolean> = {
pres_mgmt: false,
launcher: false,
badges: false,
leads: false
};
let draft: Record<string, boolean> = $state({ ...cfg_defaults });
let draft_initialized = $state(false);
let initial_json = $state('');
$effect(() => {
const ev = $lq__event_obj;
if (ev != null && !draft_initialized) {
untrack(() => {
// If modules_enabled is already configured, merge with defaults.
// Any key not present in the saved config stays false (no implicit enables).
const saved = ev.cfg_json?.modules_enabled ?? {};
draft = { ...cfg_defaults, ...saved };
initial_json = JSON.stringify(draft);
draft_initialized = true;
});
}
});
let is_dirty = $derived(draft_initialized && JSON.stringify(draft) !== initial_json);
// ---------------------------------------------------------------------------
// Save — patches only cfg_json.modules_enabled into the event
// ---------------------------------------------------------------------------
let save_status: 'idle' | 'saving' | 'success' | 'error' = $state('idle');
async function save() {
if (!event_id) return;
save_status = 'saving';
try {
// Preserve existing cfg_json fields; only overwrite modules_enabled.
const current_cfg = $lq__event_obj?.cfg_json ?? {};
const new_cfg = { ...current_cfg, modules_enabled: { ...draft } };
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event',
obj_id: event_id,
fields: { cfg_json: new_cfg },
log_lvl: 1
});
// Reload event so the landing page picks up new config immediately.
await events_func.load_ae_obj_id__event({
api_cfg: $ae_api,
event_id: event_id,
log_lvl: 1
});
initial_json = JSON.stringify(draft);
save_status = 'success';
setTimeout(() => (save_status = 'idle'), 3000);
} catch (e) {
console.error('Failed to save modules config', e);
save_status = 'error';
setTimeout(() => (save_status = 'idle'), 5000);
}
}
</script>
<svelte:head>
<title>Event Modules Config</title>
</svelte:head>
{#if !$ae_loc.administrator_access}
<div class="p-8 text-center opacity-50">
<Lock size="3em" class="mx-auto mb-2" />
<p>Administrator access required.</p>
</div>
{:else}
<div class="mx-auto w-full max-w-2xl space-y-4 px-2 py-4">
<!-- Header -->
<header class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2">
<a
href="/events/{event_id}/settings"
class="btn btn-sm preset-tonal-surface"
title="Back to Event Settings">
<ArrowLeft size="1em" />
</a>
<Settings size="1.2em" class="text-primary-500" />
<h1 class="text-xl font-bold">Event Modules</h1>
</div>
<div class="flex items-center gap-2">
{#if save_status === 'success'}
<span class="badge preset-tonal-success flex items-center gap-1">
<Check size="1em" /> Saved
</span>
{:else if save_status === 'error'}
<span class="badge preset-tonal-error flex items-center gap-1">
<AlertTriangle size="1em" /> Error saving
</span>
{/if}
<button
type="button"
class="btn preset-filled-primary-500"
onclick={save}
disabled={!is_dirty || save_status === 'saving'}>
<Save size="1em" class="mr-1" />
{save_status === 'saving' ? 'Saving...' : 'Save'}
</button>
</div>
</header>
<p class="text-surface-500 text-sm">
Enable the modules available for this event. Only enabled modules will
appear as buttons on the Event Hub landing page. Each module defaults to
<strong>off</strong> — you must explicitly enable them.
</p>
{#if !draft_initialized}
<p class="text-surface-400 italic">Loading event config...</p>
{:else}
<!-- Module toggles -->
<section class="border-surface-200-800 rounded-xl border">
<div class="border-surface-200-800 flex items-center justify-between border-b px-4 py-3">
<span class="font-semibold">Available Modules</span>
<div class="flex gap-2">
<button
type="button"
class="btn btn-sm preset-tonal-surface"
onclick={() => {
for (const m of module_list) draft[m.key] = true;
}}>
Enable All
</button>
<button
type="button"
class="btn btn-sm preset-tonal-surface"
onclick={() => {
for (const m of module_list) draft[m.key] = false;
}}>
Disable All
</button>
</div>
</div>
<div class="divide-surface-200-800 divide-y">
{#each module_list as mod (mod.key)}
<label class="flex cursor-pointer items-start gap-4 px-4 py-3 hover:bg-surface-50 dark:hover:bg-surface-800/50">
<input
type="checkbox"
class="checkbox mt-0.5"
bind:checked={draft[mod.key]} />
<div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2">
<mod.icon size="1em" class="text-primary-500" />
<span class="font-semibold">{mod.label}</span>
{#if draft[mod.key]}
<span class="badge preset-tonal-success text-xs">Enabled</span>
{:else}
<span class="badge preset-tonal-surface text-xs">Disabled</span>
{/if}
</div>
<span class="text-surface-500 text-sm">{mod.description}</span>
</div>
</label>
{/each}
</div>
</section>
<!-- Bottom save -->
<div class="flex justify-end">
<button
type="button"
class="btn preset-filled-primary-500"
onclick={save}
disabled={!is_dirty || save_status === 'saving'}>
<Save size="1em" class="mr-1" />
{save_status === 'saving' ? 'Saving...' : 'Save Config'}
</button>
</div>
{/if}
</div>
{/if}