create page working
Mon May 26 2025 15:39:06 GMT+0000 (Coordinated Universal Time)
Saved by @krisha_joshi
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;
Comments