This commit is contained in:
azeeee05
2026-08-24 11:36:00 +05:30
13 changed files with 630 additions and 358 deletions
@@ -1,4 +1,4 @@
import { XCircleIcon } from '@phosphor-icons/react';
import { useEffect } from 'react';
import {
CustomModal,
@@ -7,6 +7,7 @@ import {
CustomTextArea,
CustomSwitch,
CustomButton,
CustomAlertBanner,
} from '../../../../components/custom';
import type { ActionCategory, ActionType, ActionTypeFormData } from '../ActionBuilderTypes';
@@ -17,6 +18,7 @@ interface ActionTypeFormModalProps {
formData: ActionTypeFormData;
setFormData: React.Dispatch<React.SetStateAction<ActionTypeFormData>>;
error: string | null;
setError?: (err: string | null) => void;
onClose: () => void;
onSubmit: (e: React.FormEvent) => void;
}
@@ -28,9 +30,21 @@ export function ActionTypeFormModal({
formData,
setFormData,
error,
setError,
onClose,
onSubmit,
}: ActionTypeFormModalProps) {
useEffect(() => {
if (!error || !setError) return;
if (error === 'Please select a Category.' && formData.categoryId) {
setError(null);
} else if (error === 'Action Type Name is required.' && formData.name.trim()) {
setError(null);
} else if (error === 'Action Type Code is required.' && formData.code.trim()) {
setError(null);
}
}, [formData.categoryId, formData.name, formData.code, error, setError]);
return (
<CustomModal
isOpen={isOpen}
@@ -40,13 +54,15 @@ export function ActionTypeFormModal({
size="lg"
>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-[#D40000] rounded-[10px] text-[13px] font-medium flex items-center gap-2">
<XCircleIcon size={18} weight="fill" />
{error}
</div>
<CustomAlertBanner
message={error}
type="error"
onClose={() => setError?.(null)}
autoClose={false}
/>
)}
<form onSubmit={onSubmit} className="space-y-5 pt-1">
<form onSubmit={onSubmit} noValidate className="space-y-5 pt-1">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
@@ -55,7 +71,12 @@ export function ActionTypeFormModal({
<CustomDropdown
options={categories.map((c) => ({ label: `${c.name} (${c.code})`, value: c.id }))}
value={formData.categoryId}
onChange={(val) => setFormData((prev) => ({ ...prev, categoryId: val }))}
onChange={(val) => {
setFormData((prev) => ({ ...prev, categoryId: val }));
if (setError && val) {
setError(null);
}
}}
placeholder="Select Category"
/>
</div>
@@ -73,6 +94,9 @@ export function ActionTypeFormModal({
name: nameVal,
code: editingType ? prev.code : nameVal.toLowerCase().replace(/\s+/g, '_'),
}));
if (setError && nameVal.trim()) {
setError(null);
}
}}
placeholder="e.g. Award Miles"
/>
@@ -84,7 +108,13 @@ export function ActionTypeFormModal({
</label>
<CustomInput
value={formData.code}
onChange={(e) => setFormData((prev) => ({ ...prev, code: e.target.value }))}
onChange={(e) => {
const codeVal = e.target.value;
setFormData((prev) => ({ ...prev, code: codeVal }));
if (setError && codeVal.trim()) {
setError(null);
}
}}
placeholder="e.g. award_miles"
/>
</div>
@@ -88,9 +88,23 @@ export function ActionTypesColumn({
: 'bg-slate-50/80 hover:bg-slate-100/70 border border-gray-100 text-slate-800'
}`}
>
<div className="pr-3 space-y-0.5 min-w-0">
<div className={`font-bold text-[14px] truncate ${isSelected ? 'text-white' : 'text-slate-800'}`}>
{t.name}
<div className="pr-3 space-y-0.5 min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={`font-bold text-[14px] truncate ${isSelected ? 'text-white' : 'text-slate-800'}`}>
{t.name}
</span>
<span
title={t.isActive !== false ? 'Active' : 'Inactive'}
className={`w-2 h-2 rounded-full flex-shrink-0 ${
isSelected
? t.isActive !== false
? 'bg-emerald-300 ring-2 ring-white/30'
: 'bg-rose-300 ring-2 ring-rose-400/40'
: t.isActive !== false
? 'bg-[#1E7D5C]'
: 'bg-rose-500'
}`}
/>
</div>
<div className={`font-mono text-[11px] uppercase tracking-wider ${isSelected ? 'text-white/80' : 'text-slate-400'}`}>
{t.code}
@@ -75,13 +75,26 @@ export function CategoriesColumn({
key={cat.id}
onClick={() => onSelectCategory(cat.id)}
className={`group relative p-3.5 rounded-[14px] flex items-center justify-between cursor-pointer transition-all duration-150 ${isSelected
? 'bg-[#1E7D5C] text-white shadow-sm'
: 'bg-slate-50/80 hover:bg-slate-100/70 border border-gray-100 text-slate-800'
? 'bg-[#1E7D5C] text-white shadow-sm'
: 'bg-slate-50/80 hover:bg-slate-100/70 border border-gray-100 text-slate-800'
}`}
>
<div className="pr-3 space-y-0.5 min-w-0">
<div className={`font-bold text-[14px] truncate ${isSelected ? 'text-white' : 'text-slate-800'}`}>
{cat.name}
<div className="pr-3 space-y-1 min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className={`font-bold text-[14px] truncate ${isSelected ? 'text-white' : 'text-slate-800'}`}>
{cat.name}
</span>
<span
title={cat.isActive !== false ? 'Active' : 'Inactive'}
className={`w-2 h-2 rounded-full flex-shrink-0 ${isSelected
? cat.isActive !== false
? 'bg-emerald-300 ring-2 ring-white/30'
: 'bg-rose-300 ring-2 ring-rose-400/40'
: cat.isActive !== false
? 'bg-[#1E7D5C]'
: 'bg-rose-500'
}`}
/>
</div>
<div className={`font-mono text-[11px] uppercase tracking-wider ${isSelected ? 'text-white/80' : 'text-slate-400'}`}>
{cat.code}
@@ -114,8 +127,8 @@ export function CategoriesColumn({
{/* Count Badge */}
<div
className={`w-7 h-7 rounded-full font-bold text-[12px] flex items-center justify-center ${isSelected
? 'bg-white/20 text-white'
: 'bg-[#E8F3EF] text-[#1E7D5C]'
? 'bg-white/20 text-white'
: 'bg-[#E8F3EF] text-[#1E7D5C]'
}`}
>
{childCount}
@@ -1,4 +1,4 @@
import { XCircleIcon } from '@phosphor-icons/react';
import { useEffect } from 'react';
import {
CustomModal,
@@ -6,6 +6,7 @@ import {
CustomTextArea,
CustomSwitch,
CustomButton,
CustomAlertBanner,
} from '../../../../components/custom';
import type { ActionCategory, ActionCategoryFormData } from '../ActionBuilderTypes';
@@ -15,6 +16,7 @@ interface CategoryFormModalProps {
formData: ActionCategoryFormData;
setFormData: React.Dispatch<React.SetStateAction<ActionCategoryFormData>>;
error: string | null;
setError?: (err: string | null) => void;
onClose: () => void;
onSubmit: (e: React.FormEvent) => void;
}
@@ -25,9 +27,19 @@ export function CategoryFormModal({
formData,
setFormData,
error,
setError,
onClose,
onSubmit,
}: CategoryFormModalProps) {
useEffect(() => {
if (!error || !setError) return;
if (error === 'Category Name is required.' && formData.name.trim()) {
setError(null);
} else if (error === 'Category Code is required.' && formData.code.trim()) {
setError(null);
}
}, [formData.name, formData.code, error, setError]);
return (
<CustomModal
isOpen={isOpen}
@@ -37,13 +49,15 @@ export function CategoryFormModal({
size="lg"
>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-[#D40000] rounded-[10px] text-[13px] font-medium flex items-center gap-2">
<XCircleIcon size={18} weight="fill" />
{error}
</div>
<CustomAlertBanner
message={error}
type="error"
onClose={() => setError?.(null)}
autoClose={false}
/>
)}
<form onSubmit={onSubmit} className="space-y-4 pt-1">
<form onSubmit={onSubmit} noValidate className="space-y-4 pt-1">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
@@ -58,6 +72,9 @@ export function CategoryFormModal({
name: nameVal,
code: editingCategory ? prev.code : nameVal.toLowerCase().replace(/\s+/g, '-'),
}));
if (setError && nameVal.trim()) {
setError(null);
}
}}
placeholder="e.g. Refunds"
/>
@@ -69,7 +86,13 @@ export function CategoryFormModal({
</label>
<CustomInput
value={formData.code}
onChange={(e) => setFormData((prev) => ({ ...prev, code: e.target.value }))}
onChange={(e) => {
const codeVal = e.target.value;
setFormData((prev) => ({ ...prev, code: codeVal }));
if (setError && codeVal.trim()) {
setError(null);
}
}}
placeholder="e.g. refunds"
/>
</div>
@@ -1,4 +1,4 @@
import { XCircleIcon } from '@phosphor-icons/react';
import { useEffect } from 'react';
import {
CustomModal,
@@ -7,6 +7,7 @@ import {
CustomTextArea,
CustomSwitch,
CustomButton,
CustomAlertBanner,
} from '../../../../components/custom';
import type {
FieldDefinition,
@@ -52,6 +53,7 @@ interface FieldDefinitionFormModalProps {
formData: FieldDefinitionFormData;
setFormData: React.Dispatch<React.SetStateAction<FieldDefinitionFormData>>;
error: string | null;
setError?: (err: string | null) => void;
onClose: () => void;
onSubmit: (e: React.FormEvent) => void;
}
@@ -64,9 +66,24 @@ export function FieldDefinitionFormModal({
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
@@ -86,187 +103,187 @@ export function FieldDefinitionFormModal({
size="lg"
>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-[#D40000] rounded-[10px] text-[13px] font-medium flex items-center gap-2">
<XCircleIcon size={18} weight="fill" />
{error}
</div>
<CustomAlertBanner
message={error}
type="error"
onClose={() => setError?.(null)}
autoClose={false}
/>
)}
<form onSubmit={onSubmit} className="space-y-4 pt-1">
<form onSubmit={onSubmit} noValidate className="space-y-4 pt-1">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
Field Label / Name <span className="text-red-500">*</span>
</label>
<CustomInput
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, '_'),
}));
}}
placeholder="e.g. Field Name"
/>
</div>
<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"
/>
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
Field Code (JSON Key) <span className="text-red-500">*</span>
</label>
<CustomInput
value={formData.fieldCode}
onChange={(e) => setFormData((prev) => ({ ...prev, fieldCode: e.target.value }))}
placeholder="e.g. field_code"
/>
</div>
<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"
/>
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
Control Type <span className="text-red-500">*</span>
</label>
<CustomDropdown
options={FIELD_TYPE_OPTIONS}
value={formData.fieldType}
onChange={(val) => setFormData((prev) => ({ ...prev, fieldType: val as FieldType }))}
placeholder="Select Control Type..."
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
Master Data Lookup Source
</label>
<CustomDropdown
options={masterLookupOptions}
value={formData.lookupSource || ''}
onChange={(val) => setFormData((prev) => ({ ...prev, lookupSource: val }))}
placeholder="Select Lookup Source..."
/>
</div>
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
Form Width Layout
</label>
<CustomDropdown
options={WIDTH_OPTIONS}
value={formData.width}
onChange={(val) => setFormData((prev) => ({ ...prev, width: val as FieldWidth }))}
/>
</div>
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
Section Group Name
</label>
<CustomInput
value={formData.section || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, section: e.target.value }))}
placeholder="e.g. General Information"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">Placeholder</label>
<CustomInput
value={formData.placeholder || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, placeholder: e.target.value }))}
placeholder="e.g. Enter value..."
/>
</div>
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">Default Value</label>
<CustomInput
value={formData.defaultValue || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, defaultValue: e.target.value }))}
placeholder="e.g. Default"
/>
</div>
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">Display Order</label>
<CustomInput
type="number"
value={String(formData.displayOrder)}
onChange={(e) => setFormData((prev) => ({ ...prev, displayOrder: parseInt(e.target.value) || 1 }))}
/>
</div>
</div>
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">Help Text / Instructions</label>
<CustomTextArea
value={formData.helpText || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, helpText: e.target.value }))}
placeholder="e.g. Instructions for end users filling this field"
rows={2}
<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';
setFormData((prev) => ({
...prev,
fieldType: newType,
lookupSource: isLookupType ? prev.lookupSource : undefined,
}));
if (setError && !isLookupType) {
setError(null);
}
}}
placeholder="Select Control Type..."
/>
</div>
<div
className={`grid grid-cols-1 ${formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select'
? 'md:grid-cols-3'
: 'md:grid-cols-2'
} gap-4`}
>
{(formData.fieldType === 'dropdown' || formData.fieldType === 'multi_select') && (
<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 */}
<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">
<div>
<label className="block text-[12px] font-semibold text-slate-600 mb-1">Min Value / Length</label>
<CustomInput
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="e.g. 312"
/>
</div>
<CustomInput
label="Min Value / Length"
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="e.g. 312"
/>
<div>
<label className="block text-[12px] font-semibold text-slate-600 mb-1">Max Value / Length</label>
<CustomInput
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="e.g. 245"
/>
</div>
<CustomInput
label="Max Value / Length"
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="e.g. 245"
/>
<div>
<label className="block text-[12px] font-semibold text-slate-600 mb-1">Regex Pattern</label>
<CustomInput
value={formData.validationJson?.regex || ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
validationJson: {
...prev.validationJson,
regex: e.target.value,
},
}))
}
placeholder="e.g. ^[A-Z0-9]+$"
/>
</div>
<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>
@@ -274,61 +291,55 @@ export function FieldDefinitionFormModal({
<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">
<div>
<label className="block text-[12px] font-semibold text-slate-600 mb-1">Depends on Field Code</label>
<CustomDropdown
options={dependentOptions}
value={formData.visibilityConditionJson?.field || ''}
onChange={(val) =>
setFormData((prev) => ({
...prev,
visibilityConditionJson: {
...(prev.visibilityConditionJson || { operator: 'equals', value: '' }),
field: val,
},
}))
}
placeholder="Select dependent field..."
/>
</div>
<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..."
/>
<div>
<label className="block text-[12px] font-semibold text-slate-600 mb-1">Operator</label>
<CustomDropdown
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,
},
}))
}
/>
</div>
<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,
},
}))
}
/>
<div>
<label className="block text-[12px] font-semibold text-slate-600 mb-1">Target Value</label>
<CustomInput
value={formData.visibilityConditionJson?.value || ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
visibilityConditionJson: {
...(prev.visibilityConditionJson || { field: '', operator: 'equals' }),
value: e.target.value,
},
}))
}
placeholder="e.g. DGYRHTFUJKYHUSDERTYUIK"
/>
</div>
<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>
@@ -353,6 +364,7 @@ export function FieldDefinitionFormModal({
<div className="flex items-center gap-3">
<CustomButton
variant="outlined"
type="button"
onClick={onClose}
+22 -3
View File
@@ -431,6 +431,13 @@ export default function ActionBuilderPage() {
setFieldError('Field Code is required.');
return;
}
if (
(fieldFormData.fieldType === 'dropdown' || fieldFormData.fieldType === 'multi_select') &&
!fieldFormData.lookupSource?.trim()
) {
setFieldError('Master Data Lookup Source is required for Dropdown or Multi-Select control types.');
return;
}
const payloadData: FieldDefinitionFormData = {
...fieldFormData,
@@ -587,7 +594,11 @@ export default function ActionBuilderPage() {
formData={categoryFormData}
setFormData={setCategoryFormData}
error={categoryError}
onClose={() => setIsCategoryModalOpen(false)}
setError={setCategoryError}
onClose={() => {
setCategoryError(null);
setIsCategoryModalOpen(false);
}}
onSubmit={handleSaveCategory}
/>
@@ -598,7 +609,11 @@ export default function ActionBuilderPage() {
formData={typeFormData}
setFormData={setTypeFormData}
error={typeError}
onClose={() => setIsTypeModalOpen(false)}
setError={setTypeError}
onClose={() => {
setTypeError(null);
setIsTypeModalOpen(false);
}}
onSubmit={handleSaveType}
/>
@@ -610,7 +625,11 @@ export default function ActionBuilderPage() {
formData={fieldFormData}
setFormData={setFieldFormData}
error={fieldError}
onClose={() => setIsFieldModalOpen(false)}
setError={setFieldError}
onClose={() => {
setFieldError(null);
setIsFieldModalOpen(false);
}}
onSubmit={handleSaveField}
/>
@@ -1,10 +1,10 @@
import React from 'react';
import { XCircleIcon } from '@phosphor-icons/react';
import {
CustomModal,
CustomInput,
CustomSwitch,
CustomButton,
CustomAlertBanner,
} from '../../../../components/custom';
import type {
MasterDataCategoryItem,
@@ -21,6 +21,7 @@ interface MasterItemFormModalProps {
setFormData: React.Dispatch<React.SetStateAction<MasterDataValueFormData>>;
onSubmit: (e: React.FormEvent) => void;
errorMsg: string | null;
setErrorMsg?: (err: string | null) => void;
}
export const MasterItemFormModal: React.FC<MasterItemFormModalProps> = ({
@@ -32,7 +33,15 @@ export const MasterItemFormModal: React.FC<MasterItemFormModalProps> = ({
setFormData,
onSubmit,
errorMsg,
setErrorMsg,
}) => {
React.useEffect(() => {
if (!errorMsg || !setErrorMsg) return;
if (errorMsg === 'Value / Name is required.' && formData.value.trim()) {
setErrorMsg(null);
}
}, [formData.value, errorMsg, setErrorMsg]);
return (
<CustomModal
isOpen={isOpen}
@@ -46,13 +55,15 @@ export const MasterItemFormModal: React.FC<MasterItemFormModalProps> = ({
size="md"
>
{errorMsg && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-[#D40000] rounded-[10px] text-[13px] font-medium flex items-center gap-2">
<XCircleIcon size={18} weight="fill" />
{errorMsg}
</div>
<CustomAlertBanner
message={errorMsg}
type="error"
onClose={() => setErrorMsg?.(null)}
autoClose={false}
/>
)}
<form onSubmit={onSubmit} className="space-y-4 pt-1">
<form onSubmit={onSubmit} noValidate className="space-y-4 pt-1">
<div>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
Item Value / Name <span className="text-red-500">*</span>
@@ -66,6 +77,9 @@ export const MasterItemFormModal: React.FC<MasterItemFormModalProps> = ({
value: val,
code: editingItem ? prev.code : val.toLowerCase().trim().replace(/[^a-z0-9_]+/g, '_'),
}));
if (setErrorMsg && val.trim()) {
setErrorMsg(null);
}
}}
placeholder="e.g. Platinum Tier / USD"
/>
@@ -78,7 +92,13 @@ export const MasterItemFormModal: React.FC<MasterItemFormModalProps> = ({
</label>
<CustomInput
value={formData.code || ''}
onChange={(e) => setFormData((prev) => ({ ...prev, code: e.target.value }))}
onChange={(e) => {
const codeVal = e.target.value;
setFormData((prev) => ({ ...prev, code: codeVal }));
if (setErrorMsg && codeVal.trim()) {
setErrorMsg(null);
}
}}
placeholder="e.g. platinum_tier"
/>
</div>
@@ -247,6 +247,7 @@ export default function MasterDataManagement() {
setFormData={setFormData}
onSubmit={handleSubmit}
errorMsg={errorMsg}
setErrorMsg={setErrorMsg}
/>
{/* Delete Confirmation Modal */}
+6
View File
@@ -26,6 +26,12 @@ export interface ActionTypeField {
section?: string;
displayOrder?: number;
isActive: boolean;
validationJson?: any;
visibilityConditionJson?: {
field: string;
operator: 'equals' | 'not_equals' | 'contains';
value: any;
};
}
// ─── Local Direct Api Helpers for Self-Containment ───────────────────────────
@@ -45,8 +45,113 @@ const WIDTH_GRID_MAP: Record<string, string> = {
two_thirds: 'col-span-12 md:col-span-8',
};
function parseVisibilityCondition(visCond: any): { field?: string; operator?: string; value?: any } | null {
if (!visCond) return null;
if (typeof visCond === 'string') {
try {
return JSON.parse(visCond);
} catch {
return null;
}
}
if (typeof visCond === 'object') {
return visCond;
}
return null;
}
function isActionFieldVisible(
field: ActionTypeField,
fieldValues?: Record<string, any>,
allFields?: ActionTypeField[],
fieldLookupOptionsMap?: Record<string, OptionItem[]>
): boolean {
const visCond = parseVisibilityCondition(
field.visibilityConditionJson || (field as any).visibility_condition_json
);
if (!visCond || !visCond.field) return true;
const targetFieldCode = visCond.field;
const operator = visCond.operator || 'equals';
const expectedValue = visCond.value;
const targetField = allFields?.find((f) => f.fieldCode === targetFieldCode);
const rawActual =
fieldValues?.[targetFieldCode] !== undefined && fieldValues?.[targetFieldCode] !== null
? fieldValues[targetFieldCode]
: targetField?.defaultValue;
const actualValue =
rawActual && typeof rawActual === 'object' && 'amount' in rawActual
? rawActual.amount
: rawActual;
const candidateValues: string[] = [];
if (actualValue !== undefined && actualValue !== null) {
if (Array.isArray(actualValue)) {
actualValue.forEach((v) => candidateValues.push(String(v)));
} else {
candidateValues.push(String(actualValue));
}
if (fieldLookupOptionsMap) {
Object.values(fieldLookupOptionsMap).forEach((opts) => {
opts.forEach((opt) => {
if (
candidateValues.some(
(c) =>
c.toLowerCase() === String(opt.value || '').toLowerCase() ||
c.toLowerCase() === String(opt.id || '').toLowerCase() ||
c.toLowerCase() === String(opt.code || '').toLowerCase() ||
c.toLowerCase() === String(opt.label || '').toLowerCase()
)
) {
if (opt.value) candidateValues.push(String(opt.value));
if (opt.code) candidateValues.push(String(opt.code));
if (opt.label) candidateValues.push(String(opt.label));
if (opt.id) candidateValues.push(String(opt.id));
}
});
});
}
}
const normalizeStr = (s: any) =>
String(s ?? '')
.trim()
.toLowerCase()
.replace(/[\s_-]+/g, '');
const normExpected = normalizeStr(expectedValue);
if (operator === 'equals') {
if (typeof expectedValue === 'boolean') {
return Boolean(actualValue) === expectedValue;
}
if (expectedValue === '' || expectedValue === undefined || expectedValue === null) {
return actualValue === '' || actualValue === undefined || actualValue === null;
}
return candidateValues.some((c) => normalizeStr(c) === normExpected);
}
if (operator === 'not_equals') {
if (typeof expectedValue === 'boolean') {
return Boolean(actualValue) !== expectedValue;
}
return !candidateValues.some((c) => normalizeStr(c) === normExpected);
}
if (operator === 'contains') {
if (typeof expectedValue === 'boolean') {
return Boolean(actualValue) === expectedValue;
}
return candidateValues.some(
(c) => normalizeStr(c).includes(normExpected) || normExpected.includes(normalizeStr(c))
);
}
return true;
}
// ─── Types ───────────────────────────────────────────────────────────────────
@@ -634,7 +739,18 @@ export default function AddPolicyEngine() {
const curr = typeof val === 'object' ? val.currency : undefined;
valObj.numberValue = parseFloat(amt) || 0;
if (curr) {
valObj.currencyCodeId = curr;
const lookupOpts =
fieldLookupOptionsMap[fieldDef?.lookupSource || 'currency'] ||
fieldLookupOptionsMap['currency'] ||
[];
const matchedCurr = lookupOpts.find(
(o) => o.value === curr || o.id === curr || o.code === curr
);
valObj.currencyCodeId =
matchedCurr?.id ||
(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(curr))
? String(curr)
: undefined);
}
} else if (fieldType === 'checkbox' || fieldType === 'switch' || typeof val === 'boolean') {
valObj.booleanValue = !!val;
@@ -1255,116 +1371,136 @@ export default function AddPolicyEngine() {
<div className="py-4 flex justify-center">
<CustomLoader label="Loading Configured Dynamic Fields..." />
</div>
) : (actionTypeFieldsMap[action.actionTypeId] || []).length === 0 ? (
<p className="text-xs text-gray-400 italic">No dynamic fields configured for this Action Type.</p>
) : (
<div className="grid grid-cols-12 gap-4">
{(actionTypeFieldsMap[action.actionTypeId] || []).map((field) => {
const widthClass = WIDTH_GRID_MAP[field.width || 'full'] || 'col-span-12';
const fieldVal = action.fieldValues?.[field.fieldCode];
const lookupOpts = fieldLookupOptionsMap[field.lookupSource || ''] || [];
) : (() => {
const allFields = actionTypeFieldsMap[action.actionTypeId] || [];
const visibleFields = allFields.filter((f) =>
isActionFieldVisible(f, action.fieldValues, allFields, fieldLookupOptionsMap)
);
return (
<div key={field.id} className={widthClass}>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
{field.fieldName}
{field.isRequired && <span className="text-red-500 ml-1">*</span>}
</label>
if (allFields.length === 0) {
return <p className="text-xs text-gray-400 italic">No dynamic fields configured for this Action Type.</p>;
}
{field.fieldType === 'textarea' ? (
<CustomTextArea
value={fieldVal || ''}
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)}
placeholder={field.placeholder || 'Enter details...'}
/>
) : field.fieldType === 'currency' ? (
<div className="grid grid-cols-3 gap-2">
<div className="col-span-2">
<CustomInput
type="number"
value={fieldVal?.amount || ''}
onChange={(e) =>
if (visibleFields.length === 0) {
return null;
}
return (
<div className="grid grid-cols-12 gap-4">
{visibleFields.map((field) => {
const widthClass = WIDTH_GRID_MAP[field.width || 'full'] || 'col-span-12';
const fieldVal = action.fieldValues?.[field.fieldCode];
const lookupOpts = fieldLookupOptionsMap[field.lookupSource || ''] || [];
const helpText = field.helpText || (field as any).help_text;
return (
<div key={field.id} className={widthClass}>
<label className="block text-[13px] font-semibold text-slate-700 mb-1.5">
{field.fieldName}
{field.isRequired && <span className="text-red-500 ml-1">*</span>}
</label>
{field.fieldType === 'textarea' ? (
<CustomTextArea
value={fieldVal || ''}
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)}
placeholder={field.placeholder || 'Enter details...'}
/>
) : field.fieldType === 'currency' ? (
<div className="grid grid-cols-3 gap-2">
<div className="col-span-2">
<CustomInput
type="number"
value={fieldVal?.amount || ''}
onChange={(e) =>
handleFieldValueChange(rule.id, action.id, field.fieldCode, {
...(fieldVal || {}),
amount: e.target.value,
})
}
placeholder={field.placeholder || 'Amount'}
/>
</div>
<CustomDropdown
options={lookupOpts}
value={fieldVal?.currency}
onChange={(val) =>
handleFieldValueChange(rule.id, action.id, field.fieldCode, {
...(fieldVal || {}),
amount: e.target.value,
currency: val,
})
}
placeholder={field.placeholder || 'Amount'}
/>
</div>
) : field.fieldType === 'dropdown' ? (
<CustomDropdown
options={lookupOpts}
value={fieldVal?.currency}
onChange={(val) =>
handleFieldValueChange(rule.id, action.id, field.fieldCode, {
...(fieldVal || {}),
currency: val,
})
}
value={fieldVal || ''}
onChange={(val) => handleFieldValueChange(rule.id, action.id, field.fieldCode, val)}
placeholder={field.placeholder || 'Select option...'}
/>
</div>
) : field.fieldType === 'dropdown' ? (
<CustomDropdown
options={lookupOpts}
value={fieldVal || ''}
onChange={(val) => handleFieldValueChange(rule.id, action.id, field.fieldCode, val)}
placeholder={field.placeholder || 'Select option...'}
/>
) : field.fieldType === 'multi_select' ? (
<CustomMultiSelect
options={lookupOpts}
value={Array.isArray(fieldVal) ? fieldVal : []}
onChange={(vals) => handleFieldValueChange(rule.id, action.id, field.fieldCode, vals)}
placeholder={field.placeholder || 'Select multiple options...'}
/>
) : field.fieldType === 'checkbox' ? (
<CustomCheckBox
checked={!!fieldVal}
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.checked)}
label={field.helpText || field.fieldName}
/>
) : field.fieldType === 'switch' ? (
<div className="flex items-center gap-3 pt-1">
<CustomSwitch
) : field.fieldType === 'multi_select' ? (
<CustomMultiSelect
options={lookupOpts}
value={Array.isArray(fieldVal) ? fieldVal : []}
onChange={(vals) => handleFieldValueChange(rule.id, action.id, field.fieldCode, vals)}
placeholder={field.placeholder || 'Select multiple options...'}
/>
) : field.fieldType === 'checkbox' ? (
<CustomCheckBox
checked={!!fieldVal}
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.checked)}
label={field.fieldName}
/>
<span className="text-[13px] font-medium text-slate-700">
{fieldVal ? 'Enabled' : 'Disabled'}
</span>
</div>
) : (
<CustomInput
type={
field.fieldType === 'number' || field.fieldType === 'decimal' || field.fieldType === 'percentage'
? 'number'
: field.fieldType === 'email'
? 'email'
: field.fieldType === 'date'
? 'date'
: field.fieldType === 'time'
? 'time'
: field.fieldType === 'datetime'
? 'datetime-local'
: 'text'
}
value={fieldVal ?? ''}
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)}
placeholder={field.placeholder || 'Enter value...'}
min={
field.fieldType === 'date'
? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]
: field.fieldType === 'datetime'
? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16)
: undefined
}
/>
)}
</div>
);
})}
</div>
)}
) : field.fieldType === 'switch' ? (
<div className="flex items-center gap-3 pt-1">
<CustomSwitch
checked={!!fieldVal}
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.checked)}
/>
<span className="text-[13px] font-medium text-slate-700">
{fieldVal ? 'Enabled' : 'Disabled'}
</span>
</div>
) : (
<CustomInput
type={
field.fieldType === 'number' || field.fieldType === 'decimal' || field.fieldType === 'percentage'
? 'number'
: field.fieldType === 'email'
? 'email'
: field.fieldType === 'date'
? 'date'
: field.fieldType === 'time'
? 'time'
: field.fieldType === 'datetime'
? 'datetime-local'
: 'text'
}
value={fieldVal ?? ''}
onChange={(e) => handleFieldValueChange(rule.id, action.id, field.fieldCode, e.target.value)}
placeholder={field.placeholder || 'Enter value...'}
min={
field.fieldType === 'date'
? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]
: field.fieldType === 'datetime'
? new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16)
: undefined
}
/>
)}
{helpText && (
<p className="text-[11.5px] text-gray-500 mt-1 leading-snug">
{helpText}
</p>
)}
</div>
);
})}
</div>
);
})()}
</div>
)}
</div>
@@ -75,7 +75,6 @@ const SIMULATION_PASSENGER_POOL: Array<Omit<SimulatedPassenger, 'refund' | 'comp
];
const SCENARIO_OPTIONS: Option[] = [
{ label: 'e.g. Passenger Compensation', value: 'e.g. Passenger Compensation' },
{ label: 'Flight Delay Disruption', value: 'Flight Delay Disruption' },
{ label: 'Flight Cancellation', value: 'Flight Cancellation' },
{ label: 'Denied Boarding / Involuntary Bumping', value: 'Denied Boarding' },
@@ -83,7 +82,6 @@ const SCENARIO_OPTIONS: Option[] = [
];
const SCENARIO_SUBTYPE_OPTIONS: Option[] = [
{ label: 'e.g. Passenger Compensation', value: 'e.g. Passenger Compensation' },
{ label: 'Delay > 3 Hours (Long Haul)', value: 'Delay > 3 Hours' },
{ label: 'Delay > 4 Hours (Standard)', value: 'Delay > 4 Hours' },
{ label: 'Short Notice Cancellation (< 14 Days)', value: 'Short Notice Cancellation' },
+1 -1
View File
@@ -22,7 +22,7 @@ const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
type = "text",
leftIcon,
rightIcon,
maxLength,
maxLength = 100,
phonePrefix,
disabled,
className = "",
+1 -1
View File
@@ -16,7 +16,7 @@ const CustomTextArea = forwardRef<HTMLTextAreaElement, CustomTextAreaProps>(
label,
leftIcon,
rightIcon,
maxLength,
maxLength = 500,
disabled,
className = "",
rows = 4,