feat: Add field validation and UI configuration management, and enable editing of field assignments.

This commit is contained in:
pradeepkumar
2026-02-24 04:05:47 +05:30
parent a264c24f26
commit c352d8ff64

View File

@@ -15,12 +15,14 @@ import {
FieldType, FieldType,
FieldOption, FieldOption,
RangeConfig, RangeConfig,
FieldValidation,
FieldUiConfig,
FIELD_TYPES, FIELD_TYPES,
getErrorMessage, getErrorMessage,
} from '@/services'; } from '@/services';
type FieldModalType = 'create' | 'edit' | 'delete' | null; type FieldModalType = 'create' | 'edit' | 'delete' | null;
type AssignModalType = 'assign' | null; type AssignModalType = 'assign' | 'editAssignment' | null;
const REQUIRES_OPTIONS: FieldType[] = ['SELECT', 'RADIO', 'MULTI_SELECT', 'CHECKBOX_GROUP']; const REQUIRES_OPTIONS: FieldType[] = ['SELECT', 'RADIO', 'MULTI_SELECT', 'CHECKBOX_GROUP'];
@@ -58,6 +60,8 @@ export default function SectionDetailPage() {
isSearchableOnly: false, isSearchableOnly: false,
options: [], options: [],
rangeConfig: undefined, rangeConfig: undefined,
validation: undefined,
uiConfig: undefined,
}); });
// Options editor state // Options editor state
@@ -114,6 +118,8 @@ export default function SectionDetailPage() {
isSearchableOnly: false, isSearchableOnly: false,
options: [], options: [],
rangeConfig: undefined, rangeConfig: undefined,
validation: undefined,
uiConfig: undefined,
}); });
setOptionInput({ value: '', label: '' }); setOptionInput({ value: '', label: '' });
setModalError(''); setModalError('');
@@ -135,6 +141,8 @@ export default function SectionDetailPage() {
isSearchableOnly: field.isSearchableOnly, isSearchableOnly: field.isSearchableOnly,
options: field.options || [], options: field.options || [],
rangeConfig: field.rangeConfig || undefined, rangeConfig: field.rangeConfig || undefined,
validation: field.validation || undefined,
uiConfig: field.uiConfig || undefined,
}); });
setOptionInput({ value: '', label: '' }); setOptionInput({ value: '', label: '' });
setModalError(''); setModalError('');
@@ -217,6 +225,30 @@ export default function SectionDetailPage() {
}); });
}; };
// Validation management
const updateValidation = (key: keyof FieldValidation, value: number | string) => {
const current = fieldForm.validation || {};
const parsed = typeof value === 'string' && value === '' ? undefined : typeof value === 'string' ? (isNaN(Number(value)) ? value : Number(value)) : value;
const updated = { ...current, [key]: parsed };
// Remove undefined keys
Object.keys(updated).forEach((k) => {
if (updated[k as keyof FieldValidation] === undefined || updated[k as keyof FieldValidation] === '') {
delete updated[k as keyof FieldValidation];
}
});
setFieldForm({ ...fieldForm, validation: Object.keys(updated).length > 0 ? updated : undefined });
};
// UI Config management
const updateUiConfig = (key: keyof FieldUiConfig, value: number | string | boolean) => {
const current = fieldForm.uiConfig || {};
const updated = { ...current, [key]: value };
if (value === '' || value === undefined) {
delete updated[key as keyof FieldUiConfig];
}
setFieldForm({ ...fieldForm, uiConfig: Object.keys(updated).length > 0 ? updated : undefined });
};
// Range config management // Range config management
const updateRangeConfig = (key: keyof RangeConfig, value: number | string) => { const updateRangeConfig = (key: keyof RangeConfig, value: number | string) => {
setFieldForm({ setFieldForm({
@@ -257,6 +289,34 @@ export default function SectionDetailPage() {
setModalError(''); setModalError('');
}; };
const openEditAssignmentModal = (ats: { agentTypeId: string; sortOrder: number; isRequired: boolean }) => {
setAssignForm({
agentTypeId: ats.agentTypeId,
sortOrder: ats.sortOrder,
isRequired: ats.isRequired,
});
setModalError('');
setAssignModalType('editAssignment');
};
const handleUpdateAssignment = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setModalError('');
try {
await profileSectionsService.updateAssignment(sectionId, assignForm.agentTypeId, {
sortOrder: assignForm.sortOrder,
isRequired: assignForm.isRequired,
});
closeAssignModal();
fetchData();
} catch (err) {
setModalError(getErrorMessage(err));
} finally {
setIsSubmitting(false);
}
};
const handleAssign = async (e: React.FormEvent) => { const handleAssign = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
@@ -533,16 +593,27 @@ export default function SectionDetailPage() {
{ats.isRequired ? 'Required' : 'Optional'} • Order: {ats.sortOrder} {ats.isRequired ? 'Required' : 'Optional'} • Order: {ats.sortOrder}
</p> </p>
</div> </div>
<div className="flex items-center space-x-1">
<button
onClick={() => openEditAssignmentModal(ats)}
className="p-1 text-[#9ca3af] hover:text-[#f5a623]"
title="Edit assignment"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button <button
onClick={() => handleRemoveAssignment(ats.agentTypeId)} onClick={() => handleRemoveAssignment(ats.agentTypeId)}
className="p-1 text-[#9ca3af] hover:text-red-600" className="p-1 text-[#9ca3af] hover:text-red-600"
title="Remove assignment" title="Remove assignment"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
</div> </div>
</div>
); );
})} })}
</div> </div>
@@ -826,6 +897,120 @@ export default function SectionDetailPage() {
</div> </div>
</div> </div>
)} )}
{/* Validation Rules */}
<div className="border border-[#e5e7eb] rounded-xl p-4">
<label className="block text-sm font-medium text-[#00293d] mb-3">
Validation Rules
</label>
<div className="grid grid-cols-2 gap-3">
{['TEXT', 'TEXTAREA', 'TAG_INPUT'].includes(fieldForm.fieldType) && (
<>
<div>
<label className="block text-xs text-[#666666] mb-1">Min Length</label>
<input
type="number"
value={fieldForm.validation?.minLength ?? ''}
onChange={(e) => updateValidation('minLength', e.target.value)}
placeholder="No min"
min={0}
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
/>
</div>
<div>
<label className="block text-xs text-[#666666] mb-1">Max Length</label>
<input
type="number"
value={fieldForm.validation?.maxLength ?? ''}
onChange={(e) => updateValidation('maxLength', e.target.value)}
placeholder="No max"
min={0}
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
/>
</div>
</>
)}
{['NUMBER', 'RANGE'].includes(fieldForm.fieldType) && (
<>
<div>
<label className="block text-xs text-[#666666] mb-1">Min Value</label>
<input
type="number"
value={fieldForm.validation?.min ?? ''}
onChange={(e) => updateValidation('min', e.target.value)}
placeholder="No min"
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
/>
</div>
<div>
<label className="block text-xs text-[#666666] mb-1">Max Value</label>
<input
type="number"
value={fieldForm.validation?.max ?? ''}
onChange={(e) => updateValidation('max', e.target.value)}
placeholder="No max"
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
/>
</div>
</>
)}
<div className="col-span-2">
<label className="block text-xs text-[#666666] mb-1">Pattern (regex)</label>
<input
type="text"
value={fieldForm.validation?.pattern ?? ''}
onChange={(e) => updateValidation('pattern', e.target.value)}
placeholder="e.g., ^[a-zA-Z]+$"
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
/>
</div>
</div>
</div>
{/* UI Configuration */}
<div className="border border-[#e5e7eb] rounded-xl p-4">
<label className="block text-sm font-medium text-[#00293d] mb-3">
UI Configuration
</label>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-[#666666] mb-1">Columns (grid layout)</label>
<input
type="number"
value={fieldForm.uiConfig?.columns ?? ''}
onChange={(e) => updateUiConfig('columns', e.target.value ? Number(e.target.value) : '')}
placeholder="Auto"
min={1}
max={4}
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
/>
</div>
<div className="flex items-center pt-5">
<input
type="checkbox"
id="showInPreview"
checked={fieldForm.uiConfig?.showInPreview ?? false}
onChange={(e) => updateUiConfig('showInPreview', e.target.checked)}
className="rounded border-[#e5e7eb] text-[#f5a623] focus:ring-0"
/>
<label htmlFor="showInPreview" className="ml-2 text-sm text-[#00293d] font-serif">
Show in Preview
</label>
</div>
</div>
<div>
<label className="block text-xs text-[#666666] mb-1">Help Text</label>
<input
type="text"
value={fieldForm.uiConfig?.helpText ?? ''}
onChange={(e) => updateUiConfig('helpText', e.target.value)}
placeholder="Additional help text shown below the field"
className="w-full px-3 py-2 border border-[#e5e7eb] rounded-xl focus:outline-none focus:border-[#f5a623] text-sm text-[#00293d] bg-white placeholder:text-[#666666]"
/>
</div>
</div>
</div>
</div> </div>
<div className="px-6 py-4 border-t border-[#e5e7eb] flex justify-end space-x-3"> <div className="px-6 py-4 border-t border-[#e5e7eb] flex justify-end space-x-3">
<button <button
@@ -885,19 +1070,22 @@ export default function SectionDetailPage() {
)} )}
{/* Assign to Agent Type Modal */} {/* Assign to Agent Type Modal */}
{assignModalType === 'assign' && ( {(assignModalType === 'assign' || assignModalType === 'editAssignment') && (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-sm border border-[#e5e7eb]-xl max-w-md w-full mx-4"> <div className="bg-white rounded-xl shadow-sm border border-[#e5e7eb]-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-[#e5e7eb]"> <div className="px-6 py-4 border-b border-[#e5e7eb]">
<h3 className="text-lg font-semibold text-[#00293d] font-fractul">Assign to Agent Type</h3> <h3 className="text-lg font-semibold text-[#00293d] font-fractul">
{assignModalType === 'editAssignment' ? 'Edit Assignment' : 'Assign to Agent Type'}
</h3>
</div> </div>
<form onSubmit={handleAssign}> <form onSubmit={assignModalType === 'editAssignment' ? handleUpdateAssignment : handleAssign}>
<div className="px-6 py-4 space-y-4"> <div className="px-6 py-4 space-y-4">
{modalError && ( {modalError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg"> <div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-700 text-sm">{modalError}</p> <p className="text-red-700 text-sm">{modalError}</p>
</div> </div>
)} )}
{assignModalType === 'assign' ? (
<div> <div>
<label className="block text-sm font-medium text-[#00293d] mb-1"> <label className="block text-sm font-medium text-[#00293d] mb-1">
Agent Type <span className="text-red-500">*</span> Agent Type <span className="text-red-500">*</span>
@@ -916,6 +1104,14 @@ export default function SectionDetailPage() {
))} ))}
</select> </select>
</div> </div>
) : (
<div>
<label className="block text-sm font-medium text-[#00293d] mb-1">Agent Type</label>
<p className="px-3 py-2 bg-[#f5f9f8] rounded-xl text-[#00293d] text-sm">
{agentTypes.find((at) => at.id === assignForm.agentTypeId)?.name || 'Unknown'}
</p>
</div>
)}
<div> <div>
<label className="block text-sm font-medium text-[#00293d] mb-1"> <label className="block text-sm font-medium text-[#00293d] mb-1">
Sort Order Sort Order
@@ -953,10 +1149,10 @@ export default function SectionDetailPage() {
</button> </button>
<button <button
type="submit" type="submit"
disabled={isSubmitting || !assignForm.agentTypeId} disabled={isSubmitting || (assignModalType === 'assign' && !assignForm.agentTypeId)}
className="px-4 py-2 bg-[#f5a623] text-white rounded-xl hover:bg-[#e09620] transition-colors disabled:opacity-50" className="px-4 py-2 bg-[#f5a623] text-white rounded-xl hover:bg-[#e09620] transition-colors disabled:opacity-50"
> >
{isSubmitting ? 'Assigning...' : 'Assign'} {isSubmitting ? 'Saving...' : assignModalType === 'editAssignment' ? 'Save Changes' : 'Assign'}
</button> </button>
</div> </div>
</form> </form>