Compare commits
8 Commits
68ee22c124
...
features
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96e4b711a8 | ||
|
|
ced8d75bcc | ||
|
|
f0ee505339 | ||
|
|
21338f1541 | ||
|
|
da366272b4 | ||
|
|
af27190e29 | ||
|
|
235156a9ee | ||
|
|
01a3c38085 |
17
src/App.js
17
src/App.js
@@ -239,6 +239,20 @@ function App() {
|
|||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
|
<Route path="/admin/registration" element={
|
||||||
|
<PrivateRoute adminOnly>
|
||||||
|
<AdminLayout>
|
||||||
|
<AdminRegistrationBuilder />
|
||||||
|
</AdminLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/admin/member-tiers" element={
|
||||||
|
<PrivateRoute adminOnly>
|
||||||
|
<AdminLayout>
|
||||||
|
<AdminMemberTiers />
|
||||||
|
</AdminLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
} />
|
||||||
<Route path="/admin/plans" element={
|
<Route path="/admin/plans" element={
|
||||||
<PrivateRoute adminOnly>
|
<PrivateRoute adminOnly>
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
@@ -293,6 +307,7 @@ function App() {
|
|||||||
<Navigate to="/admin/settings/permissions" replace />
|
<Navigate to="/admin/settings/permissions" replace />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/admin/settings" element={
|
<Route path="/admin/settings" element={
|
||||||
<PrivateRoute adminOnly>
|
<PrivateRoute adminOnly>
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
@@ -303,9 +318,7 @@ function App() {
|
|||||||
<Route index element={<Navigate to="stripe" replace />} />
|
<Route index element={<Navigate to="stripe" replace />} />
|
||||||
<Route path="stripe" element={<AdminSettings />} />
|
<Route path="stripe" element={<AdminSettings />} />
|
||||||
<Route path="permissions" element={<AdminRoles />} />
|
<Route path="permissions" element={<AdminRoles />} />
|
||||||
<Route path="member-tiers" element={<AdminMemberTiers />} />
|
|
||||||
<Route path="theme" element={<AdminTheme />} />
|
<Route path="theme" element={<AdminTheme />} />
|
||||||
<Route path="registration" element={<AdminRegistrationBuilder />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* 404 - Catch all undefined routes */}
|
{/* 404 - Catch all undefined routes */}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import {
|
|||||||
Heart,
|
Heart,
|
||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
|
Star,
|
||||||
|
FileEdit
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
||||||
@@ -104,18 +106,31 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
path: '/admin',
|
path: '/admin',
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'Staff',
|
name: 'Staff & Admins',
|
||||||
icon: UserCog,
|
icon: UserCog,
|
||||||
path: '/admin/staff',
|
path: '/admin/staff',
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Members',
|
name: 'Member Roster',
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: '/admin/members',
|
path: '/admin/members',
|
||||||
disabled: false
|
disabled: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Member Tiers',
|
||||||
|
icon: Star,
|
||||||
|
path: '/admin/member-tiers',
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Registration',
|
||||||
|
icon: FileEdit,
|
||||||
|
path: '/admin/registration',
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Validations',
|
name: 'Validations',
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
@@ -316,6 +331,18 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
{/* Dashboard - Standalone */}
|
{/* Dashboard - Standalone */}
|
||||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))}
|
{renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))}
|
||||||
|
|
||||||
|
{/* Onboarding Section */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="px-4 py-2 mt-6">
|
||||||
|
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
Onboarding
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{renderNavItem(filteredNavItems.find(item => item.name === 'Registration'))}
|
||||||
|
{renderNavItem(filteredNavItems.find(item => item.name === 'Validations'))}
|
||||||
|
</div>
|
||||||
{/* MEMBERSHIP Section */}
|
{/* MEMBERSHIP Section */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="px-4 py-2 mt-6">
|
<div className="px-4 py-2 mt-6">
|
||||||
@@ -325,9 +352,9 @@ const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Staff'))}
|
{renderNavItem(filteredNavItems.find(item => item.name === 'Member Roster'))}
|
||||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Members'))}
|
{renderNavItem(filteredNavItems.find(item => item.name === 'Member Tiers'))}
|
||||||
{renderNavItem(filteredNavItems.find(item => item.name === 'Validations'))}
|
{renderNavItem(filteredNavItems.find(item => item.name === 'Staff & Admins'))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FINANCIALS Section */}
|
{/* FINANCIALS Section */}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const ChangePasswordDialog = ({ open, onOpenChange }) => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="btn-outline mr-33"
|
className="btn-outline mr-33 text-white"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { CreditCard, Shield, Star, Palette, FileEdit } from 'lucide-react';
|
|||||||
const settingsItems = [
|
const settingsItems = [
|
||||||
{ label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard },
|
{ label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard },
|
||||||
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
|
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
|
||||||
{ label: 'Member Tiers', path: '/admin/settings/member-tiers', icon: Star },
|
|
||||||
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette },
|
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette },
|
||||||
{ label: 'Registration Form', path: '/admin/settings/registration', icon: FileEdit },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const SettingsTabs = () => {
|
const SettingsTabs = () => {
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ const DynamicFormField = ({
|
|||||||
const hasError = errors.length > 0;
|
const hasError = errors.length > 0;
|
||||||
const errorMessage = errors[0];
|
const errorMessage = errors[0];
|
||||||
|
|
||||||
|
const formatPhoneNumber = (rawValue) => {
|
||||||
|
const digits = String(rawValue || '').replace(/\D/g, '').slice(0, 10);
|
||||||
|
if (digits.length <= 3) return digits;
|
||||||
|
if (digits.length <= 6) {
|
||||||
|
return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
|
||||||
|
}
|
||||||
|
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
||||||
|
};
|
||||||
|
|
||||||
// Common input className
|
// Common input className
|
||||||
const inputClassName = `h-14 rounded-xl border-2 ${
|
const inputClassName = `h-14 rounded-xl border-2 ${
|
||||||
hasError
|
hasError
|
||||||
@@ -59,6 +68,11 @@ const DynamicFormField = ({
|
|||||||
const { value: newValue, type: inputType, checked } = e.target;
|
const { value: newValue, type: inputType, checked } = e.target;
|
||||||
if (inputType === 'checkbox') {
|
if (inputType === 'checkbox') {
|
||||||
onChange(id, checked);
|
onChange(id, checked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'phone') {
|
||||||
|
onChange(id, formatPhoneNumber(newValue));
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
onChange(id, newValue);
|
onChange(id, newValue);
|
||||||
}
|
}
|
||||||
@@ -111,6 +125,8 @@ const DynamicFormField = ({
|
|||||||
value={value || ''}
|
value={value || ''}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
inputMode={type === 'phone' ? 'numeric' : undefined}
|
||||||
|
maxLength={type === 'phone' ? 14 : undefined}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
data-testid={`field-${id}`}
|
data-testid={`field-${id}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const AdminDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Link to={'/'} className=''>
|
<Link to={'/'} className=''>
|
||||||
<Button
|
<Button
|
||||||
className="btn-lavender mb-8 md:mb-0 "
|
className="btn-lavender mb-8 md:mb-0 mr-4 "
|
||||||
>
|
>
|
||||||
<Globe />
|
<Globe />
|
||||||
View Public Site
|
View Public Site
|
||||||
|
|||||||
@@ -150,9 +150,15 @@ const AdminMemberTiers = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header and Actions */}
|
{/* Header and Actions */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl md:text-5xl font-semibold text-[var(--purple-ink)] mb-4" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
Members Tiers
|
||||||
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Configure tier names, time ranges, and badges displayed in the members directory.
|
Configure tier names, time ranges, and badges displayed in the members directory.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<Button variant="outline" onClick={handleDiscardChanges}>
|
<Button variant="outline" onClick={handleDiscardChanges}>
|
||||||
|
|||||||
@@ -19,12 +19,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '../../components/ui/dialog';
|
} from '../../components/ui/dialog';
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from '../../components/ui/accordion';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import {
|
import {
|
||||||
@@ -45,12 +39,9 @@ import {
|
|||||||
AlignLeft,
|
AlignLeft,
|
||||||
Upload,
|
Upload,
|
||||||
Lock,
|
Lock,
|
||||||
ChevronUp,
|
|
||||||
ChevronDown,
|
|
||||||
Settings,
|
Settings,
|
||||||
Zap,
|
Zap,
|
||||||
Copy,
|
X
|
||||||
X,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Field type icons
|
// Field type icons
|
||||||
@@ -191,6 +182,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
|
|
||||||
// Get current step
|
// Get current step
|
||||||
const currentStep = sortedSteps.find((s) => s.id === selectedStep);
|
const currentStep = sortedSteps.find((s) => s.id === selectedStep);
|
||||||
|
const currentStepIndex = sortedSteps.findIndex((s) => s.id === selectedStep);
|
||||||
|
|
||||||
// Get sections for current step
|
// Get sections for current step
|
||||||
const sortedSections = currentStep?.sections?.sort((a, b) => a.order - b.order) || [];
|
const sortedSections = currentStep?.sections?.sort((a, b) => a.order - b.order) || [];
|
||||||
@@ -334,13 +326,13 @@ const AdminRegistrationBuilder = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add field
|
// Add field
|
||||||
const handleAddField = () => {
|
const handleAddField = (sectionId = selectedSection) => {
|
||||||
if (!selectedSection) {
|
if (!sectionId) {
|
||||||
toast.error('Please select a section first');
|
toast.error('Please select a section first');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const section = currentStep?.sections?.find((s) => s.id === selectedSection);
|
const section = currentStep?.sections?.find((s) => s.id === sectionId);
|
||||||
const fieldCount = section?.fields?.length || 0;
|
const fieldCount = section?.fields?.length || 0;
|
||||||
|
|
||||||
const newField = {
|
const newField = {
|
||||||
@@ -363,7 +355,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
? {
|
? {
|
||||||
...s,
|
...s,
|
||||||
sections: s.sections.map((sec) =>
|
sections: s.sections.map((sec) =>
|
||||||
sec.id === selectedSection
|
sec.id === sectionId
|
||||||
? { ...sec, fields: [...(sec.fields || []), newField] }
|
? { ...sec, fields: [...(sec.fields || []), newField] }
|
||||||
: sec
|
: sec
|
||||||
),
|
),
|
||||||
@@ -372,13 +364,14 @@ const AdminRegistrationBuilder = () => {
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
setSelectedSection(sectionId);
|
||||||
setSelectedField(newField.id);
|
setSelectedField(newField.id);
|
||||||
setAddFieldDialogOpen(false);
|
setAddFieldDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete field
|
// Delete field
|
||||||
const handleDeleteField = (fieldId) => {
|
const handleDeleteField = (sectionId, fieldId) => {
|
||||||
const section = currentStep?.sections?.find((s) => s.id === selectedSection);
|
const section = currentStep?.sections?.find((s) => s.id === sectionId);
|
||||||
const field = section?.fields?.find((f) => f.id === fieldId);
|
const field = section?.fields?.find((f) => f.id === fieldId);
|
||||||
|
|
||||||
if (field?.is_fixed) {
|
if (field?.is_fixed) {
|
||||||
@@ -397,7 +390,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
? {
|
? {
|
||||||
...s,
|
...s,
|
||||||
sections: s.sections.map((sec) =>
|
sections: s.sections.map((sec) =>
|
||||||
sec.id === selectedSection
|
sec.id === sectionId
|
||||||
? {
|
? {
|
||||||
...sec,
|
...sec,
|
||||||
fields: sec.fields
|
fields: sec.fields
|
||||||
@@ -417,7 +410,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Update field
|
// Update field
|
||||||
const handleUpdateField = (fieldId, updates) => {
|
const handleUpdateField = (sectionId, fieldId, updates) => {
|
||||||
updateSchema((prev) => ({
|
updateSchema((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
steps: prev.steps.map((s) =>
|
steps: prev.steps.map((s) =>
|
||||||
@@ -425,7 +418,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
? {
|
? {
|
||||||
...s,
|
...s,
|
||||||
sections: s.sections.map((sec) =>
|
sections: s.sections.map((sec) =>
|
||||||
sec.id === selectedSection
|
sec.id === sectionId
|
||||||
? {
|
? {
|
||||||
...sec,
|
...sec,
|
||||||
fields: sec.fields.map((f) =>
|
fields: sec.fields.map((f) =>
|
||||||
@@ -440,10 +433,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get selected field data
|
|
||||||
const selectedFieldData = currentStep?.sections
|
|
||||||
?.find((s) => s.id === selectedSection)
|
|
||||||
?.fields?.find((f) => f.id === selectedField);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -493,24 +483,23 @@ const AdminRegistrationBuilder = () => {
|
|||||||
|
|
||||||
{/* Main Builder Layout */}
|
{/* Main Builder Layout */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
{/* Left Sidebar - Steps & Sections */}
|
<div className="lg:col-span-9">
|
||||||
<div className="lg:col-span-3">
|
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-lg font-semibold">Steps</h2>
|
<h2 className="text-lg font-semibold">Steps</h2>
|
||||||
<Button size="sm" variant="ghost" onClick={() => setAddStepDialogOpen(true)}>
|
<Button size="sm" variant="ghost" className="w-32" onClick={() => setAddStepDialogOpen(true)}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
|
Add Step
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex flex-wrap items-end gap-2 border-b border-gray-200">
|
||||||
{sortedSteps.map((step, index) => (
|
{sortedSteps.map((step, index) => (
|
||||||
<div
|
<div
|
||||||
key={step.id}
|
key={step.id}
|
||||||
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
|
className={`px-3 py-2 rounded-t-lg border cursor-pointer transition-colors ${selectedStep === step.id
|
||||||
selectedStep === step.id
|
? 'bg-white border-gray-300 border-b-white'
|
||||||
? 'border-brand-purple bg-brand-lavender/10'
|
: 'bg-gray-50 border-transparent hover:bg-gray-100'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedStep(step.id);
|
setSelectedStep(step.id);
|
||||||
@@ -518,69 +507,59 @@ const AdminRegistrationBuilder = () => {
|
|||||||
setSelectedField(null);
|
setSelectedField(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<GripVertical className="h-4 w-4 text-gray-400" />
|
<span className="font-medium">Step {index + 1}</span>
|
||||||
<span className="font-medium text-sm">{step.title}</span>
|
{/* <span className="text-sm text-muted-foreground">{step.title || 'Untitled Step'}</span> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6"
|
className="h-6 w-6 text-red-500 hover:text-background"
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleMoveStep(step.id, 'up');
|
|
||||||
}}
|
|
||||||
disabled={index === 0}
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleMoveStep(step.id, 'down');
|
|
||||||
}}
|
|
||||||
disabled={index === sortedSteps.length - 1}
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6 text-red-500 hover:text-red-700"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteStep(step.id);
|
handleDeleteStep(step.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{sortedSteps.length === 0 && (
|
||||||
|
<div className="text-center py-6 text-muted-foreground">
|
||||||
|
No steps yet. Click "Add Step" to get started.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sections for selected step */}
|
{/* Sections for selected step */}
|
||||||
{currentStep && (
|
{currentStep && (
|
||||||
<>
|
<div className="mt-6">
|
||||||
<div className="flex justify-between items-center mt-6 mb-4">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 mb-4">
|
||||||
<h2 className="text-lg font-semibold">Sections</h2>
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
Step {currentStepIndex + 1}: {currentStep?.title || 'Untitled Step'}
|
||||||
|
</h2>
|
||||||
|
{currentStep.description && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">{currentStep.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Button size="sm" variant="ghost" onClick={() => setAddSectionDialogOpen(true)}>
|
<Button size="sm" variant="ghost" onClick={() => setAddSectionDialogOpen(true)}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
|
Add Section
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
{sortedSections.map((section) => (
|
{sortedSections.map((section) => {
|
||||||
|
const sortedFields = section.fields?.sort((a, b) => a.order - b.order) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={section.id}
|
key={section.id}
|
||||||
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
|
className={`p-6 rounded-lg border cursor-pointer transition-colors ${selectedSection === section.id
|
||||||
selectedSection === section.id
|
|
||||||
? 'border-brand-purple bg-brand-lavender/10'
|
? 'border-brand-purple bg-brand-lavender/10'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
@@ -589,12 +568,17 @@ const AdminRegistrationBuilder = () => {
|
|||||||
setSelectedField(null);
|
setSelectedField(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<span className="text-sm">{section.title}</span>
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">{section.title || 'Untitled Section'}</h3>
|
||||||
|
{section.description && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">{section.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 text-red-500 hover:text-red-700"
|
className="h-6 w-6 text-red-500 hover:text-background"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteSection(section.id);
|
handleDeleteSection(section.id);
|
||||||
@@ -603,61 +587,16 @@ const AdminRegistrationBuilder = () => {
|
|||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Center - Form Canvas */}
|
|
||||||
<div className="lg:col-span-6">
|
|
||||||
<Card className="p-6">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h2 className="text-xl font-semibold">
|
|
||||||
{currentStep?.title || 'Select a step'}
|
|
||||||
</h2>
|
|
||||||
{selectedSection && (
|
|
||||||
<Button size="sm" onClick={() => setAddFieldDialogOpen(true)}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Add Field
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{currentStep?.description && (
|
|
||||||
<p className="text-muted-foreground mb-6">{currentStep.description}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sections and Fields */}
|
|
||||||
{sortedSections.map((section) => {
|
|
||||||
const sortedFields = section.fields?.sort((a, b) => a.order - b.order) || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={section.id}
|
|
||||||
className={`mb-6 p-4 rounded-lg border-2 ${
|
|
||||||
selectedSection === section.id
|
|
||||||
? 'border-brand-purple'
|
|
||||||
: 'border-dashed border-gray-200'
|
|
||||||
}`}
|
|
||||||
onClick={() => setSelectedSection(section.id)}
|
|
||||||
>
|
|
||||||
<h3 className="text-lg font-medium mb-4">{section.title}</h3>
|
|
||||||
{section.description && (
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">{section.description}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Fields */}
|
{/* Fields */}
|
||||||
<div className="space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
{sortedFields.map((field) => {
|
{sortedFields.map((field) => {
|
||||||
const IconComponent = FIELD_TYPE_ICONS[field.type] || Type;
|
const IconComponent = FIELD_TYPE_ICONS[field.type] || Type;
|
||||||
|
const options = field.options || [];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={field.id}
|
key={field.id}
|
||||||
className={`p-3 rounded-lg border cursor-pointer transition-all ${
|
className={`p-3 rounded-lg border cursor-pointer transition-all ${selectedField === field.id
|
||||||
selectedField === field.id
|
|
||||||
? 'border-brand-purple bg-brand-lavender/5 ring-2 ring-brand-purple/20'
|
? 'border-brand-purple bg-brand-lavender/5 ring-2 ring-brand-purple/20'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
} ${field.is_fixed ? 'bg-gray-50' : ''}`}
|
} ${field.is_fixed ? 'bg-gray-50' : ''}`}
|
||||||
@@ -688,40 +627,209 @@ const AdminRegistrationBuilder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 text-red-500 hover:text-red-700"
|
className="h-6 w-6 text-red-500 hover:text-background"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteField(field.id);
|
handleDeleteField(section.id, field.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedField === field.id && (
|
||||||
|
<div className="mt-4 border-t pt-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs text-muted-foreground">Field ID</Label>
|
||||||
|
<div className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
||||||
|
{field.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`field-label-${field.id}`}>Label</Label>
|
||||||
|
<Input
|
||||||
|
id={`field-label-${field.id}`}
|
||||||
|
value={field.label}
|
||||||
|
onChange={(e) => handleUpdateField(section.id, field.id, { label: e.target.value })}
|
||||||
|
disabled={field.is_fixed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Type</Label>
|
||||||
|
<Select
|
||||||
|
value={field.type}
|
||||||
|
onValueChange={(value) => handleUpdateField(section.id, field.id, { type: value })}
|
||||||
|
disabled={field.is_fixed}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(FIELD_TYPE_LABELS).map(([type, label]) => (
|
||||||
|
<SelectItem key={type} value={type}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id={`field-required-${field.id}`}
|
||||||
|
checked={field.required}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleUpdateField(section.id, field.id, { required: checked })
|
||||||
|
}
|
||||||
|
disabled={field.is_fixed}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={`field-required-${field.id}`}>Required</Label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Width</Label>
|
||||||
|
<Select
|
||||||
|
value={field.width || 'full'}
|
||||||
|
onValueChange={(value) => handleUpdateField(section.id, field.id, { width: value })}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="full">Full Width</SelectItem>
|
||||||
|
<SelectItem value="half">Half Width</SelectItem>
|
||||||
|
<SelectItem value="third">Third Width</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{['text', 'email', 'phone', 'textarea'].includes(field.type) && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`field-placeholder-${field.id}`}>Placeholder</Label>
|
||||||
|
<Input
|
||||||
|
id={`field-placeholder-${field.id}`}
|
||||||
|
value={field.placeholder || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleUpdateField(section.id, field.id, { placeholder: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{['dropdown', 'radio', 'multiselect'].includes(field.type) && (
|
||||||
|
<div>
|
||||||
|
<Label>Options</Label>
|
||||||
|
<div className="space-y-2 mt-2">
|
||||||
|
{options.map((option, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={option.label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newOptions = [...options];
|
||||||
|
newOptions[idx] = {
|
||||||
|
...newOptions[idx],
|
||||||
|
label: e.target.value,
|
||||||
|
value: e.target.value.toLowerCase().replace(/\s+/g, '_'),
|
||||||
|
};
|
||||||
|
handleUpdateField(section.id, field.id, { options: newOptions });
|
||||||
|
}}
|
||||||
|
placeholder="Option label"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => {
|
||||||
|
const newOptions = options.filter((_, i) => i !== idx);
|
||||||
|
handleUpdateField(section.id, field.id, { options: newOptions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const newOptions = [
|
||||||
|
...options,
|
||||||
|
{ value: `option_${Date.now()}`, label: 'New Option' },
|
||||||
|
];
|
||||||
|
handleUpdateField(section.id, field.id, { options: newOptions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Option
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`field-mapping-${field.id}`}>Database Mapping</Label>
|
||||||
|
<Input
|
||||||
|
id={`field-mapping-${field.id}`}
|
||||||
|
value={field.mapping || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleUpdateField(section.id, field.id, { mapping: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Leave empty for custom data"
|
||||||
|
disabled={field.is_fixed}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Maps to User model field. Empty = stored in custom_registration_data
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{field.is_fixed && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-yellow-800 text-sm">
|
||||||
|
This is a fixed field and cannot be removed or have its core properties changed.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{sortedFields.length === 0 && (
|
{sortedFields.length === 0 && (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-6 text-muted-foreground">
|
||||||
No fields in this section. Click "Add Field" to add one.
|
No fields in this section. Click "Add Field" to add one.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full mt-4"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedSection(section.id);
|
||||||
|
setSelectedField(null);
|
||||||
|
setAddFieldDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="size-4 mr-2" />
|
||||||
|
Add Field
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{sortedSections.length === 0 && (
|
{sortedSections.length === 0 && (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-6 text-muted-foreground">
|
||||||
No sections in this step. Add a section from the left sidebar.
|
No sections yet. Click "Add Section" to get started.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Conditional Rules */}
|
<div className="lg:col-span-3">
|
||||||
<Card className="p-6 mt-6">
|
<Card className="p-6">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex flex-col justify-between items-center mb-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Zap className="h-5 w-5 text-yellow-500" />
|
<Zap className="h-5 w-5 text-yellow-500" />
|
||||||
<h2 className="text-lg font-semibold">Conditional Rules</h2>
|
<h2 className="text-lg font-semibold">Conditional Rules</h2>
|
||||||
@@ -737,185 +845,10 @@ const AdminRegistrationBuilder = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Sidebar - Field Properties */}
|
|
||||||
<div className="lg:col-span-3">
|
|
||||||
<Card className="p-4">
|
|
||||||
<h2 className="text-lg font-semibold mb-4">Field Properties</h2>
|
|
||||||
|
|
||||||
{selectedFieldData ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* Field ID */}
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs text-muted-foreground">Field ID</Label>
|
|
||||||
<div className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
|
||||||
{selectedFieldData.id}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Label */}
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="field-label">Label</Label>
|
|
||||||
<Input
|
|
||||||
id="field-label"
|
|
||||||
value={selectedFieldData.label}
|
|
||||||
onChange={(e) => handleUpdateField(selectedField, { label: e.target.value })}
|
|
||||||
disabled={selectedFieldData.is_fixed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Type */}
|
|
||||||
<div>
|
|
||||||
<Label>Type</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedFieldData.type}
|
|
||||||
onValueChange={(value) => handleUpdateField(selectedField, { type: value })}
|
|
||||||
disabled={selectedFieldData.is_fixed}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.entries(FIELD_TYPE_LABELS).map(([type, label]) => (
|
|
||||||
<SelectItem key={type} value={type}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Required */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id="field-required"
|
|
||||||
checked={selectedFieldData.required}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleUpdateField(selectedField, { required: checked })
|
|
||||||
}
|
|
||||||
disabled={selectedFieldData.is_fixed}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="field-required">Required</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Width */}
|
|
||||||
<div>
|
|
||||||
<Label>Width</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedFieldData.width || 'full'}
|
|
||||||
onValueChange={(value) => handleUpdateField(selectedField, { width: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="full">Full Width</SelectItem>
|
|
||||||
<SelectItem value="half">Half Width</SelectItem>
|
|
||||||
<SelectItem value="third">Third Width</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Placeholder */}
|
|
||||||
{['text', 'email', 'phone', 'textarea'].includes(selectedFieldData.type) && (
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="field-placeholder">Placeholder</Label>
|
|
||||||
<Input
|
|
||||||
id="field-placeholder"
|
|
||||||
value={selectedFieldData.placeholder || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleUpdateField(selectedField, { placeholder: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Options for dropdown/radio/multiselect */}
|
|
||||||
{['dropdown', 'radio', 'multiselect'].includes(selectedFieldData.type) && (
|
|
||||||
<div>
|
|
||||||
<Label>Options</Label>
|
|
||||||
<div className="space-y-2 mt-2">
|
|
||||||
{(selectedFieldData.options || []).map((option, idx) => (
|
|
||||||
<div key={idx} className="flex items-center gap-2">
|
|
||||||
<Input
|
|
||||||
value={option.label}
|
|
||||||
onChange={(e) => {
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => {
|
|
||||||
const newOptions = selectedFieldData.options.filter(
|
|
||||||
(_, i) => i !== idx
|
|
||||||
);
|
|
||||||
handleUpdateField(selectedField, { options: newOptions });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
const newOptions = [
|
|
||||||
...(selectedFieldData.options || []),
|
|
||||||
{ value: `option_${Date.now()}`, label: 'New Option' },
|
|
||||||
];
|
|
||||||
handleUpdateField(selectedField, { options: newOptions });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Add Option
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mapping */}
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="field-mapping">Database Mapping</Label>
|
|
||||||
<Input
|
|
||||||
id="field-mapping"
|
|
||||||
value={selectedFieldData.mapping || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleUpdateField(selectedField, { mapping: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Leave empty for custom data"
|
|
||||||
disabled={selectedFieldData.is_fixed}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Maps to User model field. Empty = stored in custom_registration_data
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedFieldData.is_fixed && (
|
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-yellow-800 text-sm">
|
|
||||||
This is a fixed field and cannot be removed or have its core properties changed.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Select a field to edit its properties
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Add Step Dialog */}
|
{/* Add Step Dialog */}
|
||||||
|
|
||||||
<Dialog open={addStepDialogOpen} onOpenChange={setAddStepDialogOpen}>
|
<Dialog open={addStepDialogOpen} onOpenChange={setAddStepDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -1033,7 +966,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-6 w-6 text-red-500"
|
className="h-6 w-6 text-red-500 hover-text-background"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateSchema((prev) => ({
|
updateSchema((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -1120,8 +1053,7 @@ const AdminRegistrationBuilder = () => {
|
|||||||
.map((field) => (
|
.map((field) => (
|
||||||
<div
|
<div
|
||||||
key={field.id}
|
key={field.id}
|
||||||
className={`${
|
className={`${field.width === 'full'
|
||||||
field.width === 'full'
|
|
||||||
? 'col-span-2'
|
? 'col-span-2'
|
||||||
: field.width === 'third'
|
: field.width === 'third'
|
||||||
? 'col-span-1'
|
? 'col-span-1'
|
||||||
|
|||||||
@@ -354,13 +354,7 @@ const AdminValidations = () => {
|
|||||||
Quick Overview
|
Quick Overview
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||||
<StatCard
|
|
||||||
title="Total Pending"
|
|
||||||
value={loading ? '-' : pendingUsers.length}
|
|
||||||
icon={Users}
|
|
||||||
iconBgClass="text-brand-purple"
|
|
||||||
dataTestId="stat-total-users"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Awaiting Email"
|
title="Awaiting Email"
|
||||||
@@ -394,7 +388,13 @@ const AdminValidations = () => {
|
|||||||
dataTestId="stat-rejected"
|
dataTestId="stat-rejected"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="Total Pending"
|
||||||
|
value={loading ? '-' : pendingUsers.filter(user => ['pending_email', 'pending_validation', 'pre_validated', 'payment_pending',].includes(user.status)).length}
|
||||||
|
icon={Users}
|
||||||
|
iconBgClass="text-brand-purple"
|
||||||
|
dataTestId="stat-total-users"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user