diff --git a/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx index 9c9c86f..5f1e41d 100644 --- a/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx +++ b/src/dealer-claim/components/request-creation/ClaimApproverSelectionStep.tsx @@ -70,7 +70,7 @@ export function ClaimApproverSelectionStep({ onPolicyViolation, }: ClaimApproverSelectionStepProps) { const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); - + // State for add approver modal const [showAddApproverModal, setShowAddApproverModal] = useState(false); const [addApproverEmail, setAddApproverEmail] = useState(''); @@ -96,7 +96,7 @@ export function ClaimApproverSelectionStep({ // For manual steps (3 and 8), check if approver is assigned, verified, and has TAT const approver = approvers.find((a: ClaimApprover) => a.level === step.level); - + if (!approver || !approver.email || !approver.userId || !approver.tat) { missingSteps.push(`${step.name}`); } @@ -120,20 +120,20 @@ export function ClaimApproverSelectionStep({ // Initialize approvers array for all 8 steps useEffect(() => { const currentApprovers = formData.approvers || []; - + // If we already have approvers (including additional ones), don't reinitialize // This prevents creating duplicates when approvers have been shifted if (currentApprovers.length > 0) { // Just ensure all fixed steps have their approvers, but don't recreate shifted ones const newApprovers: ClaimApprover[] = []; const additionalApprovers = currentApprovers.filter((a: ClaimApprover) => a.isAdditional); - + CLAIM_STEPS.forEach((step) => { // Find existing approver by originalStepLevel (handles shifted levels) - const existing = currentApprovers.find((a: ClaimApprover) => + const existing = currentApprovers.find((a: ClaimApprover) => a.originalStepLevel === step.level || (!a.originalStepLevel && !a.isAdditional && a.level === step.level) ); - + if (existing) { // Use existing approver (preserves shifted level) newApprovers.push(existing); @@ -182,19 +182,19 @@ export function ClaimApproverSelectionStep({ } } }); - + // Add back all additional approvers additionalApprovers.forEach((addApprover: ClaimApprover) => { newApprovers.push(addApprover); }); - + // Sort by level newApprovers.sort((a, b) => a.level - b.level); - + // Only update if there are actual changes (to avoid infinite loops) - const hasChanges = JSON.stringify(currentApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))) !== - JSON.stringify(newApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))); - + const hasChanges = JSON.stringify(currentApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))) !== + JSON.stringify(newApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel }))); + if (hasChanges) { updateFormData('approvers', newApprovers); } @@ -246,10 +246,10 @@ export function ClaimApproverSelectionStep({ const handleApproverEmailChange = (level: number, value: string) => { const approvers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility - const index = approvers.findIndex((a: ClaimApprover) => + const index = approvers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); - + if (index === -1) { // Create new approver entry const step = CLAIM_STEPS.find(s => s.level === level); @@ -304,8 +304,8 @@ export function ClaimApproverSelectionStep({ // Check for duplicates across other steps const approvers = formData.approvers || []; const isDuplicate = approvers.some( - (a: ClaimApprover) => - a.level !== level && + (a: ClaimApprover) => + a.level !== level && (a.userId === selectedUser.userId || a.email?.toLowerCase() === selectedUser.email?.toLowerCase()) ); @@ -343,10 +343,10 @@ export function ClaimApproverSelectionStep({ // Update approver in array const updatedApprovers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility - const approverIndex = updatedApprovers.findIndex((a: ClaimApprover) => + const approverIndex = updatedApprovers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); - + if (approverIndex === -1) { const step = CLAIM_STEPS.find(s => s.level === level); updatedApprovers.push({ @@ -391,10 +391,10 @@ export function ClaimApproverSelectionStep({ const handleTatChange = (level: number, tat: number | string) => { const approvers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility - const index = approvers.findIndex((a: ClaimApprover) => + const index = approvers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); - + if (index !== -1) { const existingApprover = approvers[index]; if (existingApprover) { @@ -410,10 +410,10 @@ export function ClaimApproverSelectionStep({ const handleTatTypeChange = (level: number, tatType: 'hours' | 'days') => { const approvers = [...(formData.approvers || [])]; // Find by originalStepLevel first, then fallback to level for backwards compatibility - const index = approvers.findIndex((a: ClaimApprover) => + const index = approvers.findIndex((a: ClaimApprover) => a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional) ); - + if (index !== -1) { const existingApprover = approvers[index]; if (existingApprover) { @@ -430,12 +430,12 @@ export function ClaimApproverSelectionStep({ // Handle adding additional approver between steps const handleAddApproverEmailChange = (value: string) => { setAddApproverEmail(value); - + // Clear selectedUser when manually editing if (selectedAddApproverUser && selectedAddApproverUser.email.toLowerCase() !== value.toLowerCase()) { setSelectedAddApproverUser(null); } - + // Clear existing timer if (addApproverSearchTimer.current) { clearTimeout(addApproverSearchTimer.current); @@ -484,7 +484,7 @@ export function ClaimApproverSelectionStep({ secondEmail: user.secondEmail, location: user.location }); - + setAddApproverEmail(user.email); setSelectedAddApproverUser(user); setAddApproverSearchResults([]); @@ -497,7 +497,7 @@ export function ClaimApproverSelectionStep({ const handleConfirmAddApprover = async () => { const emailToAdd = addApproverEmail.trim().toLowerCase(); - + if (!emailToAdd) { toast.error('Please enter an email address'); return; @@ -540,7 +540,7 @@ export function ClaimApproverSelectionStep({ // Check for duplicates const approvers = formData.approvers || []; const isDuplicate = approvers.some( - (a: ClaimApprover) => + (a: ClaimApprover) => (a.userId && selectedAddApproverUser?.userId && a.userId === selectedAddApproverUser.userId) || a.email?.toLowerCase() === emailToAdd ); @@ -552,15 +552,15 @@ export function ClaimApproverSelectionStep({ // Find the approver for the selected step by its originalStepLevel // This handles cases where steps have been shifted due to previous additional approvers - const approverAfter = approvers.find((a: ClaimApprover) => - a.originalStepLevel === addApproverInsertAfter || + const approverAfter = approvers.find((a: ClaimApprover) => + a.originalStepLevel === addApproverInsertAfter || (!a.originalStepLevel && !a.isAdditional && a.level === addApproverInsertAfter) ); - + // Get the current level of the approver we're inserting after // If the step has been shifted, use its current level; otherwise use the original level const currentLevelAfter = approverAfter ? approverAfter.level : addApproverInsertAfter; - + // Calculate insert level based on current shifted level const insertLevel = currentLevelAfter + 1; @@ -570,7 +570,7 @@ export function ClaimApproverSelectionStep({ // After shifting, we'll have the same number of unique levels + 1 (the new approver) const currentUniqueLevels = new Set(approvers.map((a: ClaimApprover) => a.level)).size; const newTotalLevels = currentUniqueLevels + 1; - + if (newTotalLevels > maxApprovalLevels) { const violations = [{ type: 'max_approval_levels', @@ -578,7 +578,7 @@ export function ClaimApproverSelectionStep({ currentValue: newTotalLevels, maxValue: maxApprovalLevels }]; - + if (onPolicyViolation) { onPolicyViolation(violations); } else { @@ -593,12 +593,12 @@ export function ClaimApproverSelectionStep({ try { const response = await searchUsers(emailToAdd, 1); const searchOktaResults = response.data?.data || []; - + if (searchOktaResults.length === 0) { toast.error('User not found in organization directory. Please use @ to search for users.'); return; } - + const foundUser = searchOktaResults[0]; await ensureUserExists({ userId: foundUser.userId, @@ -617,7 +617,7 @@ export function ClaimApproverSelectionStep({ secondEmail: foundUser.secondEmail, location: foundUser.location }); - + // Use found user - insert at integer level and shift subsequent approvers // insertLevel is already calculated above based on current shifted level const newApprover: ClaimApprover = { @@ -631,7 +631,7 @@ export function ClaimApproverSelectionStep({ insertAfterLevel: addApproverInsertAfter, // Store original step level for reference stepName: `Additional Approver - ${foundUser.displayName || foundUser.email}`, }; - + // Shift all approvers with level >= insertLevel up by 1 (both fixed and additional) const updatedApprovers = approvers.map((a: ClaimApprover) => { if (a.level >= insertLevel) { @@ -639,13 +639,13 @@ export function ClaimApproverSelectionStep({ } return a; }); - + // Insert the new approver updatedApprovers.push(newApprover); - + // Sort by level to maintain order updatedApprovers.sort((a, b) => a.level - b.level); - + updateFormData('approvers', updatedApprovers); toast.success(`Additional approver added and subsequent steps shifted`); } catch (error) { @@ -667,7 +667,7 @@ export function ClaimApproverSelectionStep({ insertAfterLevel: addApproverInsertAfter, // Store original step level for reference stepName: `Additional Approver - ${selectedAddApproverUser.displayName || selectedAddApproverUser.email}`, }; - + // Shift all approvers with level >= insertLevel up by 1 (both fixed and additional) const updatedApprovers = approvers.map((a: ClaimApprover) => { if (a.level >= insertLevel) { @@ -675,13 +675,13 @@ export function ClaimApproverSelectionStep({ } return a; }); - + // Insert the new approver updatedApprovers.push(newApprover); - + // Sort by level to maintain order updatedApprovers.sort((a, b) => a.level - b.level); - + updateFormData('approvers', updatedApprovers); toast.success(`Additional approver added and subsequent steps shifted`); } @@ -699,12 +699,12 @@ export function ClaimApproverSelectionStep({ const handleRemoveAdditionalApprover = (level: number) => { const approvers = [...(formData.approvers || [])]; const approverToRemove = approvers.find((a: ClaimApprover) => a.level === level); - + if (!approverToRemove) return; - + // Remove the additional approver const filtered = approvers.filter((a: ClaimApprover) => a.level !== level); - + // Shift all approvers with level > removed level down by 1 const updatedApprovers = filtered.map((a: ClaimApprover) => { if (a.level > level && !a.isAdditional) { @@ -712,10 +712,10 @@ export function ClaimApproverSelectionStep({ } return a; }); - + // Sort by level to maintain order updatedApprovers.sort((a, b) => a.level - b.level); - + updateFormData('approvers', updatedApprovers); toast.success('Additional approver removed and subsequent steps shifted back'); }; @@ -829,15 +829,15 @@ export function ClaimApproverSelectionStep({ {/* Dynamic Approver Cards - Filter out system steps (auto-processed) */} {(() => { // Count additional approvers before first step - const additionalBeforeFirst = sortedApprovers.filter((a: ClaimApprover) => + const additionalBeforeFirst = sortedApprovers.filter((a: ClaimApprover) => a.isAdditional && a.insertAfterLevel === 0 ); - + let displayIndex = additionalBeforeFirst.length; // Start index after additional approvers before first step - + return CLAIM_STEPS.filter((step) => !step.isAuto).map((step, index, filteredSteps) => { // Find approver by originalStepLevel first, then fallback to level - const approver = approvers.find((a: ClaimApprover) => + const approver = approvers.find((a: ClaimApprover) => a.originalStepLevel === step.level || (!a.originalStepLevel && a.level === step.level && !a.isAdditional) ) || { email: '', @@ -856,17 +856,17 @@ export function ClaimApproverSelectionStep({ // Additional approvers inserted after this step will have insertAfterLevel === step.level // and their level will be step.level + 1 (or higher if multiple are added) const additionalApproversAfter = sortedApprovers.filter( - (a: ClaimApprover) => - a.isAdditional && + (a: ClaimApprover) => + a.isAdditional && a.insertAfterLevel === step.level ).sort((a, b) => a.level - b.level); // Calculate current step's display number const currentStepDisplayNumber = displayIndex + 1; - + // Increment display index for this step displayIndex++; - + // Increment display index for each additional approver after this step displayIndex += additionalApproversAfter.length; @@ -875,238 +875,259 @@ export function ClaimApproverSelectionStep({
- - {/* Render additional approvers before this step if any */} - {index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => { - const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers - return ( -
-
-
-
-
-
-
- {addDisplayNumber} -
-
-
- - Additional Approver - - - ADDITIONAL - - -
-

- {addApprover.name || addApprover.email} -

-
-
Email: {addApprover.email}
-
TAT: {addApprover.tat} {addApprover.tatType}
-
+ + {/* Render additional approvers before this step if any */} + {index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => { + const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers + return ( +
+
+
+
+
+
+
+ {addDisplayNumber} +
+
+
+ + Additional Approver + + + ADDITIONAL + + +
+

+ {addApprover.name || addApprover.email} +

+
+
Email: {addApprover.email}
+
TAT: {addApprover.tat} {addApprover.tatType}
+
+
-
- ); - })} - -
-
-
- {currentStepDisplayNumber} -
-
-
- - {step.name} - - {isLast && ( - FINAL - )} - {isPreFilled && ( - PRE-FILLED - )} +
+
+ {currentStepDisplayNumber}
-

{step.description}

- - {isEditable && ( -
-
-
- - {approver.email && approver.userId && ( - - - Verified - - )} +
+
+ + {step.name} + + {isLast && ( + FINAL + )} + {isPreFilled && ( + PRE-FILLED + )} +
+

{step.description}

+ + {isEditable && (() => { + const isVerified = !!(approver.email && approver.userId); + const isEmpty = !approver.email && !isPreFilled; + + return ( +
+
+
+ + {isVerified && ( + + + Verified + + )} +
+
+ { + const newValue = e.target.value; + if (!isPreFilled) { + handleApproverEmailChange(step.level, newValue); + } + }} + disabled={isPreFilled || step.isAuto} + className={`h-9 border-2 transition-all mt-1 w-full text-sm ${isPreFilled + ? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed font-medium' + : isVerified + ? 'bg-green-50/50 border-green-600 focus:border-green-700 ring-offset-green-50 focus:ring-1 focus:ring-green-100 font-semibold text-gray-900' + : 'bg-white border-blue-300 shadow-sm shadow-blue-100/50 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900' + }`} + /> + {/* Search suggestions dropdown */} + {!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && ( +
+ {userSearchLoading[step.level - 1] ? ( +
Searching...
+ ) : ( +
    + {userSearchResults[step.level - 1]?.map((u) => ( +
  • handleUserSelect(step.level, u)} + > +
    {u.displayName || u.email}
    +
    {u.email}
    + {u.department && ( +
    {u.department}
    + )} +
  • + ))} +
+ )} +
+ )} +
+ {approver.name && ( +

+ Selected: {approver.name} +

+ )} +
+ +
+ +
+ handleTatChange(step.level, parseInt(e.target.value) || '')} + disabled={step.isAuto} + className={`h-9 border-2 transition-all flex-1 text-sm ${isPreFilled + ? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed font-medium' + : isVerified + ? 'bg-green-50/50 border-green-600 focus:border-green-700 focus:ring-1 focus:ring-green-100 font-semibold text-gray-900' + : 'bg-white border-blue-300 shadow-sm shadow-blue-100/50 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900' + }`} + /> + +
+
-
- { - const newValue = e.target.value; - if (!isPreFilled) { - handleApproverEmailChange(step.level, newValue); - } - }} - disabled={isPreFilled || step.isAuto} - className="h-9 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full text-sm" - /> - {/* Search suggestions dropdown */} - {!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && ( -
- {userSearchLoading[step.level - 1] ? ( -
Searching...
- ) : ( -
    - {userSearchResults[step.level - 1]?.map((u) => ( -
  • handleUserSelect(step.level, u)} - > -
    {u.displayName || u.email}
    -
    {u.email}
    - {u.department && ( -
    {u.department}
    - )} -
  • - ))} -
+ ); + })()} +
+
+
+ + {/* Render additional approvers after this step */} + {additionalApproversAfter.map((addApprover: ClaimApprover, addIndex: number) => { + // Additional approvers come after the current step, so they should be numbered after it + const addDisplayNumber = currentStepDisplayNumber + addIndex + 1; + return ( +
+
+
+
+
+
+
+ {addDisplayNumber} +
+
+
+ + {addApprover.stepName || 'Additional Approver'} + + + ADDITIONAL + + {addApprover.email && addApprover.userId && ( + + + Verified + + )} + +
+

+ {addApprover.name || addApprover.email || 'No approver assigned'} +

+ {addApprover.email && ( +
+
Email: {addApprover.email}
+ {addApprover.tat && ( +
TAT: {addApprover.tat} {addApprover.tatType}
)}
)}
- {approver.name && ( -

- Selected: {approver.name} -

- )} -
- -
- -
- handleTatChange(step.level, parseInt(e.target.value) || '')} - disabled={step.isAuto} - className="h-9 border-2 border-gray-300 focus:border-blue-500 flex-1 text-sm" - /> - -
- )} -
-
+
+ ); + })}
- - {/* Render additional approvers after this step */} - {additionalApproversAfter.map((addApprover: ClaimApprover, addIndex: number) => { - // Additional approvers come after the current step, so they should be numbered after it - const addDisplayNumber = currentStepDisplayNumber + addIndex + 1; - return ( -
-
-
-
-
-
-
- {addDisplayNumber} -
-
-
- - {addApprover.stepName || 'Additional Approver'} - - - ADDITIONAL - - {addApprover.email && addApprover.userId && ( - - - Verified - - )} - -
-

- {addApprover.name || addApprover.email || 'No approver assigned'} -

- {addApprover.email && ( -
-
Email: {addApprover.email}
- {addApprover.tat && ( -
TAT: {addApprover.tat} {addApprover.tatType}
- )} -
- )} -
-
-
-
- ); - })} -
- ); + ); }); })()} @@ -1125,17 +1146,17 @@ export function ClaimApproverSelectionStep({ {sortedApprovers.map((approver: ClaimApprover) => { // Skip system/auto steps // Find step by originalStepLevel first, then fallback to level - const step = approver.originalStepLevel + const step = approver.originalStepLevel ? CLAIM_STEPS.find(s => s.level === approver.originalStepLevel) : CLAIM_STEPS.find(s => s.level === approver.level && !approver.isAdditional); - + if (step?.isAuto) return null; - + const tat = Number(approver.tat || 0); const tatType = approver.tatType || 'hours'; const hours = tatType === 'days' ? tat * 24 : tat; if (!tat) return null; - + // Handle additional approvers if (approver.isAdditional) { const afterStep = CLAIM_STEPS.find(s => s.level === approver.insertAfterLevel); @@ -1148,7 +1169,7 @@ export function ClaimApproverSelectionStep({
); } - + return (
{step?.name || 'Unknown'} @@ -1173,13 +1194,13 @@ export function ClaimApproverSelectionStep({ Add an additional approver between workflow steps. The approver will be inserted after the selected step. Additional approvers cannot be added after "Requestor Claim Approval". - +
{/* Insert After Level Selection */}
-