From f72ef36629167f64926ed784b954f180b8cae2e2 Mon Sep 17 00:00:00 2001 From: Syed Waseem Date: Fri, 21 Aug 2026 15:26:31 +0530 Subject: [PATCH] feat: implement Action Builder and Master Data configuration UI components and custom input controls --- .../components/ActionTypeFormModal.tsx | 46 +++++++++++++--- .../components/ActionTypesColumn.tsx | 20 +++++-- .../components/CategoriesColumn.tsx | 27 +++++++--- .../components/CategoryFormModal.tsx | 37 ++++++++++--- .../components/FieldDefinitionFormModal.tsx | 52 ++++++++++++++++--- src/app/configuration/actionBuilder/index.tsx | 18 +++++-- .../components/MasterItemFormModal.tsx | 34 +++++++++--- src/app/configuration/masterData/index.tsx | 1 + src/components/custom/CustomInput.tsx | 2 +- src/components/custom/CustomTextArea.tsx | 2 +- 10 files changed, 194 insertions(+), 45 deletions(-) diff --git a/src/app/configuration/actionBuilder/components/ActionTypeFormModal.tsx b/src/app/configuration/actionBuilder/components/ActionTypeFormModal.tsx index 2541f4a..a62285f 100644 --- a/src/app/configuration/actionBuilder/components/ActionTypeFormModal.tsx +++ b/src/app/configuration/actionBuilder/components/ActionTypeFormModal.tsx @@ -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>; 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 ( {error && ( -
- - {error} -
+ setError?.(null)} + autoClose={false} + /> )} -
+
@@ -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({ 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" />
diff --git a/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx b/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx index 88b06d0..6ee8066 100644 --- a/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx +++ b/src/app/configuration/actionBuilder/components/ActionTypesColumn.tsx @@ -88,9 +88,23 @@ export function ActionTypesColumn({ : 'bg-slate-50/80 hover:bg-slate-100/70 border border-gray-100 text-slate-800' }`} > -
-
- {t.name} +
+
+ + {t.name} + +
{t.code} diff --git a/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx b/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx index 0ea9b72..b850684 100644 --- a/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx +++ b/src/app/configuration/actionBuilder/components/CategoriesColumn.tsx @@ -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' }`} > -
-
- {cat.name} +
+
+ + {cat.name} + +
{cat.code} @@ -114,8 +127,8 @@ export function CategoriesColumn({ {/* Count Badge */}
{childCount} diff --git a/src/app/configuration/actionBuilder/components/CategoryFormModal.tsx b/src/app/configuration/actionBuilder/components/CategoryFormModal.tsx index 5bebd02..fc8c900 100644 --- a/src/app/configuration/actionBuilder/components/CategoryFormModal.tsx +++ b/src/app/configuration/actionBuilder/components/CategoryFormModal.tsx @@ -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>; 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 ( {error && ( -
- - {error} -
+ setError?.(null)} + autoClose={false} + /> )} - +
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" />
diff --git a/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx b/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx index 37c87e0..2b57fd7 100644 --- a/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx +++ b/src/app/configuration/actionBuilder/components/FieldDefinitionFormModal.tsx @@ -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>; 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,13 +103,15 @@ export function FieldDefinitionFormModal({ size="lg" > {error && ( -
- - {error} -
+ setError?.(null)} + autoClose={false} + /> )} - +
@@ -113,7 +135,13 @@ export function FieldDefinitionFormModal({ label="Field Code (JSON Key)" required value={formData.fieldCode} - onChange={(e) => setFormData((prev) => ({ ...prev, fieldCode: e.target.value }))} + onChange={(e) => { + const val = e.target.value; + setFormData((prev) => ({ ...prev, fieldCode: val })); + if (setError && val.trim()) { + setError(null); + } + }} placeholder="e.g. field_code" /> @@ -130,6 +158,9 @@ export function FieldDefinitionFormModal({ fieldType: newType, lookupSource: isLookupType ? prev.lookupSource : undefined, })); + if (setError && !isLookupType) { + setError(null); + } }} placeholder="Select Control Type..." /> @@ -147,7 +178,12 @@ export function FieldDefinitionFormModal({ required options={masterLookupOptions} value={formData.lookupSource || ''} - onChange={(val) => setFormData((prev) => ({ ...prev, lookupSource: val }))} + onChange={(val) => { + setFormData((prev) => ({ ...prev, lookupSource: val })); + if (setError && val?.trim()) { + setError(null); + } + }} placeholder="Select Lookup Source..." /> )} diff --git a/src/app/configuration/actionBuilder/index.tsx b/src/app/configuration/actionBuilder/index.tsx index b374602..3b459b5 100644 --- a/src/app/configuration/actionBuilder/index.tsx +++ b/src/app/configuration/actionBuilder/index.tsx @@ -594,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} /> @@ -605,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} /> @@ -617,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} /> diff --git a/src/app/configuration/masterData/components/MasterItemFormModal.tsx b/src/app/configuration/masterData/components/MasterItemFormModal.tsx index 3d7c5ec..c77104c 100644 --- a/src/app/configuration/masterData/components/MasterItemFormModal.tsx +++ b/src/app/configuration/masterData/components/MasterItemFormModal.tsx @@ -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>; onSubmit: (e: React.FormEvent) => void; errorMsg: string | null; + setErrorMsg?: (err: string | null) => void; } export const MasterItemFormModal: React.FC = ({ @@ -32,7 +33,15 @@ export const MasterItemFormModal: React.FC = ({ 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 ( = ({ size="md" > {errorMsg && ( -
- - {errorMsg} -
+ setErrorMsg?.(null)} + autoClose={false} + /> )} - +
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" />
diff --git a/src/app/configuration/masterData/index.tsx b/src/app/configuration/masterData/index.tsx index f7f70db..965cb9a 100644 --- a/src/app/configuration/masterData/index.tsx +++ b/src/app/configuration/masterData/index.tsx @@ -247,6 +247,7 @@ export default function MasterDataManagement() { setFormData={setFormData} onSubmit={handleSubmit} errorMsg={errorMsg} + setErrorMsg={setErrorMsg} /> {/* Delete Confirmation Modal */} diff --git a/src/components/custom/CustomInput.tsx b/src/components/custom/CustomInput.tsx index 80c0ed6..4861bfe 100644 --- a/src/components/custom/CustomInput.tsx +++ b/src/components/custom/CustomInput.tsx @@ -22,7 +22,7 @@ const CustomInput = forwardRef( type = "text", leftIcon, rightIcon, - maxLength, + maxLength = 100, phonePrefix, disabled, className = "", diff --git a/src/components/custom/CustomTextArea.tsx b/src/components/custom/CustomTextArea.tsx index c69a4d7..8a8b274 100644 --- a/src/components/custom/CustomTextArea.tsx +++ b/src/components/custom/CustomTextArea.tsx @@ -16,7 +16,7 @@ const CustomTextArea = forwardRef( label, leftIcon, rightIcon, - maxLength, + maxLength = 500, disabled, className = "", rows = 4,