import React, { useState, useEffect, useCallback } from 'react'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; import { Input } from '../../components/ui/input'; import { Label } from '../../components/ui/label'; import { Textarea } from '../../components/ui/textarea'; import { Checkbox } from '../../components/ui/checkbox'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../components/ui/select'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../../components/ui/dialog'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '../../components/ui/accordion'; import { toast } from 'sonner'; import api from '../../utils/api'; import { Loader2, Save, RotateCcw, Eye, Plus, Trash2, GripVertical, Type, Mail, Phone, Calendar, List, CheckSquare, Circle, AlignLeft, Upload, Lock, ChevronUp, ChevronDown, Settings, Zap, Copy, X, Grip } from 'lucide-react'; // Field type icons const FIELD_TYPE_ICONS = { text: Type, email: Mail, phone: Phone, date: Calendar, dropdown: List, checkbox: CheckSquare, radio: Circle, multiselect: CheckSquare, textarea: AlignLeft, file_upload: Upload, password: Lock, address_group: Type, }; // Field type labels const FIELD_TYPE_LABELS = { text: 'Text Input', email: 'Email', phone: 'Phone', date: 'Date', dropdown: 'Dropdown', checkbox: 'Checkbox', radio: 'Radio Group', multiselect: 'Multi-Select', textarea: 'Text Area', file_upload: 'File Upload', password: 'Password', address_group: 'Address Group', }; const AdminRegistrationBuilder = () => { const [schema, setSchema] = useState(null); const [originalSchema, setOriginalSchema] = useState(null); const [fieldTypes, setFieldTypes] = useState({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); // UI State const [selectedStep, setSelectedStep] = useState(null); const [selectedSection, setSelectedSection] = useState(null); const [selectedField, setSelectedField] = useState(null); const [previewOpen, setPreviewOpen] = useState(false); const [addStepDialogOpen, setAddStepDialogOpen] = useState(false); const [addSectionDialogOpen, setAddSectionDialogOpen] = useState(false); const [addFieldDialogOpen, setAddFieldDialogOpen] = useState(false); const [conditionalDialogOpen, setConditionalDialogOpen] = useState(false); // Form state for dialogs const [newStepData, setNewStepData] = useState({ title: '', description: '' }); const [newSectionData, setNewSectionData] = useState({ title: '', description: '' }); const [newFieldType, setNewFieldType] = useState('text'); // Fetch schema and field types on mount useEffect(() => { fetchSchema(); fetchFieldTypes(); }, []); const fetchSchema = async () => { try { const response = await api.get('/admin/registration/schema'); setSchema(response.data.schema); setOriginalSchema(JSON.parse(JSON.stringify(response.data.schema))); if (response.data.schema?.steps?.length > 0) { setSelectedStep(response.data.schema.steps[0].id); } } catch (error) { toast.error('Failed to load registration schema'); console.error(error); } finally { setLoading(false); } }; const fetchFieldTypes = async () => { try { const response = await api.get('/admin/registration/field-types'); setFieldTypes(response.data); } catch (error) { console.error('Failed to load field types:', error); } }; // Check for changes useEffect(() => { if (schema && originalSchema) { setHasChanges(JSON.stringify(schema) !== JSON.stringify(originalSchema)); } }, [schema, originalSchema]); // Save schema const handleSave = async () => { setSaving(true); try { // Validate first const validateResponse = await api.post('/admin/registration/schema/validate', { schema_data: schema, }); if (!validateResponse.data.valid) { toast.error(validateResponse.data.errors.join(', ')); return; } // Save await api.put('/admin/registration/schema', { schema_data: schema }); setOriginalSchema(JSON.parse(JSON.stringify(schema))); toast.success('Registration form saved successfully'); } catch (error) { toast.error(error.response?.data?.detail?.message || 'Failed to save schema'); } finally { setSaving(false); } }; // Reset to default const handleReset = async () => { if (!window.confirm('Are you sure you want to reset to the default registration form? This cannot be undone.')) { return; } try { await api.post('/admin/registration/schema/reset'); await fetchSchema(); toast.success('Registration form reset to default'); } catch (error) { toast.error('Failed to reset schema'); } }; // Get sorted steps const sortedSteps = schema?.steps?.sort((a, b) => a.order - b.order) || []; // Get current step const currentStep = sortedSteps.find((s) => s.id === selectedStep); // Get sections for current step const sortedSections = currentStep?.sections?.sort((a, b) => a.order - b.order) || []; // Update schema const updateSchema = useCallback((updater) => { setSchema((prev) => { const updated = typeof updater === 'function' ? updater(prev) : updater; return { ...updated }; }); }, []); // Add step const handleAddStep = () => { if (!newStepData.title.trim()) { toast.error('Step title is required'); return; } const newStep = { id: `step_${Date.now()}`, title: newStepData.title, description: newStepData.description, order: sortedSteps.length + 1, sections: [], }; updateSchema((prev) => ({ ...prev, steps: [...prev.steps, newStep], })); setSelectedStep(newStep.id); setAddStepDialogOpen(false); setNewStepData({ title: '', description: '' }); }; // Delete step const handleDeleteStep = (stepId) => { const step = schema.steps.find((s) => s.id === stepId); if (step?.sections?.some((sec) => sec.fields?.some((f) => f.is_fixed))) { toast.error('Cannot delete step containing fixed fields'); return; } if (!window.confirm('Are you sure you want to delete this step?')) { return; } updateSchema((prev) => ({ ...prev, steps: prev.steps .filter((s) => s.id !== stepId) .map((s, idx) => ({ ...s, order: idx + 1 })), })); if (selectedStep === stepId) { setSelectedStep(sortedSteps[0]?.id); } }; // Move step const handleMoveStep = (stepId, direction) => { const stepIndex = sortedSteps.findIndex((s) => s.id === stepId); const newIndex = direction === 'up' ? stepIndex - 1 : stepIndex + 1; if (newIndex < 0 || newIndex >= sortedSteps.length) return; updateSchema((prev) => { const steps = [...prev.steps]; const step = steps.find((s) => s.id === stepId); const otherStep = sortedSteps[newIndex]; const otherStepInArray = steps.find((s) => s.id === otherStep.id); const tempOrder = step.order; step.order = otherStepInArray.order; otherStepInArray.order = tempOrder; return { ...prev, steps }; }); }; // Add section const handleAddSection = () => { if (!newSectionData.title.trim()) { toast.error('Section title is required'); return; } const newSection = { id: `section_${Date.now()}`, title: newSectionData.title, description: newSectionData.description, order: sortedSections.length + 1, fields: [], }; updateSchema((prev) => ({ ...prev, steps: prev.steps.map((s) => s.id === selectedStep ? { ...s, sections: [...(s.sections || []), newSection] } : s ), })); setSelectedSection(newSection.id); setAddSectionDialogOpen(false); setNewSectionData({ title: '', description: '' }); }; // Delete section const handleDeleteSection = (sectionId) => { const section = currentStep?.sections?.find((s) => s.id === sectionId); if (section?.fields?.some((f) => f.is_fixed)) { toast.error('Cannot delete section containing fixed fields'); return; } if (!window.confirm('Are you sure you want to delete this section?')) { return; } updateSchema((prev) => ({ ...prev, steps: prev.steps.map((s) => s.id === selectedStep ? { ...s, sections: s.sections .filter((sec) => sec.id !== sectionId) .map((sec, idx) => ({ ...sec, order: idx + 1 })), } : s ), })); if (selectedSection === sectionId) { setSelectedSection(null); } }; // Add field const handleAddField = () => { if (!selectedSection) { toast.error('Please select a section first'); return; } const section = currentStep?.sections?.find((s) => s.id === selectedSection); const fieldCount = section?.fields?.length || 0; const newField = { id: `field_${Date.now()}`, type: newFieldType, label: `New ${FIELD_TYPE_LABELS[newFieldType] || 'Field'}`, required: false, is_fixed: false, width: 'full', order: fieldCount + 1, options: ['dropdown', 'radio', 'multiselect'].includes(newFieldType) ? [{ value: 'option1', label: 'Option 1' }] : undefined, }; updateSchema((prev) => ({ ...prev, steps: prev.steps.map((s) => s.id === selectedStep ? { ...s, sections: s.sections.map((sec) => sec.id === selectedSection ? { ...sec, fields: [...(sec.fields || []), newField] } : sec ), } : s ), })); setSelectedField(newField.id); setAddFieldDialogOpen(false); }; // Delete field const handleDeleteField = (fieldId) => { const section = currentStep?.sections?.find((s) => s.id === selectedSection); const field = section?.fields?.find((f) => f.id === fieldId); if (field?.is_fixed) { toast.error('Cannot delete fixed fields'); return; } if (!window.confirm('Are you sure you want to delete this field?')) { return; } updateSchema((prev) => ({ ...prev, steps: prev.steps.map((s) => s.id === selectedStep ? { ...s, sections: s.sections.map((sec) => sec.id === selectedSection ? { ...sec, fields: sec.fields .filter((f) => f.id !== fieldId) .map((f, idx) => ({ ...f, order: idx + 1 })), } : sec ), } : s ), })); if (selectedField === fieldId) { setSelectedField(null); } }; // Update field const handleUpdateField = (fieldId, updates) => { updateSchema((prev) => ({ ...prev, steps: prev.steps.map((s) => s.id === selectedStep ? { ...s, sections: s.sections.map((sec) => sec.id === selectedSection ? { ...sec, fields: sec.fields.map((f) => f.id === fieldId ? { ...f, ...updates } : f ), } : sec ), } : s ), })); }; // Get selected field data const selectedFieldData = currentStep?.sections ?.find((s) => s.id === selectedSection) ?.fields?.find((f) => f.id === selectedField); if (loading) { return (
); } return (
{/* Header */}

Registration Form Builder

Customize the registration form steps, sections, and fields.

{hasChanges && (
You have unsaved changes. Click "Save Changes" to apply them.
)} {/* Main Builder Layout */} {/* Left Sidebar - Steps & Sections */}

Steps

{sortedSteps.map((step, index) => (
{ setSelectedStep(step.id); setSelectedSection(null); setSelectedField(null); }} >
Step:
{step.title}
{/* Mod Buttons */}
{/* */}
))}
{/* Sections for selected step */} {currentStep && ( <>

Sections

{sortedSections.map((section) => (
{ setSelectedSection(section.id); setSelectedField(null); }} >
{section.title}
))}
)}
{/* Center - Form Canvas */}

