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>
|
</div>
|
||||||
</details>
|
</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">
|
<details class="details">
|
||||||
<summary class="summary">General Config (cfg_json)</summary>
|
<summary class="summary">General Config (cfg_json)</summary>
|
||||||
<div class="p-4">
|
<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