- Install Tailwind CSS v4 via @tailwindcss/vite; configure vite.config.ts - Rewrite app.css: Tailwind base, Exo 2 font, brand tokens (orange #fd6216, navy #050026), scrollbar utility; fix @import order - Rewrite AdminShell.tsx: fixed header, fixed inset body grid (sidebar + main), session check, sub-tab system, logout, admin avatar/name/role - Rewrite AdminSidebar.tsx: collapsible w-64/w-20, orange active rail + badge/dot, CSS filter for SVG icon tinting, min-h-0 flex fix - Replace 84 route stub CSS classes (page-title, card, btn, table-wrap, etc.) with Tailwind equivalents via safe class-attr-only regex script - Rewrite admin dashboard: Lucide icons in colored chip backgrounds, 4-col KPI grid, Control Plane 6-module grid, hover lift animations - Disable SSR (ssr: false) to fix Vinxi dev manifest error; clear stale .vinxi cache - Add lucide-solid icon library - Add scripts/cleanup-css.mjs for class migration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
6 KiB
TypeScript
141 lines
6 KiB
TypeScript
import { A, useNavigate } from '@solidjs/router';
|
|
import { createMemo, createSignal, onMount } from 'solid-js';
|
|
import AdminShell from '~/components/AdminShell';
|
|
import OnboardingManagementTabs from '~/components/admin/OnboardingManagementTabs';
|
|
import OnboardingFlowBuilder, {
|
|
buildStepsFromFields,
|
|
createDefaultFields,
|
|
type OnboardingField,
|
|
} from '~/components/admin/OnboardingFlowBuilder';
|
|
|
|
const API = '/api/gateway';
|
|
const FRONTEND_PREVIEW_BASE = String(import.meta.env.VITE_FRONTEND_PREVIEW_URL || 'http://localhost:3001').replace(/\/+$/, '');
|
|
|
|
function normalizeRoleKey(value: string): string {
|
|
return String(value || '').trim().toUpperCase().replace(/[-\s]+/g, '_');
|
|
}
|
|
|
|
export default function NewOnboardingSchemaPage() {
|
|
const navigate = useNavigate();
|
|
const [roleMap, setRoleMap] = createSignal<Record<string, string>>({});
|
|
const [title, setTitle] = createSignal('');
|
|
const [roleKey, setRoleKey] = createSignal('company');
|
|
const [description, setDescription] = createSignal('');
|
|
const [finalSubmissionMessage, setFinalSubmissionMessage] = createSignal('Your onboarding has been submitted for review. We will notify you once it is approved.');
|
|
const [stepCount, setStepCount] = createSignal(2);
|
|
const [selectedFields, setSelectedFields] = createSignal<OnboardingField[]>(createDefaultFields('company'));
|
|
const [saving, setSaving] = createSignal(false);
|
|
const [error, setError] = createSignal('');
|
|
|
|
onMount(async () => {
|
|
try {
|
|
const res = await fetch(`${API}/api/admin/roles?audience=EXTERNAL`);
|
|
if (!res.ok) return;
|
|
const payload = await res.json();
|
|
const rows = Array.isArray(payload) ? payload : (payload.roles || []);
|
|
const map: Record<string, string> = {};
|
|
rows
|
|
.filter((item: any) => String(item?.audience || '').toUpperCase() === 'EXTERNAL')
|
|
.forEach((item: any) => {
|
|
const key = String(item?.key || '').trim().toUpperCase();
|
|
if (!key) return;
|
|
map[key] = String(item?.id || '');
|
|
});
|
|
setRoleMap(map);
|
|
} catch {
|
|
setRoleMap({});
|
|
}
|
|
});
|
|
|
|
const payload = createMemo(() => ({
|
|
title: title(),
|
|
roleKey: roleKey(),
|
|
description: description(),
|
|
finalSubmissionMessage: finalSubmissionMessage(),
|
|
steps: buildStepsFromFields(selectedFields(), stepCount()),
|
|
}));
|
|
const livePreviewUrl = createMemo(() => {
|
|
const role = normalizeRoleKey(roleKey());
|
|
if (!role) return '';
|
|
return `${FRONTEND_PREVIEW_BASE}/onboarding?${new URLSearchParams({ roleKey: role }).toString()}`;
|
|
});
|
|
|
|
const handleChange = (next: {
|
|
title?: string;
|
|
roleKey?: string;
|
|
description?: string;
|
|
finalSubmissionMessage?: string;
|
|
stepCount?: number;
|
|
selectedFields?: OnboardingField[];
|
|
}) => {
|
|
if (typeof next.title === 'string') setTitle(next.title);
|
|
if (typeof next.description === 'string') setDescription(next.description);
|
|
if (typeof next.finalSubmissionMessage === 'string') setFinalSubmissionMessage(next.finalSubmissionMessage);
|
|
if (typeof next.stepCount === 'number') setStepCount(next.stepCount);
|
|
if (Array.isArray(next.selectedFields)) setSelectedFields(next.selectedFields);
|
|
if (typeof next.roleKey === 'string') {
|
|
setRoleKey(next.roleKey);
|
|
setSelectedFields(createDefaultFields(next.roleKey));
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
try {
|
|
setSaving(true);
|
|
setError('');
|
|
const normalizedRole = normalizeRoleKey(roleKey());
|
|
const roleId = roleMap()[normalizedRole];
|
|
if (!roleId) {
|
|
throw new Error('Please choose a valid role before creating this flow.');
|
|
}
|
|
const response = await fetch(`${API}/api/admin/onboarding-config`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ role_id: roleId, schema_json: payload() }),
|
|
});
|
|
const body = await response.json();
|
|
if (!response.ok) throw new Error(body?.message || 'Failed to create onboarding flow');
|
|
navigate(`/admin/onboarding-schemas/${body.role_id || roleId}`);
|
|
} catch (nextError: any) {
|
|
setError(nextError?.message || 'Failed to create onboarding flow');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AdminShell>
|
|
<OnboardingManagementTabs />
|
|
|
|
<div class="mb-6 flex items-start justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900">Create Onboarding Flow</h1>
|
|
<p class="mt-1 text-sm text-gray-500">Create one onboarding form at a time. Pick the role, choose the questions, set the steps, and write the final success message.</p>
|
|
</div>
|
|
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/onboarding-schemas">Back to Onboarding Management</A>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:16px">
|
|
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Role</p><p class="kv-value">{roleKey().replace(/_/g, ' ').toUpperCase()}</p></div>
|
|
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Steps</p><p class="kv-value">{stepCount()}</p></div>
|
|
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Questions</p><p class="kv-value">{selectedFields().length}</p></div>
|
|
</div>
|
|
|
|
<OnboardingFlowBuilder
|
|
title={title()}
|
|
roleKey={roleKey()}
|
|
description={description()}
|
|
finalSubmissionMessage={finalSubmissionMessage()}
|
|
stepCount={stepCount()}
|
|
selectedFields={selectedFields()}
|
|
saving={saving()}
|
|
error={error()}
|
|
livePreviewUrl={livePreviewUrl()}
|
|
livePreviewHint="Create page preview uses the role-level runtime onboarding flow. Save the flow first to preview the exact saved flow by schema id."
|
|
primaryLabel="Create Onboarding Flow"
|
|
onChange={handleChange}
|
|
onSubmit={handleSubmit}
|
|
/>
|
|
</AdminShell>
|
|
);
|
|
}
|