{currentStep?.title || 'Select a step'}

{selectedSection && ( )}
{currentStep?.description && (

{currentStep.description}

)} {/* Sections and Fields */} {sortedSections.map((section) => { const sortedFields = section.fields?.sort((a, b) => a.order - b.order) || []; return (
setSelectedSection(section.id)} >

{section.title}

{section.description && (

{section.description}

)} {/* Fields */}
{sortedFields.map((field) => { const IconComponent = FIELD_TYPE_ICONS[field.type] || Type; return (
{ e.stopPropagation(); setSelectedSection(section.id); setSelectedField(field.id); }} >
{field.label} {field.required && *} ({FIELD_TYPE_LABELS[field.type] || field.type})
{field.is_fixed && ( )}
{!field.is_fixed && ( )}
); })} {sortedFields.length === 0 && (
No fields in this section. Click "Add Field" to add one.
)}
); })} {sortedSections.length === 0 && (
No sections in this step. Add a section from the left sidebar.
)}
{/* Right Sidebar - Field Properties */}

Field Properties

{selectedFieldData ? (
{/* Field ID */}
{selectedFieldData.id}
{/* Label */}
handleUpdateField(selectedField, { label: e.target.value })} disabled={selectedFieldData.is_fixed} />
{/* Type */}
{/* Required */}
handleUpdateField(selectedField, { required: checked }) } disabled={selectedFieldData.is_fixed} />
{/* Width */}
{/* Placeholder */} {['text', 'email', 'phone', 'textarea'].includes(selectedFieldData.type) && (
handleUpdateField(selectedField, { placeholder: e.target.value }) } />
)} {/* Options for dropdown/radio/multiselect */} {['dropdown', 'radio', 'multiselect'].includes(selectedFieldData.type) && (
{(selectedFieldData.options || []).map((option, idx) => (
{ const newOptions = [...selectedFieldData.options]; newOptions[idx] = { ...newOptions[idx], label: e.target.value, value: e.target.value.toLowerCase().replace(/\s+/g, '_'), }; handleUpdateField(selectedField, { options: newOptions }); }} placeholder="Option label" />
))}
)} {/* Mapping */}
handleUpdateField(selectedField, { mapping: e.target.value }) } placeholder="Leave empty for custom data" disabled={selectedFieldData.is_fixed} />

Maps to User model field. Empty = stored in custom_registration_data

{selectedFieldData.is_fixed && (
This is a fixed field and cannot be removed or have its core properties changed.
)}
) : (
Select a field to edit its properties
)}
{/* Conditional Rules */}

Conditional Rules

{schema?.conditional_rules?.length || 0} conditional rule(s) configured
{/* Add Step Dialog */} Add New Step
setNewStepData({ ...newStepData, title: e.target.value })} placeholder="e.g., Additional Information" />