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_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_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_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 Ae_comp_event_settings_abstracts_form from './ae_comp__event_settings_abstracts_form.svelte';
|
||||||
import { Modal } from 'flowbite-svelte';
|
import { Modal } from 'flowbite-svelte';
|
||||||
import Comp_badge_create_form from '../(badges)/badges/ae_comp__badge_create_form.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_id = page.params.event_id as string;
|
||||||
let event_obj: Event | undefined | null = $state(null);
|
let event_obj: Event | undefined | null = $state(null);
|
||||||
let cfg_json_view = $state('form');
|
let cfg_json_view = $state('form');
|
||||||
let badges_json_view = $state('form');
|
|
||||||
let abstracts_json_view = $state('form');
|
let abstracts_json_view = $state('form');
|
||||||
|
|
||||||
// Temp string values for CodeMirror binding
|
// Temp string values for CodeMirror binding
|
||||||
@@ -273,42 +271,38 @@ async function handle_save(field_name: string, data: any) {
|
|||||||
|
|
||||||
<details class="details">
|
<details class="details">
|
||||||
<summary class="summary">Badges (mod_badges_json)</summary>
|
<summary class="summary">Badges (mod_badges_json)</summary>
|
||||||
<div class="p-4">
|
<div class="p-4 space-y-3">
|
||||||
<div class="flex justify-end">
|
<p class="text-sm text-surface-500">
|
||||||
<button
|
Manage badge search controls, QR config, access passcodes, and
|
||||||
type="button"
|
per-event field edit permissions.
|
||||||
class="btn btn-sm"
|
</p>
|
||||||
onclick={() => (badges_json_view = 'form')}
|
<a
|
||||||
>Form</button>
|
href="/events/{event_id}/badges/config"
|
||||||
<button
|
class="btn preset-tonal-primary">
|
||||||
type="button"
|
Go to Badges Config →
|
||||||
class="btn btn-sm"
|
</a>
|
||||||
onclick={() => (badges_json_view = 'json')}
|
<!-- Raw JSON fallback for debugging / emergency edits -->
|
||||||
>JSON</button>
|
<details class="mt-2">
|
||||||
</div>
|
<summary class="cursor-pointer text-xs text-surface-400">Raw JSON (advanced)</summary>
|
||||||
{#if badges_json_view === 'form'}
|
<div class="mt-2 space-y-2">
|
||||||
<Ae_comp_event_settings_badges_form
|
<AE_Comp_Editor_CodeMirror
|
||||||
bind:mod_badges_json={event_obj.mod_badges_json}
|
readonly={false}
|
||||||
onsave={(data: any) =>
|
content={tmp_badges_json_str}
|
||||||
handle_save('mod_badges_json', data)} />
|
bind:new_content={tmp_badges_json_str}
|
||||||
{:else}
|
show_line_numbers={true}
|
||||||
<AE_Comp_Editor_CodeMirror
|
placeholder="JSON config"
|
||||||
readonly={false}
|
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
|
||||||
content={tmp_badges_json_str}
|
<button
|
||||||
bind:new_content={tmp_badges_json_str}
|
type="button"
|
||||||
show_line_numbers={true}
|
class="btn preset-tonal-primary"
|
||||||
placeholder="JSON config"
|
onclick={() => {
|
||||||
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
|
handle_save(
|
||||||
<button
|
'mod_badges_json',
|
||||||
type="button"
|
tmp_badges_json_str
|
||||||
class="btn preset-tonal-primary"
|
);
|
||||||
onclick={() => {
|
}}>Save Raw JSON</button>
|
||||||
handle_save(
|
</div>
|
||||||
'mod_badges_json',
|
</details>
|
||||||
tmp_badges_json_str
|
|
||||||
);
|
|
||||||
}}>Save</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user