diff --git a/src/components/admin/OnboardingFlowBuilder.tsx b/src/components/admin/OnboardingFlowBuilder.tsx new file mode 100644 index 0000000..d5d2fe5 --- /dev/null +++ b/src/components/admin/OnboardingFlowBuilder.tsx @@ -0,0 +1,339 @@ +import { createMemo, createSignal, For, Show } from 'solid-js'; + +export type OnboardingFieldType = 'text' | 'textarea' | 'number' | 'email' | 'tel' | 'date' | 'select' | 'url' | 'file' | 'checkbox'; + +export type OnboardingField = { + id: string; + label: string; + type: OnboardingFieldType; + required: boolean; + placeholder?: string; + options?: { label: string; value: string }[]; +}; + +export type OnboardingStep = { + id: string; + title: string; + description?: string; + fields: OnboardingField[]; +}; + +type QuestionCategory = 'business' | 'documents' | 'experience' | 'contact' | 'profile'; + +type OnboardingQuestion = { + key: string; + label: string; + type: OnboardingFieldType; + required: boolean; + category: QuestionCategory; + options?: { label: string; value: string }[]; +}; + +type Props = { + title: string; + roleKey: string; + description: string; + finalSubmissionMessage: string; + stepCount: number; + selectedFields: OnboardingField[]; + saving?: boolean; + error?: string; + primaryLabel: string; + onChange: (next: { + title?: string; + roleKey?: string; + description?: string; + finalSubmissionMessage?: string; + stepCount?: number; + selectedFields?: OnboardingField[]; + }) => void; + onSubmit: () => void; +}; + +const ROLE_OPTIONS = ['company', 'job_seeker', 'customer', 'photographer', 'video_editor', 'graphic_designer', 'social_media_manager', 'fitness_trainer', 'catering_services', 'makeup_artist', 'tutor', 'developer']; + +const DEFAULT_LIBRARY: OnboardingQuestion[] = [ + { key: 'full_name', label: 'Full Name', type: 'text', required: true, category: 'profile' }, + { key: 'email', label: 'Email', type: 'email', required: true, category: 'contact' }, + { key: 'phone', label: 'Phone', type: 'tel', required: true, category: 'contact' }, + { key: 'city', label: 'City', type: 'text', required: true, category: 'contact' }, + { key: 'experience_years', label: 'Experience (Years)', type: 'number', required: false, category: 'experience' }, + { key: 'portfolio_url', label: 'Portfolio URL', type: 'url', required: false, category: 'experience' }, + { key: 'gst_number', label: 'GST Number', type: 'text', required: false, category: 'business' }, + { key: 'pan_number', label: 'PAN Number', type: 'text', required: false, category: 'business' }, + { key: 'identity_document', label: 'Identity Document', type: 'file', required: true, category: 'documents' }, + { key: 'address_proof', label: 'Address Proof', type: 'file', required: true, category: 'documents' }, +]; + +const FIELD_TYPES: OnboardingFieldType[] = ['text', 'textarea', 'number', 'select', 'file', 'email', 'tel', 'date', 'url', 'checkbox']; + +function makeId(prefix: string): string { + return `${prefix}_${Math.random().toString(36).slice(2, 10)}`; +} + +function toSlug(input: string): string { + return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); +} + +function createFieldFromQuestion(question: OnboardingQuestion): OnboardingField { + return { + id: question.key || makeId('fld'), + label: question.label, + type: question.type, + required: question.required, + placeholder: question.type === 'file' ? 'Upload file' : `Enter ${question.label}`, + options: question.options, + }; +} + +function categoryLabel(category: QuestionCategory): string { + if (category === 'business') return 'Business'; + if (category === 'documents') return 'Documents'; + if (category === 'experience') return 'Experience'; + if (category === 'contact') return 'Contact'; + return 'Profile'; +} + +export function inferStepCount(steps: OnboardingStep[]): number { + return Math.max(1, steps.length || 1); +} + +export function buildStepsFromFields(selectedFields: OnboardingField[], stepCount: number): OnboardingStep[] { + const safeStepCount = Math.max(1, stepCount || 1); + const buckets: OnboardingField[][] = Array.from({ length: safeStepCount }, () => []); + selectedFields.forEach((field, index) => { + buckets[index % safeStepCount].push(field); + }); + return buckets.map((fields, index) => ({ + id: `step_${index + 1}`, + title: `Step ${index + 1}`, + description: '', + fields, + })); +} + +export function createDefaultFields(roleKey: string): OnboardingField[] { + if (roleKey === 'company') { + return DEFAULT_LIBRARY.filter((q) => ['full_name', 'email', 'phone', 'gst_number', 'pan_number', 'identity_document'].includes(q.key)).map(createFieldFromQuestion); + } + if (roleKey === 'photographer') { + return DEFAULT_LIBRARY.filter((q) => ['full_name', 'email', 'phone', 'city', 'experience_years', 'portfolio_url', 'identity_document'].includes(q.key)).map(createFieldFromQuestion); + } + return DEFAULT_LIBRARY.filter((q) => ['full_name', 'email', 'phone', 'city', 'identity_document'].includes(q.key)).map(createFieldFromQuestion); +} + +export default function OnboardingFlowBuilder(props: Props) { + const [activeTab, setActiveTab] = createSignal<'overview' | 'library' | 'selected' | 'preview'>('overview'); + const [customLabel, setCustomLabel] = createSignal(''); + const [customType, setCustomType] = createSignal('text'); + const [customRequired, setCustomRequired] = createSignal(true); + const [customOptions, setCustomOptions] = createSignal(''); + + const groupedQuestions = createMemo(() => { + const groups = new Map(); + DEFAULT_LIBRARY.forEach((question) => { + if (!groups.has(question.category)) groups.set(question.category, []); + groups.get(question.category)!.push(question); + }); + return Array.from(groups.entries()); + }); + + const selectedKeySet = createMemo(() => new Set(props.selectedFields.map((field) => field.id))); + const previewSteps = createMemo(() => buildStepsFromFields(props.selectedFields, props.stepCount)); + + const toggleQuestion = (question: OnboardingQuestion) => { + const checked = selectedKeySet().has(question.key); + if (checked) { + props.onChange({ selectedFields: props.selectedFields.filter((field) => field.id !== question.key) }); + return; + } + props.onChange({ selectedFields: [...props.selectedFields, createFieldFromQuestion(question)] }); + }; + + const updateField = (id: string, patch: Partial) => { + props.onChange({ + selectedFields: props.selectedFields.map((field) => (field.id === id ? { ...field, ...patch } : field)), + }); + }; + + const removeField = (id: string) => { + props.onChange({ selectedFields: props.selectedFields.filter((field) => field.id !== id) }); + }; + + const addCustomField = () => { + const label = customLabel().trim(); + if (!label) return; + const key = toSlug(label); + if (!key || selectedKeySet().has(key)) return; + const field: OnboardingField = { + id: key, + label, + type: customType(), + required: customRequired(), + options: customType() === 'select' + ? customOptions().split(',').map((item) => item.trim()).filter(Boolean).map((value) => ({ label: value, value })) + : undefined, + placeholder: customType() === 'file' ? 'Upload file' : `Enter ${label}`, + }; + props.onChange({ selectedFields: [...props.selectedFields, field] }); + setCustomLabel(''); + setCustomType('text'); + setCustomRequired(true); + setCustomOptions(''); + }; + + return ( +
+

Flow Builder

+

+ Choose the role, number of steps, questions, and final submission message. +

+ + +
{props.error}
+
+ +
+ + + + +
+ + +
+
+ + props.onChange({ title: event.currentTarget.value })} /> +
+
+ + +
+
+ +