events(settings): add modules config page and settings link
This commit is contained in:
@@ -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">
|
||||
|
||||
265
src/routes/events/[event_id]/settings/modules/+page.svelte
Normal file
265
src/routes/events/[event_id]/settings/modules/+page.svelte
Normal 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}
|
||||
Reference in New Issue
Block a user