feat: implement Action Builder and Master Data configuration UI components and custom input controls

This commit is contained in:
Syed Waseem
2026-08-21 15:26:31 +05:30
parent 0d2394e5ee
commit f72ef36629
10 changed files with 194 additions and 45 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'}`}>
<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}
@@ -79,9 +79,22 @@ export function CategoriesColumn({
: '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'}`}>
<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}
@@ -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,13 +103,15 @@ 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">
<CustomInput
label="Field Label / Name"
@@ -105,6 +124,9 @@ export function FieldDefinitionFormModal({
fieldName: val,
fieldCode: editingField ? prev.fieldCode : val.toLowerCase().replace(/[^a-z0-9_]+/g, '_'),
}));
if (setError && val.trim()) {
setError(null);
}
}}
placeholder="e.g. Field Name"
/>
@@ -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..."
/>
)}
+15 -3
View File
@@ -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}
/>
@@ -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 */}
+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,