2026-03-16 23:20:54 +01:00
|
|
|
import { createSignal } from 'solid-js';
|
|
|
|
|
import AdminShell from '~/components/AdminShell';
|
|
|
|
|
import { saveRuntimeConfig } from '~/lib/runtime/storage';
|
2026-03-16 23:37:26 +01:00
|
|
|
import type { RuntimeOnboardingConfig, RuntimeOnboardingStep } from '~/lib/runtime/types';
|
2026-03-16 23:20:54 +01:00
|
|
|
|
2026-03-16 23:37:26 +01:00
|
|
|
function buildDefaultConfig(): RuntimeOnboardingConfig {
|
|
|
|
|
return {
|
2026-03-16 23:20:54 +01:00
|
|
|
schemaId: 'photographer_onboarding_v1',
|
|
|
|
|
roleKey: 'PHOTOGRAPHER',
|
|
|
|
|
version: 1,
|
|
|
|
|
steps: [
|
|
|
|
|
{
|
2026-03-16 23:37:26 +01:00
|
|
|
id: 'step_1_service',
|
|
|
|
|
title: 'Select Service Category',
|
2026-03-16 23:20:54 +01:00
|
|
|
fields: [
|
2026-03-16 23:37:26 +01:00
|
|
|
{
|
|
|
|
|
id: 'profession',
|
|
|
|
|
label: 'Service Category',
|
|
|
|
|
type: 'select',
|
|
|
|
|
required: true,
|
|
|
|
|
options: [
|
|
|
|
|
{ label: 'Photographer', value: 'Photographer' },
|
|
|
|
|
{ label: 'Makeup Artist', value: 'Makeup Artist' },
|
|
|
|
|
],
|
|
|
|
|
},
|
2026-03-16 23:20:54 +01:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-03-16 23:37:26 +01:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stringifySteps(steps: RuntimeOnboardingStep[]): string {
|
|
|
|
|
return JSON.stringify(steps, null, 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function CreateOnboardingSchemaPage() {
|
|
|
|
|
const [config, setConfig] = createSignal<RuntimeOnboardingConfig>(buildDefaultConfig());
|
|
|
|
|
const [stepsJson, setStepsJson] = createSignal(stringifySteps(buildDefaultConfig().steps));
|
2026-03-16 23:20:54 +01:00
|
|
|
const [statusMessage, setStatusMessage] = createSignal('');
|
2026-03-16 23:37:26 +01:00
|
|
|
const [stepsError, setStepsError] = createSignal('');
|
|
|
|
|
|
|
|
|
|
const syncSteps = (raw: string) => {
|
|
|
|
|
setStepsJson(raw);
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(raw) as RuntimeOnboardingStep[];
|
|
|
|
|
if (!Array.isArray(parsed)) {
|
|
|
|
|
setStepsError('Steps JSON must be an array.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setConfig({ ...config(), steps: parsed });
|
|
|
|
|
setStepsError('');
|
|
|
|
|
} catch {
|
|
|
|
|
setStepsError('Invalid JSON. Fix syntax before saving.');
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-16 23:20:54 +01:00
|
|
|
|
2026-03-17 20:48:27 +01:00
|
|
|
const [isSaving, setIsSaving] = createSignal(false);
|
|
|
|
|
|
|
|
|
|
const persist = async (status: 'draft' | 'published') => {
|
2026-03-16 23:20:54 +01:00
|
|
|
const payload = config();
|
|
|
|
|
if (!payload.schemaId.trim()) {
|
|
|
|
|
setStatusMessage('Schema ID is required before saving.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-16 23:37:26 +01:00
|
|
|
if (stepsError()) {
|
|
|
|
|
setStatusMessage('Fix steps JSON errors before saving.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (payload.steps.length === 0) {
|
|
|
|
|
setStatusMessage('Add at least one onboarding step before saving.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 20:48:27 +01:00
|
|
|
setIsSaving(true);
|
|
|
|
|
setStatusMessage('Saving to backend...');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await saveRuntimeConfig('onboarding', payload.schemaId, payload, status);
|
|
|
|
|
setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Onboarding schema published successfully.');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
}
|
2026-03-16 23:20:54 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<AdminShell>
|
|
|
|
|
<h1 class="page-title">Create Onboarding Flow</h1>
|
2026-03-16 23:37:26 +01:00
|
|
|
<p class="page-subtitle">All onboarding fields, validations, upload rules, and select behaviors are runtime schema-driven.</p>
|
2026-03-16 23:20:54 +01:00
|
|
|
<div class="grid">
|
|
|
|
|
<section class="card">
|
|
|
|
|
<h2>Onboarding Builder</h2>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Schema ID</label>
|
|
|
|
|
<input value={config().schemaId} onInput={(e) => setConfig({ ...config(), schemaId: e.currentTarget.value })} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Role Key</label>
|
|
|
|
|
<input value={config().roleKey} onInput={(e) => setConfig({ ...config(), roleKey: e.currentTarget.value.toUpperCase() })} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
|
|
|
|
<label>Version</label>
|
|
|
|
|
<input type="number" value={config().version} onInput={(e) => setConfig({ ...config(), version: Number(e.currentTarget.value || 1) })} />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field">
|
2026-03-16 23:37:26 +01:00
|
|
|
<label>Steps JSON (full runtime schema)</label>
|
|
|
|
|
<textarea class="json-input" rows={18} value={stepsJson()} onInput={(e) => syncSteps(e.currentTarget.value)} />
|
|
|
|
|
{stepsError() && <p class="error-note">{stepsError()}</p>}
|
2026-03-16 23:20:54 +01:00
|
|
|
</div>
|
|
|
|
|
<div class="actions">
|
2026-03-17 20:48:27 +01:00
|
|
|
<button class="btn" onClick={() => persist('draft')} disabled={isSaving()}>
|
|
|
|
|
{isSaving() ? 'Saving...' : 'Save Draft'}
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn primary" onClick={() => persist('published')} disabled={isSaving()}>
|
|
|
|
|
{isSaving() ? 'Publishing...' : 'Publish'}
|
|
|
|
|
</button>
|
2026-03-16 23:20:54 +01:00
|
|
|
</div>
|
|
|
|
|
{statusMessage() && <p class="inline-note">{statusMessage()}</p>}
|
|
|
|
|
</section>
|
|
|
|
|
<section class="card">
|
|
|
|
|
<h2>Runtime Config Preview</h2>
|
|
|
|
|
<pre class="json">{JSON.stringify(config(), null, 2)}</pre>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</AdminShell>
|
|
|
|
|
);
|
|
|
|
|
}
|