Files
aeroresolve_frontend/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx
T

411 lines
14 KiB
TypeScript

import { useEffect } from 'react';
import {
CustomModal,
CustomInput,
CustomDropdown,
CustomTextArea,
CustomSwitch,
CustomButton,
CustomAlertBanner,
} from '../../../../components/custom';
import type {
FieldDefinition,
FieldDefinitionFormData,
FieldType,
FieldWidth,
} from '../ActionBuilderTypes';
const FIELD_TYPE_OPTIONS: { label: string; value: FieldType }[] = [
{ label: 'Text Box (Single Line)', value: 'textbox' },
{ label: 'Text Area (Multi Line)', value: 'textarea' },
{ label: 'Email Address', value: 'email' },
{ label: 'Phone Number', value: 'phone' },
{ label: 'URL / Link', value: 'url' },
{ label: 'Number (Integer)', value: 'number' },
{ label: 'Decimal / Price', value: 'decimal' },
{ label: 'Percentage (%)', value: 'percentage' },
{ label: 'Currency (Amount + Currency)', value: 'currency' },
{ label: 'Dropdown Selection', value: 'dropdown' },
{ label: 'Radio Selection', value: 'radio' },
{ label: 'Multi-Select Checkboxes', value: 'multi_select' },
{ label: 'Single Checkbox', value: 'checkbox' },
{ label: 'Toggle Switch', value: 'switch' },
{ label: 'Date Picker', value: 'date' },
{ label: 'Date & Time Picker', value: 'datetime' },
{ label: 'Time Picker', value: 'time' },
{ label: 'Color Picker', value: 'color' },
{ label: 'Formula (Read-only Computed)', value: 'formula' },
];
const WIDTH_OPTIONS: { label: string; value: FieldWidth }[] = [
{ label: 'Full Width (12/12)', value: 'full' },
{ label: 'Half Width (6/12)', value: 'half' },
{ label: 'One-Third Width (4/12)', value: 'third' },
{ label: 'Two-Thirds Width (8/12)', value: 'two_thirds' },
];
interface FieldDefinitionFormModalProps {
isOpen: boolean;
editingField: FieldDefinition | null;
fields: FieldDefinition[];
masterLookupOptions: { label: string; value: string }[];
formData: FieldDefinitionFormData;
setFormData: React.Dispatch<React.SetStateAction<FieldDefinitionFormData>>;
error: string | null;
setError?: (err: string | null) => void;
onClose: () => void;
onSubmit: (e: React.FormEvent) => void;
}
const VALIDATION_RELEVANT_TYPES: FieldType[] = [
'textbox',
'textarea',
'email',
'phone',
'url',
'number',
'decimal',
'percentage',
'currency',
];
export function FieldDefinitionFormModal({
isOpen,
editingField,
fields,
masterLookupOptions,
formData,
setFormData,
error,
setError,
onClose,
onSubmit,
}: FieldDefinitionFormModalProps) {
useEffect(() => {
if (!error || !setError) return;
if (error === 'Field Name is required.' && formData.fieldName.trim()) {
setError(null);
} else if (error === 'Field Code is required.' && formData.fieldCode.trim()) {
setError(null);
} else if (
error === 'Master Data Lookup Source is required for Dropdown or Multi-Select control types.' &&
formData.lookupSource?.trim()
) {
setError(null);
}
}, [formData.fieldName, formData.fieldCode, formData.lookupSource, error, setError]);
const dependentOptions = [
{ label: 'None (No Dependency)', value: '' },
...fields
.filter((f) => !editingField || f.id !== editingField.id)
.map((f) => ({
label: `${f.fieldName} (${f.fieldCode})`,
value: f.fieldCode,
})),
];
const isNumericType = ['number', 'decimal', 'percentage', 'currency'].includes(formData.fieldType);
const minLabel = isNumericType ? 'Min Value' : 'Min Length';
const maxLabel = isNumericType ? 'Max Value' : 'Max Length';
const minPlaceholder = isNumericType ? 'e.g. 0' : 'e.g. 3';
const maxPlaceholder = isNumericType ? 'e.g. 1000' : 'e.g. 250';
return (
<CustomModal
isOpen={isOpen}
onClose={onClose}
title={editingField ? `Edit Field: ${editingField.fieldName}` : 'Create Field Definition'}
description="Configure field code, label, control type, section layout, validation rules, and visibility."
size="lg"
>
{error && (
<CustomAlertBanner
message={error}
type="error"
onClose={() => setError?.(null)}
autoClose={false}
/>
)}
<form onSubmit={onSubmit} noValidate className="space-y-4 pt-1">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<CustomInput
label="Field Label / Name"
required
value={formData.fieldName}
onChange={(e) => {
const val = e.target.value;
setFormData((prev) => ({
...prev,
fieldName: val,
fieldCode: editingField ? prev.fieldCode : val.toLowerCase().replace(/[^a-z0-9_]+/g, '_'),
}));
if (setError && val.trim()) {
setError(null);
}
}}
placeholder="e.g. Field Name"
/>
<CustomInput
label="Field Code (JSON Key)"
required
value={formData.fieldCode}
onChange={(e) => {
const val = e.target.value;
setFormData((prev) => ({ ...prev, fieldCode: val }));
if (setError && val.trim()) {
setError(null);
}
}}
placeholder="e.g. field_code"
/>
<CustomDropdown
label="Control Type"
required
options={FIELD_TYPE_OPTIONS}
value={formData.fieldType}
onChange={(val) => {
const newType = val as FieldType;
const isLookupType = newType === 'dropdown' || newType === 'multi_select';
const isValRelevant = VALIDATION_RELEVANT_TYPES.includes(newType);
setFormData((prev) => ({
...prev,
fieldType: newType,
lookupSource: isLookupType ? prev.lookupSource : undefined,
validationJson: isValRelevant ? prev.validationJson : undefined,
}));
if (setError && !isLookupType) {
setError(null);
}
}}
placeholder="Select Control Type..."
/>
</div>
<div
className={`grid grid-cols-1 ${formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select' || formData.fieldType === 'currency'
? 'md:grid-cols-3'
: 'md:grid-cols-2'
} gap-4`}
>
{(formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select' || formData.fieldType === 'currency') && (
<CustomDropdown
label="Master Data Lookup Source"
required
options={masterLookupOptions}
value={formData.lookupSource || ''}
onChange={(val) => {
setFormData((prev) => ({ ...prev, lookupSource: val }));
if (setError && val?.trim()) {
setError(null);
}
}}
placeholder="Select Lookup Source..."
/>
)}
<CustomDropdown
label="Form Width Layout"
options={WIDTH_OPTIONS}
value={formData.width}
onChange={(val) => setFormData((prev) => ({ ...prev, width: val as FieldWidth }))}
/>
<CustomInput
label="Section Group Name"
value={formData.section || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, section: e.target.value }))}
placeholder="e.g. General Information"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<CustomInput
label="Placeholder"
value={formData.placeholder || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, placeholder: e.target.value }))}
placeholder="e.g. Enter value..."
/>
<CustomInput
label="Default Value"
value={formData.defaultValue || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, defaultValue: e.target.value }))}
placeholder="e.g. Default"
/>
<CustomInput
label="Display Order"
type="number"
value={String(formData.displayOrder)}
onChange={(e) => setFormData((prev) => ({ ...prev, displayOrder: parseInt(e.target.value) || 1 }))}
/>
</div>
<CustomTextArea
label="Help Text / Instructions"
value={formData.helpText || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, helpText: e.target.value }))}
placeholder="e.g. Instructions for end users filling this field"
rows={2}
/>
{/* Validation JSON Rules - Only shown for relevant field types */}
{VALIDATION_RELEVANT_TYPES.includes(formData.fieldType) && (
<div className="p-4 bg-slate-50 border border-slate-200 rounded-[14px] space-y-3">
<h5 className="text-[13px] font-bold text-slate-800">Validation Rules (validation_json)</h5>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<CustomInput
label={minLabel}
type="number"
value={formData.validationJson?.min !== undefined ? String(formData.validationJson.min) : ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
validationJson: {
...prev.validationJson,
min: e.target.value !== '' ? Number(e.target.value) : undefined,
},
}))
}
placeholder={minPlaceholder}
/>
<CustomInput
label={maxLabel}
type="number"
value={formData.validationJson?.max !== undefined ? String(formData.validationJson.max) : ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
validationJson: {
...prev.validationJson,
max: e.target.value !== '' ? Number(e.target.value) : undefined,
},
}))
}
placeholder={maxPlaceholder}
/>
<CustomInput
label="Regex Pattern"
value={formData.validationJson?.regex || ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
validationJson: {
...prev.validationJson,
regex: e.target.value,
},
}))
}
placeholder="e.g. ^[A-Z0-9]+$"
/>
</div>
</div>
)}
{/* Conditional Visibility */}
<div className="p-4 bg-amber-50/60 border border-amber-200 rounded-[14px] space-y-3">
<h5 className="text-[13px] font-bold text-amber-900">Conditional Visibility (visibility_condition_json)</h5>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<CustomDropdown
label="Depends on Field Code"
options={dependentOptions}
value={formData.visibilityConditionJson?.field || ''}
onChange={(val) =>
setFormData((prev) => ({
...prev,
visibilityConditionJson: {
...(prev.visibilityConditionJson || { operator: 'equals', value: '' }),
field: val,
},
}))
}
placeholder="Select dependent field..."
/>
<CustomDropdown
label="Operator"
options={[
{ label: 'Equals', value: 'equals' },
{ label: 'Not Equals', value: 'not_equals' },
{ label: 'Contains', value: 'contains' },
]}
value={formData.visibilityConditionJson?.operator || 'equals'}
onChange={(val) =>
setFormData((prev) => ({
...prev,
visibilityConditionJson: {
...(prev.visibilityConditionJson || { field: '', value: '' }),
operator: val as any,
},
}))
}
/>
<CustomInput
label="Target Value"
value={formData.visibilityConditionJson?.value || ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
visibilityConditionJson: {
...(prev.visibilityConditionJson || { field: '', operator: 'equals' }),
value: e.target.value,
},
}))
}
placeholder="e.g. DGYRHTFUJKYHUSDERTYUIK"
/>
</div>
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-100 mt-6">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-slate-700">Required Field</span>
<CustomSwitch
checked={formData.isRequired}
onChange={(e) => setFormData((prev) => ({ ...prev, isRequired: e.target.checked }))}
/>
</div>
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-slate-700">Active Status</span>
<CustomSwitch
checked={formData.isActive}
onChange={(e) => setFormData((prev) => ({ ...prev, isActive: e.target.checked }))}
/>
</div>
</div>
<div className="flex items-center gap-3">
<CustomButton
variant="outlined"
type="button"
onClick={onClose}
className="!border-gray-300 !text-gray-600 font-semibold px-5"
>
Cancel
</CustomButton>
<CustomButton
variant="primary"
type="submit"
className="!bg-[#1E7D5C] hover:!bg-[#17664B] font-semibold px-6 shadow-sm"
>
{editingField ? 'Save Changes' : 'Create Field Definition'}
</CustomButton>
</div>
</div>
</form>
</CustomModal>
);
}