badges(config): fix duplicate keys and initialize draft when mod_badges_json missing; update settings button style
This commit is contained in:
472
src/routes/events/[event_id]/(badges)/badges/config/+page.svelte
Normal file
472
src/routes/events/[event_id]/(badges)/badges/config/+page.svelte
Normal file
@@ -0,0 +1,472 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Badges Config Page
|
||||
* Route: /events/[event_id]/(badges)/badges/config
|
||||
*
|
||||
* Admin UI for managing event.mod_badges_json (BadgesRemoteCfg).
|
||||
* Access: administrator_access only (passcode fields present — stricter than pres_mgmt).
|
||||
*
|
||||
* Save pattern: load → merge draft → PATCH event via V3 API.
|
||||
*/
|
||||
import { untrack } from 'svelte';
|
||||
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_slct } from '$lib/stores/ae_events_stores';
|
||||
import { events_func } from '$lib/ae_events/ae_events_functions';
|
||||
import { api } from '$lib/api/api';
|
||||
import type { BadgesRemoteCfg } from '$lib/stores/ae_events_stores__badges_defaults';
|
||||
import {
|
||||
default_authenticated_can_edit,
|
||||
default_trusted_can_edit
|
||||
} from '$lib/stores/ae_events_stores__badges_defaults';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Lock,
|
||||
Save,
|
||||
Settings
|
||||
} from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
}
|
||||
let { data }: Props = $props();
|
||||
|
||||
let event_id = $derived($events_slct.event_id ?? '');
|
||||
|
||||
let lq__event_obj = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!event_id) return null;
|
||||
return await db_events.event.get(event_id);
|
||||
})
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Known editable field keys (used to render checkboxes in permission sections)
|
||||
// ---------------------------------------------------------------------------
|
||||
const attendee_field_options: { key: string; label: string }[] = [
|
||||
{ key: 'pronouns_override', label: 'Pronouns' },
|
||||
{ key: 'full_name_override', label: 'Full Name' },
|
||||
{ key: 'professional_title_override', label: 'Professional Title' },
|
||||
{ key: 'affiliations_override', label: 'Affiliations' },
|
||||
{ key: 'phone_override', label: 'Phone' },
|
||||
{ key: 'location_override', label: 'Location' },
|
||||
{ key: 'allow_tracking', label: 'Allow Tracking (Exhibitor Leads)' },
|
||||
{ key: 'agree_to_tc', label: 'Agree to Terms & Conditions (placeholder)' }
|
||||
];
|
||||
|
||||
const staff_extra_field_options: { key: string; label: string }[] = [
|
||||
{ key: 'email_override', label: 'Email' },
|
||||
{ key: 'badge_type_code_override', label: 'Badge Type Code' },
|
||||
{ key: 'registration_type_code_override', label: 'Registration Type Code' },
|
||||
{ key: 'other_1_code', label: 'Option 1' },
|
||||
{ key: 'other_2_code', label: 'Option 2' },
|
||||
{ key: 'other_3_code', label: 'Option 3' },
|
||||
{ key: 'other_4_code', label: 'Option 4' },
|
||||
{ key: 'other_5_code', label: 'Option 5' },
|
||||
{ key: 'other_6_code', label: 'Option 6' },
|
||||
{ key: 'other_7_code', label: 'Option 7' },
|
||||
{ key: 'other_8_code', label: 'Option 8' },
|
||||
{ key: 'ticket_1_code', label: 'Ticket 1' },
|
||||
{ key: 'ticket_2_code', label: 'Ticket 2' },
|
||||
{ key: 'ticket_3_code', label: 'Ticket 3' },
|
||||
{ key: 'ticket_4_code', label: 'Ticket 4' },
|
||||
{ key: 'ticket_5_code', label: 'Ticket 5' },
|
||||
{ key: 'ticket_6_code', label: 'Ticket 6' },
|
||||
{ key: 'ticket_7_code', label: 'Ticket 7' },
|
||||
{ key: 'ticket_8_code', label: 'Ticket 8' },
|
||||
{ key: 'allow_tracking', label: 'Allow Tracking' },
|
||||
{ key: 'agree_to_tc', label: 'Agree to Terms (placeholder)' },
|
||||
{ key: 'hide', label: 'Hide (archive)' },
|
||||
{ key: 'priority', label: 'Priority' },
|
||||
{ key: 'notes', label: 'Notes' }
|
||||
];
|
||||
|
||||
// All field options shown in the staff section (attendee fields first, then extras).
|
||||
// Exclude agree_to_tc and allow_tracking from the attendee slice — both appear in
|
||||
// staff_extra_field_options, so keeping them here would create duplicate keys.
|
||||
const all_staff_field_options = [
|
||||
...attendee_field_options.filter((f) => f.key !== 'agree_to_tc' && f.key !== 'allow_tracking'),
|
||||
...staff_extra_field_options
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Draft state — initialized from the live event config
|
||||
// ---------------------------------------------------------------------------
|
||||
const cfg_defaults: BadgesRemoteCfg = {
|
||||
badge_id_only_search: false,
|
||||
enable_mass_print: false,
|
||||
enable_add_badge_btn: false,
|
||||
enable_upload_badge_li_btn: false,
|
||||
enable_search_qr: false,
|
||||
qr_type: null,
|
||||
trusted_passcode: null,
|
||||
administrator_passcode: null,
|
||||
edit_permissions: {
|
||||
authenticated: { can_edit: [...default_authenticated_can_edit] },
|
||||
trusted: { can_edit: [...default_trusted_can_edit] },
|
||||
administrator: { can_edit: '*' }
|
||||
}
|
||||
};
|
||||
|
||||
let draft: BadgesRemoteCfg = $state({ ...cfg_defaults });
|
||||
// edit_permissions is a nested object — deep-clone it separately
|
||||
let draft_auth_fields: string[] = $state([...default_authenticated_can_edit]);
|
||||
let draft_trusted_fields: string[] = $state([...default_trusted_can_edit]);
|
||||
|
||||
let draft_initialized = $state(false);
|
||||
let initial_json = $state('');
|
||||
|
||||
$effect(() => {
|
||||
// Initialize once the event object is available — even if mod_badges_json is null/unset,
|
||||
// so admins can configure a fresh event rather than getting stuck on "Loading...".
|
||||
const event_obj = $lq__event_obj;
|
||||
if (event_obj && !draft_initialized) {
|
||||
const event_cfg = event_obj.mod_badges_json;
|
||||
untrack(() => {
|
||||
draft = { ...cfg_defaults, ...(event_cfg ?? {}) };
|
||||
// Normalize edit_permissions to ensure sub-objects exist
|
||||
draft.edit_permissions = {
|
||||
authenticated: event_cfg?.edit_permissions?.authenticated ?? {
|
||||
can_edit: [...default_authenticated_can_edit]
|
||||
},
|
||||
trusted: event_cfg?.edit_permissions?.trusted ?? {
|
||||
can_edit: [...default_trusted_can_edit]
|
||||
},
|
||||
administrator: { can_edit: '*' }
|
||||
};
|
||||
const auth_cfg = draft.edit_permissions.authenticated?.can_edit;
|
||||
draft_auth_fields = Array.isArray(auth_cfg)
|
||||
? [...auth_cfg]
|
||||
: [...default_authenticated_can_edit];
|
||||
const trusted_cfg = draft.edit_permissions.trusted?.can_edit;
|
||||
draft_trusted_fields = Array.isArray(trusted_cfg)
|
||||
? [...trusted_cfg]
|
||||
: [...default_trusted_can_edit];
|
||||
|
||||
initial_json = JSON.stringify({
|
||||
...draft,
|
||||
_auth: draft_auth_fields,
|
||||
_trusted: draft_trusted_fields
|
||||
});
|
||||
draft_initialized = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let is_dirty = $derived(
|
||||
draft_initialized &&
|
||||
JSON.stringify({
|
||||
...draft,
|
||||
_auth: draft_auth_fields,
|
||||
_trusted: draft_trusted_fields
|
||||
}) !== initial_json
|
||||
);
|
||||
|
||||
// Toggle a field in the auth checkbox list
|
||||
function toggle_auth_field(key: string) {
|
||||
if (draft_auth_fields.includes(key)) {
|
||||
draft_auth_fields = draft_auth_fields.filter((f) => f !== key);
|
||||
} else {
|
||||
draft_auth_fields = [...draft_auth_fields, key];
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle a field in the trusted checkbox list
|
||||
function toggle_trusted_field(key: string) {
|
||||
if (draft_trusted_fields.includes(key)) {
|
||||
draft_trusted_fields = draft_trusted_fields.filter((f) => f !== key);
|
||||
} else {
|
||||
draft_trusted_fields = [...draft_trusted_fields, key];
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Save
|
||||
// ---------------------------------------------------------------------------
|
||||
let save_status: 'idle' | 'saving' | 'success' | 'error' = $state('idle');
|
||||
|
||||
async function save() {
|
||||
if (!event_id) return;
|
||||
save_status = 'saving';
|
||||
try {
|
||||
const payload: BadgesRemoteCfg = {
|
||||
...draft,
|
||||
edit_permissions: {
|
||||
authenticated: { can_edit: draft_auth_fields },
|
||||
trusted: { can_edit: draft_trusted_fields },
|
||||
administrator: { can_edit: '*' }
|
||||
}
|
||||
};
|
||||
await api.update_ae_obj({
|
||||
api_cfg: $ae_api,
|
||||
obj_type: 'event',
|
||||
obj_id: event_id,
|
||||
fields: { mod_badges_json: payload },
|
||||
log_lvl: 1
|
||||
});
|
||||
await events_func.load_ae_obj_id__event({
|
||||
api_cfg: $ae_api,
|
||||
event_id: event_id,
|
||||
log_lvl: 1
|
||||
});
|
||||
initial_json = JSON.stringify({
|
||||
...draft,
|
||||
_auth: draft_auth_fields,
|
||||
_trusted: draft_trusted_fields
|
||||
});
|
||||
save_status = 'success';
|
||||
setTimeout(() => (save_status = 'idle'), 3000);
|
||||
} catch (e) {
|
||||
console.error('Failed to save badges config', e);
|
||||
save_status = 'error';
|
||||
setTimeout(() => (save_status = 'idle'), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Section collapse state
|
||||
let sections: Record<string, boolean> = $state({
|
||||
ui: true,
|
||||
qr: true,
|
||||
passcodes: true,
|
||||
auth_fields: true,
|
||||
trusted_fields: true
|
||||
});
|
||||
function toggle(key: string) {
|
||||
sections[key] = !sections[key];
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Badges 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-3xl 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}/badges"
|
||||
class="btn btn-sm preset-tonal-surface"
|
||||
title="Back to Badges">
|
||||
<ArrowLeft size="1em" />
|
||||
</a>
|
||||
<Settings size="1.2em" class="text-primary-500" />
|
||||
<h1 class="text-xl font-bold">Badges Config</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
|
||||
</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">
|
||||
Changes here update <code>event.mod_badges_json</code> and take effect on the
|
||||
next page load. Field permission changes affect which badge fields each access
|
||||
level may edit on the Badge Review page.
|
||||
</p>
|
||||
|
||||
{#if !draft_initialized}
|
||||
<p class="text-surface-400 italic">Loading event config...</p>
|
||||
{:else}
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- SEARCH & UI -->
|
||||
<!-- ================================================================ -->
|
||||
<section class="border-surface-200-800 rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
|
||||
onclick={() => toggle('ui')}>
|
||||
<span>Search & UI Controls</span>
|
||||
{#if sections.ui}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
|
||||
</button>
|
||||
{#if sections.ui}
|
||||
<div class="border-surface-200-800 grid grid-cols-2 gap-3 border-t px-4 py-3">
|
||||
{#each [
|
||||
{ field: 'badge_id_only_search' as const, label: 'Badge ID Only Search' },
|
||||
{ field: 'enable_mass_print' as const, label: 'Enable Mass Print' },
|
||||
{ field: 'enable_add_badge_btn' as const, label: 'Enable Add Badge Button' },
|
||||
{ field: 'enable_upload_badge_li_btn' as const, label: 'Enable Upload Badge List Button' },
|
||||
{ field: 'enable_search_qr' as const, label: 'Enable QR Scan Search' }
|
||||
] as item (item.field)}
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
bind:checked={draft[item.field]} />
|
||||
<span class="text-sm">{item.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- QR CONFIG -->
|
||||
<!-- ================================================================ -->
|
||||
<section class="border-surface-200-800 rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
|
||||
onclick={() => toggle('qr')}>
|
||||
<span>QR Code Config</span>
|
||||
{#if sections.qr}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
|
||||
</button>
|
||||
{#if sections.qr}
|
||||
<div class="border-surface-200-800 space-y-3 border-t px-4 py-3">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">QR Type</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm"
|
||||
placeholder="e.g. badge_id, url (leave blank for default)"
|
||||
bind:value={draft.qr_type} />
|
||||
<span class="text-surface-400 text-xs">
|
||||
Controls the QR payload format. Verify against the scan component before changing.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- PASSCODES -->
|
||||
<!-- ================================================================ -->
|
||||
<section class="border-surface-200-800 rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
|
||||
onclick={() => toggle('passcodes')}>
|
||||
<span class="flex items-center gap-2">
|
||||
<Lock size="1em" class="text-warning-500" />
|
||||
Access Passcodes
|
||||
<span class="text-surface-400 text-xs font-normal">(administrator only)</span>
|
||||
</span>
|
||||
{#if sections.passcodes}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
|
||||
</button>
|
||||
{#if sections.passcodes}
|
||||
<div class="border-surface-200-800 grid grid-cols-1 gap-3 border-t px-4 py-3 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Trusted Staff Passcode</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm font-mono"
|
||||
placeholder="leave blank to disable"
|
||||
bind:value={draft.trusted_passcode} />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Administrator Passcode</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm font-mono"
|
||||
placeholder="leave blank to disable"
|
||||
bind:value={draft.administrator_passcode} />
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- ATTENDEE EDITABLE FIELDS -->
|
||||
<!-- ================================================================ -->
|
||||
<section class="border-surface-200-800 rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
|
||||
onclick={() => toggle('auth_fields')}>
|
||||
<span>
|
||||
Attendee Editable Fields
|
||||
<span class="text-surface-400 text-xs font-normal">(passcode-authenticated)</span>
|
||||
</span>
|
||||
{#if sections.auth_fields}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
|
||||
</button>
|
||||
{#if sections.auth_fields}
|
||||
<div class="border-surface-200-800 grid grid-cols-2 gap-2 border-t px-4 py-3">
|
||||
{#each attendee_field_options as field (field.key)}
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
checked={draft_auth_fields.includes(field.key)}
|
||||
onchange={() => toggle_auth_field(field.key)} />
|
||||
<span>{field.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- STAFF EDITABLE FIELDS -->
|
||||
<!-- ================================================================ -->
|
||||
<section class="border-surface-200-800 rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
|
||||
onclick={() => toggle('trusted_fields')}>
|
||||
<span>
|
||||
Staff Editable Fields
|
||||
<span class="text-surface-400 text-xs font-normal">(trusted access)</span>
|
||||
</span>
|
||||
{#if sections.trusted_fields}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
|
||||
</button>
|
||||
{#if sections.trusted_fields}
|
||||
<div class="border-surface-200-800 grid grid-cols-2 gap-2 border-t px-4 py-3">
|
||||
{#each all_staff_field_options as field (field.key)}
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
checked={draft_trusted_fields.includes(field.key)}
|
||||
onchange={() => toggle_trusted_field(field.key)} />
|
||||
<span>{field.label}</span>
|
||||
</label>
|
||||
{/each}
|
||||
<p class="col-span-2 mt-1 text-xs text-surface-400 italic">
|
||||
Administrators always have access to all fields — not configurable here.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Bottom save button -->
|
||||
<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}
|
||||
@@ -17,7 +17,6 @@ import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/element_editor_codemirror.svelte';
|
||||
import Ae_comp_event_settings_form from './ae_comp__event_settings_form.svelte';
|
||||
import Ae_comp_event_settings_basic_form from './ae_comp__event_settings_basic_form.svelte';
|
||||
import Ae_comp_event_settings_badges_form from './ae_comp__event_settings_badges_form.svelte';
|
||||
import Ae_comp_event_settings_abstracts_form from './ae_comp__event_settings_abstracts_form.svelte';
|
||||
import { Modal } from 'flowbite-svelte';
|
||||
import Comp_badge_create_form from '../(badges)/badges/ae_comp__badge_create_form.svelte';
|
||||
@@ -26,7 +25,6 @@ import Comp_badge_upload_form from '../(badges)/badges/ae_comp__badge_upload_for
|
||||
let event_id = page.params.event_id as string;
|
||||
let event_obj: Event | undefined | null = $state(null);
|
||||
let cfg_json_view = $state('form');
|
||||
let badges_json_view = $state('form');
|
||||
let abstracts_json_view = $state('form');
|
||||
|
||||
// Temp string values for CodeMirror binding
|
||||
@@ -273,42 +271,38 @@ async function handle_save(field_name: string, data: any) {
|
||||
|
||||
<details class="details">
|
||||
<summary class="summary">Badges (mod_badges_json)</summary>
|
||||
<div class="p-4">
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
onclick={() => (badges_json_view = 'form')}
|
||||
>Form</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm"
|
||||
onclick={() => (badges_json_view = 'json')}
|
||||
>JSON</button>
|
||||
</div>
|
||||
{#if badges_json_view === 'form'}
|
||||
<Ae_comp_event_settings_badges_form
|
||||
bind:mod_badges_json={event_obj.mod_badges_json}
|
||||
onsave={(data: any) =>
|
||||
handle_save('mod_badges_json', data)} />
|
||||
{:else}
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
readonly={false}
|
||||
content={tmp_badges_json_str}
|
||||
bind:new_content={tmp_badges_json_str}
|
||||
show_line_numbers={true}
|
||||
placeholder="JSON config"
|
||||
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-primary"
|
||||
onclick={() => {
|
||||
handle_save(
|
||||
'mod_badges_json',
|
||||
tmp_badges_json_str
|
||||
);
|
||||
}}>Save</button>
|
||||
{/if}
|
||||
<div class="p-4 space-y-3">
|
||||
<p class="text-sm text-surface-500">
|
||||
Manage badge search controls, QR config, access passcodes, and
|
||||
per-event field edit permissions.
|
||||
</p>
|
||||
<a
|
||||
href="/events/{event_id}/badges/config"
|
||||
class="btn preset-tonal-primary">
|
||||
Go to Badges Config →
|
||||
</a>
|
||||
<!-- Raw JSON fallback for debugging / emergency edits -->
|
||||
<details class="mt-2">
|
||||
<summary class="cursor-pointer text-xs text-surface-400">Raw JSON (advanced)</summary>
|
||||
<div class="mt-2 space-y-2">
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
readonly={false}
|
||||
content={tmp_badges_json_str}
|
||||
bind:new_content={tmp_badges_json_str}
|
||||
show_line_numbers={true}
|
||||
placeholder="JSON config"
|
||||
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-primary"
|
||||
onclick={() => {
|
||||
handle_save(
|
||||
'mod_badges_json',
|
||||
tmp_badges_json_str
|
||||
);
|
||||
}}>Save Raw JSON</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user