import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { McButton, McInput, McMultiSelect } from '@maersk-global/mds-react-wrapper';
import { McSelect } from '@maersk-global/mds-react-wrapper/components-core/mc-select';
import { McOption } from '@maersk-global/mds-react-wrapper/components-core/mc-option';
import styles from '../styles/CreateRule.module.css';
import data from '../data/PnLGroup.json';
const CreateRules = () => {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('ruleInfo');
const [isLoading, setIsLoading] = useState(false);
const [ruleData, setRuleData] = useState({
num: '',
name: '',
desc: '',
custRefID: '',
ruleGroup: '',
isActive: 'Y',
pnlGroup: '',
});
const [steps, setSteps] = useState([
{
stepNo: '',
stepName: 'Single Step',
StepDesc: '',
stepType: 'S',
preAggregatorColumns: '',
sourceTable: '',
sourceFilters: '',
joinColumns: '',
allocationColumns: '',
driverTableID: '',
driverWeightColumn: '',
driverFilters: '',
},
]);
const [errors, setErrors] = useState({ rule: {}, steps: [{}] });
const pnlGroups = data.PnLGroups ? Object.keys(data.PnLGroups) : [];
const ruleGroups = ruleData.pnlGroup && data.PnLGroups[ruleData.pnlGroup]
? data.PnLGroups[ruleData.pnlGroup].RuleGroups || []
: [];
console.log('pnlGroups:', pnlGroups);
console.log('ruleGroups:', ruleGroups);
const addStep = () => {
setSteps((prevSteps) => [
...prevSteps,
{
stepNo: '',
stepName: 'Single Step',
StepDesc: '',
stepType: 'S',
preAggregatorColumns: '',
sourceTable: '',
sourceFilters: '',
joinColumns: '',
allocationColumns: '',
driverTableID: '',
driverWeightColumn: '',
driverFilters: '',
},
]);
setErrors((prevErrors) => ({
...prevErrors,
steps: [...prevErrors.steps, {}],
}));
};
const removeStep = (index) => {
if (steps.length === 1) {
alert('At least one step is required.');
return;
}
setSteps((prevSteps) => prevSteps.filter((_, i) => i !== index));
setErrors((prevErrors) => ({
...prevErrors,
steps: prevErrors.steps.filter((_, i) => i !== index),
}));
};
const validateForm = () => {
const newErrors = { rule: {}, steps: steps.map(() => ({})) };
let isValid = true;
if (!ruleData.num) {
newErrors.rule.num = 'Rule Number is required';
isValid = false;
} else if (!/^[a-zA-Z0-9]+$/.test(ruleData.num)) {
newErrors.rule.num = 'Rule Number must be alphanumeric';
isValid = false;
}
if (!ruleData.name) {
newErrors.rule.name = 'Rule Name is required';
isValid = false;
}
if (!ruleData.desc) {
newErrors.rule.desc = 'Description is required';
isValid = false;
}
if (!ruleData.custRefID) {
newErrors.rule.custRefID = 'Customer Reference ID is required';
isValid = false;
}
if (!ruleData.pnlGroup) {
newErrors.rule.pnlGroup = 'PnL Group is required';
isValid = false;
}
if (!ruleData.ruleGroup) {
newErrors.rule.ruleGroup = 'Rule Group is required';
isValid = false;
}
if (!ruleData.isActive) {
newErrors.rule.isActive = 'Active status is required';
isValid = false;
}
const stepNumbers = new Set();
steps.forEach((step, index) => {
const stepErrors = {};
if (!step.stepNo) {
stepErrors.stepNo = 'Step Number is required';
isValid = false;
} else if (stepNumbers.has(step.stepNo)) {
stepErrors.stepNo = 'Step Number must be unique';
isValid = false;
} else {
stepNumbers.add(step.stepNo);
}
if (!step.stepName) {
stepErrors.stepName = 'Step Name is required';
isValid = false;
}
if (!step.StepDesc) {
stepErrors.StepDesc = 'Step Description is required';
isValid = false;
}
if (!step.stepType) {
stepErrors.stepType = 'Step Type is required';
isValid = false;
}
if (!step.preAggregatorColumns) {
stepErrors.preAggregatorColumns = 'Pre-Aggregator Columns are required';
isValid = false;
}
if (!step.sourceTable) {
stepErrors.sourceTable = 'Source Table is required';
isValid = false;
}
if (!step.sourceFilters) {
stepErrors.sourceFilters = 'Source Filters are required';
isValid = false;
} else {
try {
parseFilters(step.sourceFilters);
} catch (e) {
stepErrors.sourceFilters = 'Invalid Source Filter format';
isValid = false;
}
}
if (!step.joinColumns) {
stepErrors.joinColumns = 'Join Columns are required';
isValid = false;
}
if (!step.allocationColumns) {
stepErrors.allocationColumns = 'Allocation Columns are required';
isValid = false;
}
if (!step.driverTableID) {
stepErrors.driverTableID = 'Driver Table ID is required';
isValid = false;
}
if (!step.driverWeightColumn) {
stepErrors.driverWeightColumn = 'Driver Weight Column is required';
isValid = false;
}
if (!step.driverFilters) {
stepErrors.driverFilters = 'Driver Filters are required';
isValid = false;
} else {
try {
parseFilters(step.driverFilters);
} catch (e) {
stepErrors.driverFilters = 'Invalid Driver Filter format';
isValid = false;
}
}
newErrors.steps[index] = stepErrors;
});
setErrors(newErrors);
return isValid;
};
const parseColumns = (input) => input.split(',').map((item) => item.trim()).filter((item) => item);
const parseFilters = (input) => {
if (!input) return [];
const filters = input.split(';').map((item) => item.trim()).filter((item) => item);
return filters.map((filter) => {
const parts = filter.split(':').map((item) => item.trim());
if (parts.length !== 3) {
throw new Error('Invalid filter format');
}
const [name, filterType, values] = parts;
if (!name || !filterType || !values) {
throw new Error('Invalid filter format');
}
return { name, filterType, values };
});
};
const handleInputChange = (e, stepIndex = null) => {
const { name, value } = e.target;
console.log(`Input changed: ${name} = ${value}${stepIndex !== null ? ` (Step ${stepIndex + 1})` : ''}`);
if (stepIndex !== null) {
setSteps((prevSteps) => {
const newSteps = [...prevSteps];
newSteps[stepIndex] = { ...newSteps[stepIndex], [name]: value };
return newSteps;
});
setErrors((prevErrors) => ({
...prevErrors,
steps: prevErrors.steps.map((stepErrors, i) =>
i === stepIndex ? { ...stepErrors, [name]: '' } : stepErrors
),
}));
} else {
setRuleData((prevData) => {
const newData = {
...prevData,
[name]: value,
...(name === 'pnlGroup' ? { ruleGroup: '' } : {}),
};
console.log('Updated ruleData:', newData);
return newData;
});
setErrors((prevErrors) => ({
...prevErrors,
rule: { ...prevErrors.rule, [name]: '' },
}));
}
};
const resetForm = () => {
setRuleData({
num: '',
name: '',
desc: '',
custRefID: '',
ruleGroup: '',
isActive: 'Y',
pnlGroup: '',
});
setSteps([
{
stepNo: '',
stepName: 'Single Step',
StepDesc: '',
stepType: 'S',
preAggregatorColumns: '',
sourceTable: '',
sourceFilters: '',
joinColumns: '',
allocationColumns: '',
driverTableID: '',
driverWeightColumn: '',
driverFilters: '',
},
]);
setErrors({ rule: {}, steps: [{}] });
setActiveTab('ruleInfo');
};
const handleSave = async () => {
if (!validateForm()) {
console.log('Validation failed:', JSON.stringify(errors, null, 2));
alert('Please fill out all required fields.');
return;
}
setIsLoading(true);
const ruleJson = {
rules: {
rule: [
{
num: ruleData.num,
name: ruleData.name,
desc: ruleData.desc,
custRefID: ruleData.custRefID,
ruleGroup: ruleData.ruleGroup,
isActive: ruleData.isActive,
Step: steps.map((step, index) => ({
stepNo: step.stepNo || `${ruleData.num}.${index + 1}`,
stepName: step.stepName === 'Single Step' ? 'single' : 'multi',
StepDesc: step.StepDesc,
stepType: step.stepType,
isActive: 'Y',
SourceTable: {
id: '1',
Name: step.sourceTable,
},
sourceFilters: {
columns: parseFilters(step.sourceFilters),
},
preAggregator: {
columns: parseColumns(step.preAggregatorColumns),
},
join: {
columns: parseColumns(step.joinColumns),
},
allocation: {
columns: parseColumns(step.allocationColumns),
},
driver: {
driverTableID: step.driverTableID,
driverWeightColumn: step.driverWeightColumn,
driverFilters: {
columns: parseFilters(step.driverFilters),
},
},
})),
},
],
},
};
console.log('Saving Rule Data:', JSON.stringify(ruleJson, null, 2));
try {
const response = await fetch('/api/rules', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(ruleJson),
});
if (response.ok) {
console.log('Rule created successfully');
alert('Rule created successfully!');
resetForm();
navigate('/');
} else {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
console.error('Failed to create rule:', response.status, errorData);
alert(`Failed to create rule: ${errorData.message || response.statusText}`);
}
} catch (error) {
console.error('Error during API call:', error.message);
alert('An error occurred while saving the rule. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleCancel = () => {
console.log('Cancelling rule creation');
navigate('/');
};
const renderTabContent = () => {
console.log('Rendering tab:', activeTab);
switch (activeTab) {
case 'ruleInfo':
return (
<div className={styles.tabContent}>
{Object.values(errors.rule).some((error) => error) && (
<div className={styles.errorSummary}>
<h4>Please fix the following errors:</h4>
<ul>
{Object.entries(errors.rule).map(([key, error]) => error && (
<li key={key}>{error}</li>
))}
</ul>
</div>
)}
<h3 className={styles.sectionTitle}>Rule Information</h3>
<div className={styles.formGrid}>
<div className={styles.gridItem}>
<McSelect
label="PnL Group"
name="pnlGroup"
value={ruleData.pnlGroup}
input={handleInputChange}
placeholder="Select a PnL Group"
required
invalid={!!errors.rule.pnlGroup}
invalidmessage={errors.rule.pnlGroup}
>
{pnlGroups.map((group) => (
<McOption key={group} value={group}>
{group}
</McOption>
))}
</McSelect>
</div>
<div className={styles.gridItem}>
<McSelect
label="Rule Group"
name="ruleGroup"
value={ruleData.ruleGroup}
input={handleInputChange}
placeholder={ruleGroups.length ? "Select a Rule Group" : "Select a PnL Group first"}
required
disabled={!ruleData.pnlGroup || !ruleGroups.length}
invalid={!!errors.rule.ruleGroup}
invalidmessage={errors.rule.ruleGroup}
>
{ruleGroups.map((group) => (
<McOption key={group} value={group}>
{group}
</McOption>
))}
</McSelect>
</div>
</div>
<div className={styles.inputGroup}>
<McInput
label="Rule Number"
name="num"
value={ruleData.num}
input={handleInputChange}
placeholder="Enter rule number"
required
invalid={!!errors.rule.num}
invalidmessage={errors.rule.num}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Rule Name"
name="name"
value={ruleData.name}
input={handleInputChange}
placeholder="Enter rule name"
required
invalid={!!errors.rule.name}
invalidmessage={errors.rule.name}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Description"
name="desc"
value={ruleData.desc}
input={handleInputChange}
placeholder="Enter rule description"
multiline
rows={3}
required
invalid={!!errors.rule.desc}
invalidmessage={errors.rule.desc}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Customer Reference ID"
name="custRefID"
value={ruleData.custRefID}
input={handleInputChange}
placeholder="Enter customer reference ID"
required
invalid={!!errors.rule.custRefID}
invalidmessage={errors.rule.custRefID}
/>
</div>
</div>
);
case 'step':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Step Information</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Step Number"
name="stepNo"
value={step.stepNo}
input={(e) => handleInputChange(e, index)}
placeholder={`Enter step number (e.g., ${ruleData.num}.${index + 1})`}
required
invalid={!!errors.steps[index].stepNo}
invalidmessage={errors.steps[index].stepNo}
/>
</div>
<div className={styles.inputGroup}>
<McSelect
label="Step Name"
name="stepName"
value={step.stepName}
input={(e) => handleInputChange(e, index)}
required
placeholder="Select Step Name"
invalid={!!errors.steps[index].stepName}
invalidmessage={errors.steps[index].stepName}
>
<McOption value="Single Step">Single Step</McOption>
<McOption value="Multi Step">Multi Step</McOption>
</McSelect>
</div>
<div className={styles.inputGroup}>
<McInput
label="Step Description"
name="StepDesc"
value={step.StepDesc}
input={(e) => handleInputChange(e, index)}
placeholder="Enter step description"
multiline
rows={3}
required
invalid={!!errors.steps[index].StepDesc}
invalidmessage={errors.steps[index].StepDesc}
/>
</div>
<div className={styles.inputGroup}>
<McSelect
label="Step Type"
name="stepType"
value={step.stepType}
input={(e) => handleInputChange(e, index)}
required
placeholder="Select Step Type"
invalid={!!errors.steps[index].stepType}
invalidmessage={errors.steps[index].stepType}
>
<McOption value="S">S</McOption>
<McOption value="M">M</McOption>
</McSelect>
</div>
</div>
))}
<div className={styles.stepButtonContainer}>
<McButton
label="Add Step"
appearance="secondary"
click={addStep}
className={styles.actionButton}
/>
{steps.length > 1 && (
<McButton
label="Remove Step"
appearance="neutral"
click={() => removeStep(steps.length - 1)}
className={styles.actionButton}
/>
)}
</div>
</div>
);
case 'source':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Source Information</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Source Table"
name="sourceTable"
value={step.sourceTable}
input={(e) => handleInputChange(e, index)}
placeholder="Enter source table name"
required
invalid={!!errors.steps[index].sourceTable}
invalidmessage={errors.steps[index].sourceTable}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Source Filters"
name="sourceFilters"
value={step.sourceFilters}
input={(e) => handleInputChange(e, index)}
placeholder="Enter filters (e.g., PNL_LINE:IN:PnL.DVC.214,PnL.DVC.215;MOVE_TYPE:EQ:EX)"
multiline
rows={3}
required
invalid={!!errors.steps[index].sourceFilters}
invalidmessage={errors.steps[index].sourceFilters}
/>
</div>
</div>
))}
</div>
);
case 'preAggregate':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Pre-Aggregate Columns</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Pre-Aggregator Columns"
name="preAggregatorColumns"
value={step.preAggregatorColumns}
input={(e) => handleInputChange(e, index)}
placeholder="Enter columns (comma-separated)"
multiline
rows={3}
required
invalid={!!errors.steps[index].preAggregatorColumns}
invalidmessage={errors.steps[index].preAggregatorColumns}
/>
</div>
</div>
))}
</div>
);
case 'join':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Join Columns</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Join Columns"
name="joinColumns"
value={step.joinColumns}
input={(e) => handleInputChange(e, index)}
placeholder="Enter columns (comma-separated)"
multiline
rows={3}
required
invalid={!!errors.steps[index].joinColumns}
invalidmessage={errors.steps[index].joinColumns}
/>
</div>
</div>
))}
</div>
);
case 'allocation':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Allocation Columns</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Allocation Columns"
name="allocationColumns"
value={step.allocationColumns}
input={(e) => handleInputChange(e, index)}
placeholder="Enter columns (comma-separated)"
multiline
rows={3}
required
invalid={!!errors.steps[index].allocationColumns}
invalidmessage={errors.steps[index].allocationColumns}
/>
</div>
</div>
))}
</div>
);
case 'driver':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Driver Information</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Driver Table ID"
name="driverTableID"
value={step.driverTableID}
input={(e) => handleInputChange(e, index)}
placeholder="Enter driver table ID"
required
invalid={!!errors.steps[index].driverTableID}
invalidmessage={errors.steps[index].driverTableID}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Driver Weight Column"
name="driverWeightColumn"
value={step.driverWeightColumn}
input={(e) => handleInputChange(e, index)}
placeholder="Enter driver weight column"
required
invalid={!!errors.steps[index].driverWeightColumn}
invalidmessage={errors.steps[index].driverWeightColumn}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Driver Filters"
name="driverFilters"
value={step.driverFilters}
input={(e) => handleInputChange(e, index)}
placeholder="Enter filters"
multiline
rows={3}
required
invalid={!!errors.steps[index].driverFilters}
invalidmessage={errors.steps[index].driverFilters}
/>
</div>
</div>
))}
</div>
);
default:
return <div className={styles.tabContent}>No Tab Selected</div>;
}
};
return (
<div className={styles.pageWrapper}>
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.buttonContainer}>
<McButton
label="Back"
appearance="neutral"
click={handleCancel}
className={styles.actionButton}
/>
<McButton
label="Save"
appearance="primary"
click={handleSave}
className={styles.actionButton}
loading={isLoading}
disabled={isLoading}
/>
</div>
<div className={styles.tabs}>
<button
className={`${styles.tabButton} ${activeTab === 'ruleInfo' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('ruleInfo')}
>
Rule Info
</button>
<button
className={`${styles.tabButton} ${activeTab === 'step' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('step')}
>
Step
</button>
<button
className={`${styles.tabButton} ${activeTab === 'source' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('source')}
>
Source
</button>
<button
className={`${styles.tabButton} ${activeTab === 'preAggregate' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('preAggregate')}
>
Pre-Aggregate
</button>
<button
className={`${styles.tabButton} ${activeTab === 'join' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('join')}
>
Join
</button>
<button
className={`${styles.tabButton} ${activeTab === 'allocation' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('allocation')}
>
Allocation
</button>
<button
className={`${styles.tabButton} ${activeTab === 'driver' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('driver')}
>
Driver
</button>
</div>
{renderTabContent()}
</div>
</div>
</div>
);
};
export default CreateRules;
import React, { useState, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { McButton, McInput, McMultiSelect, McSelect } from '@maersk-global/mds-react-wrapper';
import { McOption } from '@maersk-global/mds-react-wrapper/components-core/mc-option';
import styles from '../styles/CreateRule.module.css';
import data from '../data/PnLGroup.json';
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Something went wrong. Please refresh the page.</div>;
}
return this.props.children;
}
}
const CreateRules = () => {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('ruleInfo');
const [isLoading, setIsLoading] = useState(false);
const [ruleData, setRuleData] = useState({
num: '',
name: '',
desc: '',
custRefID: '',
ruleGroup: '',
isActive: 'Y',
pnlGroup: '',
});
const [steps, setSteps] = useState([
{
stepNo: '',
stepName: 'Single Step',
stepDesc: '',
stepType: 'S',
preAggregatorColumns: [],
sourceTableID: '',
sourceFilters: [],
sourceOperator: '',
joinColumns: [],
allocationColumns: [],
driverTableID: '',
driverWeightColumn: '',
driverFilters: [],
driverOperator: '',
},
]);
const [errors, setErrors] = useState({ rule: {}, steps: [{}] });
const pnLGroups = data.PnLGroups && typeof data.PnLGroups === 'object'
? Object.keys(data.PnLGroups)
: [];
const ruleGroups = ruleData.pnlGroup && data.PnLGroups[ruleData.pnlGroup]
? data.PnLGroups[ruleData.pnlGroup].RuleGroups || []
: [];
const sourceFilterOptions = [
{ value: 'Source_Filter_1', label: 'Source Filter 1' },
{ value: 'Source_Filter_2', label: 'Source Filter 2' },
{ value: 'Source_Filter_3', label: 'Source Filter 3' },
];
const preAggregatorOptions = [
{ value: 'column1', label: 'Column 1' },
{ value: 'column2', label: 'Column 2' },
{ value: 'column3', label: 'Column 3' },
];
const joinColumnsOptions = [
{ value: 'join_col1', label: 'Join Column 1' },
{ value: 'join_col2', label: 'Join Column 2' },
{ value: 'join_col3', label: 'Join Column 3' },
];
const allocationColumnsOptions = [
{ value: 'alloc_col1', label: 'Allocation Column 1' },
{ value: 'alloc_col2', label: 'Allocation Column 2' },
{ value: 'alloc_col3', label: 'Allocation Column 3' },
];
const driverFilterOptions = [
{ value: 'Driver_Type_1', label: 'Driver Type: Type 1' },
{ value: 'Driver_Type_2', label: 'Driver Type: Type 2' },
{ value: 'Driver_Status_Active', label: 'Driver Status: Active' },
];
const operatorOptions = useMemo(() => [
{ value: 'IN', label: 'IN' },
{ value: 'NOT IN', label: 'NOT IN' },
{ value: 'EQ', label: 'EQ' },
{ value: 'NTEQ', label: 'NTEQ' },
{ value: 'IS NULL', label: 'IS NULL' },
{ value: 'GT', label: 'GT' },
{ value: 'LT', label: 'LT' },
{ value: 'GTEQ', label: 'GTEQ' },
{ value: 'LTEQ', label: 'LTEQ' },
{ value: 'BETWEEN', label: 'BETWEEN' },
{ value: 'NOT BETWEEN', label: 'NOT BETWEEN' },
{ value: 'LIKE', label: 'LIKE' },
], []);
const addStep = useCallback(() => {
setSteps((prevSteps) => [
...prevSteps,
{
stepNo: '',
stepName: 'Single Step',
stepDesc: '',
stepType: 'S',
preAggregatorColumns: [],
sourceTableID: '',
sourceFilters: [],
sourceOperator: '',
joinColumns: [],
allocationColumns: [],
driverTableID: '',
driverWeightColumn: '',
driverFilters: [],
driverOperator: '',
},
]);
setErrors((prevErrors) => ({
...prevErrors,
steps: [...prevErrors.steps, {}],
}));
}, []);
const removeStep = useCallback((index) => {
if (steps.length === 1) {
alert('At least one step is required.');
return;
}
setSteps((prevSteps) => prevSteps.filter((_, i) => i !== index));
setErrors((prevErrors) => ({
...prevErrors,
steps: prevErrors.steps.filter((_, i) => i !== index),
}));
}, [steps.length]);
const validateForm = useCallback(() => {
try {
const newErrors = { rule: {}, steps: steps.map(() => ({})) };
let isValid = true;
if (!ruleData.num) {
newErrors.rule.num = 'Rule Number is required';
isValid = false;
} else if (!/^[a-zA-Z0-9]+$/.test(ruleData.num)) {
newErrors.rule.num = 'Rule Number must be alphanumeric';
isValid = false;
}
if (!ruleData.name) {
newErrors.rule.name = 'Rule Name is required';
isValid = false;
}
if (!ruleData.desc) {
newErrors.rule.desc = 'Description is required';
isValid = false;
}
if (!ruleData.custRefID) {
newErrors.rule.custRefID = 'Customer Reference ID is required';
isValid = false;
}
if (!ruleData.pnlGroup) {
newErrors.rule.pnlGroup = 'PnL Group is required';
isValid = false;
}
if (!ruleData.ruleGroup) {
newErrors.rule.ruleGroup = 'Rule Group is required';
isValid = false;
}
if (!ruleData.isActive) {
newErrors.rule.isActive = 'Active status is required';
isValid = false;
}
const stepNumbers = new Set();
steps.forEach((step, index) => {
const stepErrors = {};
if (!step.stepNo) {
stepErrors.stepNo = 'Step Number is required';
isValid = false;
} else if (stepNumbers.has(step.stepNo)) {
stepErrors.stepNo = 'Step Number must be unique';
isValid = false;
} else {
stepNumbers.add(step.stepNo);
}
if (!step.stepName) {
stepErrors.stepName = 'Step Name is required';
isValid = false;
}
if (!step.stepDesc) {
stepErrors.stepDesc = 'Step Description is required';
isValid = false;
}
if (!step.stepType) {
stepErrors.stepType = 'Step Type is required';
isValid = false;
}
if (!step.preAggregatorColumns.length) {
stepErrors.preAggregatorColumns = 'Pre-Aggregator Columns are required';
isValid = false;
}
if (!step.sourceTableID) {
stepErrors.sourceTableID = 'Source Table ID is required';
isValid = false;
}
if (!step.sourceFilters.length) {
stepErrors.sourceFilters = 'Source Filters are required';
isValid = false;
}
if (!step.sourceOperator) {
stepErrors.sourceOperator = 'Source Operator is required';
isValid = false;
}
if (!step.joinColumns.length) {
stepErrors.joinColumns = 'Join Columns are required';
isValid = false;
}
if (!step.allocationColumns.length) {
stepErrors.allocationColumns = 'Allocation Columns are required';
isValid = false;
}
if (!step.driverTableID) {
stepErrors.driverTableID = 'Driver Table ID is required';
isValid = false;
}
if (!step.driverWeightColumn) {
stepErrors.driverWeightColumn = 'Driver Weight Column is required';
isValid = false;
}
if (!step.driverFilters.length) {
stepErrors.driverFilters = 'Driver Filters are required';
isValid = false;
}
if (!step.driverOperator) {
stepErrors.driverOperator = 'Driver Operator is required';
isValid = false;
}
newErrors.steps[index] = stepErrors;
});
setErrors(newErrors);
return isValid;
} catch (error) {
alert('An error occurred during form validation. Please try again.');
return false;
}
}, [ruleData, steps]);
const parseColumns = (input) => input;
const parseFilters = (filters, operator) => {
if (!filters.length || !operator) return [];
return filters.map((filter) => ({
name: filter,
filterType: operator,
values: filter,
}));
};
const handleInputChange = useCallback((e, stepIndex = null) => {
const { name, value } = e.target;
if (stepIndex !== null) {
setSteps((prevSteps) => {
const newSteps = [...prevSteps];
newSteps[stepIndex] = { ...newSteps[stepIndex], [name]: value };
return newSteps;
});
setErrors((prevErrors) => ({
...prevErrors,
steps: prevErrors.steps.map((stepErrors, i) =>
i === stepIndex ? { ...stepErrors, [name]: '' } : stepErrors
),
}));
} else {
setRuleData((prevData) => ({
...prevData,
[name]: value,
...(name === 'pnlGroup' ? { ruleGroup: '' } : {}),
}));
setErrors((prevErrors) => ({
...prevErrors,
rule: { ...prevErrors.rule, [name]: '' },
}));
}
}, []);
const handleMultiSelectChange = useCallback((e, stepIndex, fieldName) => {
const selectedValues = e.detail.map((option) => option.value);
setSteps((prevSteps) => {
const newSteps = [...prevSteps];
newSteps[stepIndex] = { ...newSteps[stepIndex], [fieldName]: selectedValues };
return newSteps;
});
setErrors((prevErrors) => ({
...prevErrors,
steps: prevErrors.steps.map((stepErrors, i) =>
i === stepIndex ? { ...stepErrors, [fieldName]: '' } : stepErrors
),
}));
}, []);
const resetForm = useCallback(() => {
setRuleData({
num: '',
name: '',
desc: '',
custRefID: '',
ruleGroup: '',
isActive: 'Y',
pnlGroup: '',
});
setSteps([
{
stepNo: '',
stepName: 'Single Step',
stepDesc: '',
stepType: 'S',
preAggregatorColumns: [],
sourceTableID: '',
sourceFilters: [],
sourceOperator: '',
joinColumns: [],
allocationColumns: [],
driverTableID: '',
driverWeightColumn: '',
driverFilters: [],
driverOperator: '',
},
]);
setErrors({ rule: {}, steps: [{}] });
setActiveTab('ruleInfo');
}, []);
const handleSave = useCallback(async () => {
if (!validateForm()) {
alert('Please fill out all required fields.');
return;
}
setIsLoading(true);
const ruleJson = {
rules: {
rule: [
{
num: ruleData.num,
name: ruleData.name,
desc: ruleData.desc,
custRefID: ruleData.custRefID,
ruleGroup: ruleData.ruleGroup,
isActive: ruleData.isActive,
Step: steps.map((step, index) => ({
stepNo: step.stepNo || `${ruleData.num}.${index + 1}`,
stepName: step.stepName === 'Single Step' ? 'single' : 'multi',
stepDesc: step.stepDesc,
stepType: step.stepType,
isActive: 'Y',
SourceTable: {
id: step.sourceTableID,
Name: step.sourceTableID,
},
sourceFilters: {
columns: parseFilters(step.sourceFilters, step.sourceOperator),
operator: step.sourceOperator,
},
preAggregator: {
columns: parseColumns(step.preAggregatorColumns),
},
join: {
columns: parseColumns(step.joinColumns),
},
allocation: {
columns: parseColumns(step.allocationColumns),
},
driver: {
driverTableID: step.driverTableID,
driverWeightColumn: step.driverWeightColumn,
driverFilters: {
columns: parseFilters(step.driverFilters, step.driverOperator),
operator: step.driverOperator,
},
},
})),
},
],
},
};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch('/api/rules', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(ruleJson),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (response.ok) {
alert('Rule created successfully!');
resetForm();
navigate('/');
} else {
const errorData = await response.json().catch(() => ({ message: response.statusText }));
alert(`Failed to create rule: ${errorData.message || response.statusText}`);
}
} catch (error) {
if (error.name === 'AbortError') {
alert('Request timed out. Please try again.');
} else {
alert('An error occurred while saving the rule. Please try again.');
}
} finally {
setIsLoading(false);
}
}, [validateForm, ruleData, steps, resetForm, navigate]);
const handleCancel = useCallback(() => {
navigate('/');
}, [navigate]);
const renderTabContent = () => {
switch (activeTab) {
case 'ruleInfo':
return (
<div className={styles.tabContent}>
{Object.values(errors.rule).some((error) => error) && (
<div className={styles.errorSummary}>
<h4>Please fix the following errors:</h4>
<ul>
{Object.entries(errors.rule).map(([key, error]) => error && (
<li key={key}>{error}</li>
))}
</ul>
</div>
)}
<h3 className={styles.sectionTitle}>Rule Information</h3>
<div className={styles.formGrid}>
<div className={styles.gridItem}>
<McSelect
label="PnL Group"
name="pnlGroup"
value={ruleData.pnlGroup}
input={handleInputChange}
placeholder="Select a PnL Group"
required
invalid={!!errors.rule.pnlGroup}
invalidmessage={errors.rule.pnlGroup}
>
{pnLGroups.map((group) => (
<McOption key={group} value={group}>
{group}
</McOption>
))}
</McSelect>
</div>
<div className={styles.gridItem}>
<McSelect
label="Rule Group"
name="ruleGroup"
value={ruleData.ruleGroup}
input={handleInputChange}
placeholder={ruleGroups.length ? "Select a Rule Group" : "Select a PnL Group first"}
required
disabled={!ruleData.pnlGroup || !ruleGroups.length}
invalid={!!errors.rule.ruleGroup}
invalidmessage={errors.rule.ruleGroup}
>
{ruleGroups.map((group) => (
<McOption key={group} value={group}>
{group}
</McOption>
))}
</McSelect>
</div>
</div>
<div className={styles.inputGroup}>
<McInput
label="Rule Number"
name="num"
value={ruleData.num}
input={handleInputChange}
placeholder="Enter rule number"
required
invalid={!!errors.rule.num}
invalidmessage={errors.rule.num}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Rule Name"
name="name"
value={ruleData.name}
input={handleInputChange}
placeholder="Enter rule name"
required
invalid={!!errors.rule.name}
invalidmessage={errors.rule.name}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Description"
name="desc"
value={ruleData.desc}
input={handleInputChange}
placeholder="Enter rule description"
multiline
rows={3}
required
invalid={!!errors.rule.desc}
invalidmessage={errors.rule.desc}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Customer Reference ID"
name="custRefID"
value={ruleData.custRefID}
input={handleInputChange}
placeholder="Enter customer reference ID"
required
invalid={!!errors.rule.custRefID}
invalidmessage={errors.rule.custRefID}
/>
</div>
</div>
);
case 'step':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Step Information</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Step Number"
name="stepNo"
value={step.stepNo}
input={(e) => handleInputChange(e, index)}
placeholder={`Enter step number (e.g., ${ruleData.num}.${index + 1})`}
required
invalid={!!errors.steps[index].stepNo}
invalidmessage={errors.steps[index].stepNo}
/>
</div>
<div className={styles.inputGroup}>
<McSelect
label="Step Name"
name="stepName"
value={step.stepName}
input={(e) => handleInputChange(e, index)}
required
placeholder="Select Step Name"
invalid={!!errors.steps[index].stepName}
invalidmessage={errors.steps[index].stepName}
>
<McOption value="Single Step">Single Step</McOption>
<McOption value="Multi Step">Multi Step</McOption>
</McSelect>
</div>
<div className={styles.inputGroup}>
<McInput
label="Step Description"
name="stepDesc"
value={step.stepDesc}
input={(e) => handleInputChange(e, index)}
placeholder="Enter step description"
multiline
rows={3}
required
invalid={!!errors.steps[index].stepDesc}
invalidmessage={errors.steps[index].stepDesc}
/>
</div>
<div className={styles.inputGroup}>
<McSelect
label="Step Type"
name="stepType"
value={step.stepType}
input={(e) => handleInputChange(e, index)}
required
placeholder="Select Step Type"
invalid={!!errors.steps[index].stepType}
invalidmessage={errors.steps[index].stepType}
>
<McOption value="S">S</McOption>
<McOption value="M">M</McOption>
</McSelect>
</div>
</div>
))}
<div className={styles.stepButtonContainer}>
<McButton
label="Add Step"
appearance="secondary"
click={addStep}
className={styles.actionButton}
/>
{steps.length > 1 && (
<McButton
label="Remove Step"
appearance="neutral"
click={() => removeStep(steps.length - 1)}
className={styles.actionButton}
/>
)}
</div>
</div>
);
case 'source':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Source Information</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Source Table ID"
name="sourceTableID"
value={step.sourceTableID}
input={(e) => handleInputChange(e, index)}
placeholder="Enter source table ID"
required
invalid={!!errors.steps[index].sourceTableID}
invalidmessage={errors.steps[index].sourceTableID}
/>
</div>
<div className={styles.inputGroup}>
<McMultiSelect
label="Source Filters"
name="sourceFilters"
value={step.sourceFilters}
optionselected={(e) => handleMultiSelectChange(e, index, 'sourceFilters')}
placeholder="Select source filters"
required
invalid={!!errors.steps[index].sourceFilters}
invalidmessage={errors.steps[index].sourceFilters}
>
{sourceFilterOptions.map((option) => (
<McOption key={option.value} value={option.value}>
{option.label}
</McOption>
))}
</McMultiSelect>
</div>
<div className={styles.inputGroup}>
<McSelect
label="Source Operator"
name="sourceOperator"
value={step.sourceOperator}
input={(e) => handleInputChange(e, index)}
placeholder="Select an operator"
required
invalid={!!errors.steps[index].sourceOperator}
invalidmessage={errors.steps[index].sourceOperator}
>
{operatorOptions.map((option) => (
<McOption key={option.value} value={option.value}>
{option.label}
</McOption>
))}
</McSelect>
</div>
</div>
))}
</div>
);
case 'preAggregate':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Pre-Aggregate Columns</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McMultiSelect
label="Pre-Aggregator Columns"
name="preAggregatorColumns"
value={step.preAggregatorColumns}
optionselected={(e) => handleMultiSelectChange(e, index, 'preAggregatorColumns')}
placeholder="Select pre-aggregator columns"
required
invalid={!!errors.steps[index].preAggregatorColumns}
invalidmessage={errors.steps[index].preAggregatorColumns}
>
{preAggregatorOptions.map((option) => (
<McOption key={option.value} value={option.value}>
{option.label}
</McOption>
))}
</McMultiSelect>
</div>
</div>
))}
</div>
);
case 'join':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Join Columns</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McMultiSelect
label="Join Columns"
name="joinColumns"
value={step.joinColumns}
optionselected={(e) => handleMultiSelectChange(e, index, 'joinColumns')}
placeholder="Select join columns"
required
invalid={!!errors.steps[index].joinColumns}
invalidmessage={errors.steps[index].joinColumns}
>
{joinColumnsOptions.map((option) => (
<McOption key={option.value} value={option.value}>
{option.label}
</McOption>
))}
</McMultiSelect>
</div>
</div>
))}
</div>
);
case 'allocation':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Allocation Columns</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McMultiSelect
label="Allocation Columns"
name="allocationColumns"
value={step.allocationColumns}
optionselected={(e) => handleMultiSelectChange(e, index, 'allocationColumns')}
placeholder="Select allocation columns"
required
invalid={!!errors.steps[index].allocationColumns}
invalidmessage={errors.steps[index].allocationColumns}
>
{allocationColumnsOptions.map((option) => (
<McOption key={option.value} value={option.value}>
{option.label}
</McOption>
))}
</McMultiSelect>
</div>
</div>
))}
</div>
);
case 'driver':
return (
<div className={styles.tabContent}>
<h3 className={styles.sectionTitle}>Driver Information</h3>
{steps.map((step, index) => (
<div key={index} className={styles.stepCase}>
<h4 style={{ color: '#35B0CB' }}>STEP {index + 1}</h4>
<div className={styles.inputGroup}>
<McInput
label="Driver Table ID"
name="driverTableID"
value={step.driverTableID}
input={(e) => handleInputChange(e, index)}
placeholder="Enter driver table ID"
required
invalid={!!errors.steps[index].driverTableID}
invalidmessage={errors.steps[index].driverTableID}
/>
</div>
<div className={styles.inputGroup}>
<McInput
label="Driver Weight Column"
name="driverWeightColumn"
value={step.driverWeightColumn}
input={(e) => handleInputChange(e, index)}
placeholder="Enter driver weight column"
required
invalid={!!errors.steps[index].driverWeightColumn}
invalidmessage={errors.steps[index].driverWeightColumn}
/>
</div>
<div className={styles.inputGroup}>
<McMultiSelect
label="Driver Filters"
name="driverFilters"
value={step.driverFilters}
optionselected={(e) => handleMultiSelectChange(e, index, 'driverFilters')}
placeholder="Select driver filters"
required
invalid={!!errors.steps[index].driverFilters}
invalidmessage={errors.steps[index].driverFilters}
>
{driverFilterOptions.map((option) => (
<McOption key={option.value} value={option.value}>
{option.label}
</McOption>
))}
</McMultiSelect>
</div>
<div className={styles.inputGroup}>
<McSelect
label="Driver Operator"
name="driverOperator"
value={step.driverOperator}
input={(e) => handleInputChange(e, index)}
placeholder="Select an operator"
required
invalid={!!errors.steps[index].driverOperator}
invalidmessage={errors.steps[index].driverOperator}
>
{operatorOptions.map((option) => (
<McOption key={option.value} value={option.value}>
{option.label}
</McOption>
))}
</McSelect>
</div>
</div>
))}
</div>
);
default:
return <div className={styles.tabContent}>No Tab Selected</div>;
}
};
return (
<ErrorBoundary>
<div className={styles.pageWrapper}>
{isLoading && (
<div className={styles.loader}>Loading...</div>
)}
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.buttonContainer}>
<McButton
label="Back"
appearance="neutral"
click={handleCancel}
className={styles.actionButton}
/>
<McButton
label="Save"
appearance="primary"
click={handleSave}
className={styles.actionButton}
loading={isLoading}
disabled={isLoading}
/>
</div>
<div className={styles.tabs}>
<button
className={`${styles.tabButton} ${activeTab === 'ruleInfo' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('ruleInfo')}
>
Rule Info
</button>
<button
className={`${styles.tabButton} ${activeTab === 'step' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('step')}
>
Step
</button>
<button
className={`${styles.tabButton} ${activeTab === 'source' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('source')}
>
Source
</button>
<button
className={`${styles.tabButton} ${activeTab === 'preAggregate' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('preAggregate')}
>
Pre-Aggregate
</button>
<button
className={`${styles.tabButton} ${activeTab === 'join' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('join')}
>
Join
</button>
<button
className={`${styles.tabButton} ${activeTab === 'allocation' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('allocation')}
>
Allocation
</button>
<button
className={`${styles.tabButton} ${activeTab === 'driver' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('driver')}
>
Driver
</button>
</div>
{renderTabContent()}
</div>
</div>
</div>
</ErrorBoundary>
);
};
export default CreateRules;