in pwc implemention implemented upto the invoice genration ui altred to show total amout that may change later afer clarification
This commit is contained in:
parent
7ae9133b98
commit
80ed407cd8
@ -70,7 +70,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
onPolicyViolation,
|
onPolicyViolation,
|
||||||
}: ClaimApproverSelectionStepProps) {
|
}: ClaimApproverSelectionStepProps) {
|
||||||
const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch();
|
const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch();
|
||||||
|
|
||||||
// State for add approver modal
|
// State for add approver modal
|
||||||
const [showAddApproverModal, setShowAddApproverModal] = useState(false);
|
const [showAddApproverModal, setShowAddApproverModal] = useState(false);
|
||||||
const [addApproverEmail, setAddApproverEmail] = useState('');
|
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
|
// 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);
|
const approver = approvers.find((a: ClaimApprover) => a.level === step.level);
|
||||||
|
|
||||||
if (!approver || !approver.email || !approver.userId || !approver.tat) {
|
if (!approver || !approver.email || !approver.userId || !approver.tat) {
|
||||||
missingSteps.push(`${step.name}`);
|
missingSteps.push(`${step.name}`);
|
||||||
}
|
}
|
||||||
@ -120,20 +120,20 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Initialize approvers array for all 8 steps
|
// Initialize approvers array for all 8 steps
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentApprovers = formData.approvers || [];
|
const currentApprovers = formData.approvers || [];
|
||||||
|
|
||||||
// If we already have approvers (including additional ones), don't reinitialize
|
// If we already have approvers (including additional ones), don't reinitialize
|
||||||
// This prevents creating duplicates when approvers have been shifted
|
// This prevents creating duplicates when approvers have been shifted
|
||||||
if (currentApprovers.length > 0) {
|
if (currentApprovers.length > 0) {
|
||||||
// Just ensure all fixed steps have their approvers, but don't recreate shifted ones
|
// Just ensure all fixed steps have their approvers, but don't recreate shifted ones
|
||||||
const newApprovers: ClaimApprover[] = [];
|
const newApprovers: ClaimApprover[] = [];
|
||||||
const additionalApprovers = currentApprovers.filter((a: ClaimApprover) => a.isAdditional);
|
const additionalApprovers = currentApprovers.filter((a: ClaimApprover) => a.isAdditional);
|
||||||
|
|
||||||
CLAIM_STEPS.forEach((step) => {
|
CLAIM_STEPS.forEach((step) => {
|
||||||
// Find existing approver by originalStepLevel (handles shifted levels)
|
// 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)
|
a.originalStepLevel === step.level || (!a.originalStepLevel && !a.isAdditional && a.level === step.level)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
// Use existing approver (preserves shifted level)
|
// Use existing approver (preserves shifted level)
|
||||||
newApprovers.push(existing);
|
newApprovers.push(existing);
|
||||||
@ -182,19 +182,19 @@ export function ClaimApproverSelectionStep({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add back all additional approvers
|
// Add back all additional approvers
|
||||||
additionalApprovers.forEach((addApprover: ClaimApprover) => {
|
additionalApprovers.forEach((addApprover: ClaimApprover) => {
|
||||||
newApprovers.push(addApprover);
|
newApprovers.push(addApprover);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort by level
|
// Sort by level
|
||||||
newApprovers.sort((a, b) => a.level - b.level);
|
newApprovers.sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
// Only update if there are actual changes (to avoid infinite loops)
|
// Only update if there are actual changes (to avoid infinite loops)
|
||||||
const hasChanges = JSON.stringify(currentApprovers.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 })));
|
JSON.stringify(newApprovers.map(a => ({ level: a.level, originalStepLevel: a.originalStepLevel })));
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
updateFormData('approvers', newApprovers);
|
updateFormData('approvers', newApprovers);
|
||||||
}
|
}
|
||||||
@ -246,10 +246,10 @@ export function ClaimApproverSelectionStep({
|
|||||||
const handleApproverEmailChange = (level: number, value: string) => {
|
const handleApproverEmailChange = (level: number, value: string) => {
|
||||||
const approvers = [...(formData.approvers || [])];
|
const approvers = [...(formData.approvers || [])];
|
||||||
// Find by originalStepLevel first, then fallback to level for backwards compatibility
|
// 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)
|
a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
// Create new approver entry
|
// Create new approver entry
|
||||||
const step = CLAIM_STEPS.find(s => s.level === level);
|
const step = CLAIM_STEPS.find(s => s.level === level);
|
||||||
@ -304,8 +304,8 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Check for duplicates across other steps
|
// Check for duplicates across other steps
|
||||||
const approvers = formData.approvers || [];
|
const approvers = formData.approvers || [];
|
||||||
const isDuplicate = approvers.some(
|
const isDuplicate = approvers.some(
|
||||||
(a: ClaimApprover) =>
|
(a: ClaimApprover) =>
|
||||||
a.level !== level &&
|
a.level !== level &&
|
||||||
(a.userId === selectedUser.userId || a.email?.toLowerCase() === selectedUser.email?.toLowerCase())
|
(a.userId === selectedUser.userId || a.email?.toLowerCase() === selectedUser.email?.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -343,10 +343,10 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Update approver in array
|
// Update approver in array
|
||||||
const updatedApprovers = [...(formData.approvers || [])];
|
const updatedApprovers = [...(formData.approvers || [])];
|
||||||
// Find by originalStepLevel first, then fallback to level for backwards compatibility
|
// 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)
|
a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (approverIndex === -1) {
|
if (approverIndex === -1) {
|
||||||
const step = CLAIM_STEPS.find(s => s.level === level);
|
const step = CLAIM_STEPS.find(s => s.level === level);
|
||||||
updatedApprovers.push({
|
updatedApprovers.push({
|
||||||
@ -391,10 +391,10 @@ export function ClaimApproverSelectionStep({
|
|||||||
const handleTatChange = (level: number, tat: number | string) => {
|
const handleTatChange = (level: number, tat: number | string) => {
|
||||||
const approvers = [...(formData.approvers || [])];
|
const approvers = [...(formData.approvers || [])];
|
||||||
// Find by originalStepLevel first, then fallback to level for backwards compatibility
|
// 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)
|
a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const existingApprover = approvers[index];
|
const existingApprover = approvers[index];
|
||||||
if (existingApprover) {
|
if (existingApprover) {
|
||||||
@ -410,10 +410,10 @@ export function ClaimApproverSelectionStep({
|
|||||||
const handleTatTypeChange = (level: number, tatType: 'hours' | 'days') => {
|
const handleTatTypeChange = (level: number, tatType: 'hours' | 'days') => {
|
||||||
const approvers = [...(formData.approvers || [])];
|
const approvers = [...(formData.approvers || [])];
|
||||||
// Find by originalStepLevel first, then fallback to level for backwards compatibility
|
// 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)
|
a.originalStepLevel === level || (!a.originalStepLevel && a.level === level && !a.isAdditional)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const existingApprover = approvers[index];
|
const existingApprover = approvers[index];
|
||||||
if (existingApprover) {
|
if (existingApprover) {
|
||||||
@ -430,12 +430,12 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Handle adding additional approver between steps
|
// Handle adding additional approver between steps
|
||||||
const handleAddApproverEmailChange = (value: string) => {
|
const handleAddApproverEmailChange = (value: string) => {
|
||||||
setAddApproverEmail(value);
|
setAddApproverEmail(value);
|
||||||
|
|
||||||
// Clear selectedUser when manually editing
|
// Clear selectedUser when manually editing
|
||||||
if (selectedAddApproverUser && selectedAddApproverUser.email.toLowerCase() !== value.toLowerCase()) {
|
if (selectedAddApproverUser && selectedAddApproverUser.email.toLowerCase() !== value.toLowerCase()) {
|
||||||
setSelectedAddApproverUser(null);
|
setSelectedAddApproverUser(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear existing timer
|
// Clear existing timer
|
||||||
if (addApproverSearchTimer.current) {
|
if (addApproverSearchTimer.current) {
|
||||||
clearTimeout(addApproverSearchTimer.current);
|
clearTimeout(addApproverSearchTimer.current);
|
||||||
@ -484,7 +484,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
secondEmail: user.secondEmail,
|
secondEmail: user.secondEmail,
|
||||||
location: user.location
|
location: user.location
|
||||||
});
|
});
|
||||||
|
|
||||||
setAddApproverEmail(user.email);
|
setAddApproverEmail(user.email);
|
||||||
setSelectedAddApproverUser(user);
|
setSelectedAddApproverUser(user);
|
||||||
setAddApproverSearchResults([]);
|
setAddApproverSearchResults([]);
|
||||||
@ -497,7 +497,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
|
|
||||||
const handleConfirmAddApprover = async () => {
|
const handleConfirmAddApprover = async () => {
|
||||||
const emailToAdd = addApproverEmail.trim().toLowerCase();
|
const emailToAdd = addApproverEmail.trim().toLowerCase();
|
||||||
|
|
||||||
if (!emailToAdd) {
|
if (!emailToAdd) {
|
||||||
toast.error('Please enter an email address');
|
toast.error('Please enter an email address');
|
||||||
return;
|
return;
|
||||||
@ -540,7 +540,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
const approvers = formData.approvers || [];
|
const approvers = formData.approvers || [];
|
||||||
const isDuplicate = approvers.some(
|
const isDuplicate = approvers.some(
|
||||||
(a: ClaimApprover) =>
|
(a: ClaimApprover) =>
|
||||||
(a.userId && selectedAddApproverUser?.userId && a.userId === selectedAddApproverUser.userId) ||
|
(a.userId && selectedAddApproverUser?.userId && a.userId === selectedAddApproverUser.userId) ||
|
||||||
a.email?.toLowerCase() === emailToAdd
|
a.email?.toLowerCase() === emailToAdd
|
||||||
);
|
);
|
||||||
@ -552,15 +552,15 @@ export function ClaimApproverSelectionStep({
|
|||||||
|
|
||||||
// Find the approver for the selected step by its originalStepLevel
|
// Find the approver for the selected step by its originalStepLevel
|
||||||
// This handles cases where steps have been shifted due to previous additional approvers
|
// This handles cases where steps have been shifted due to previous additional approvers
|
||||||
const approverAfter = approvers.find((a: ClaimApprover) =>
|
const approverAfter = approvers.find((a: ClaimApprover) =>
|
||||||
a.originalStepLevel === addApproverInsertAfter ||
|
a.originalStepLevel === addApproverInsertAfter ||
|
||||||
(!a.originalStepLevel && !a.isAdditional && a.level === addApproverInsertAfter)
|
(!a.originalStepLevel && !a.isAdditional && a.level === addApproverInsertAfter)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get the current level of the approver we're inserting after
|
// 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
|
// If the step has been shifted, use its current level; otherwise use the original level
|
||||||
const currentLevelAfter = approverAfter ? approverAfter.level : addApproverInsertAfter;
|
const currentLevelAfter = approverAfter ? approverAfter.level : addApproverInsertAfter;
|
||||||
|
|
||||||
// Calculate insert level based on current shifted level
|
// Calculate insert level based on current shifted level
|
||||||
const insertLevel = currentLevelAfter + 1;
|
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)
|
// 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 currentUniqueLevels = new Set(approvers.map((a: ClaimApprover) => a.level)).size;
|
||||||
const newTotalLevels = currentUniqueLevels + 1;
|
const newTotalLevels = currentUniqueLevels + 1;
|
||||||
|
|
||||||
if (newTotalLevels > maxApprovalLevels) {
|
if (newTotalLevels > maxApprovalLevels) {
|
||||||
const violations = [{
|
const violations = [{
|
||||||
type: 'max_approval_levels',
|
type: 'max_approval_levels',
|
||||||
@ -578,7 +578,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
currentValue: newTotalLevels,
|
currentValue: newTotalLevels,
|
||||||
maxValue: maxApprovalLevels
|
maxValue: maxApprovalLevels
|
||||||
}];
|
}];
|
||||||
|
|
||||||
if (onPolicyViolation) {
|
if (onPolicyViolation) {
|
||||||
onPolicyViolation(violations);
|
onPolicyViolation(violations);
|
||||||
} else {
|
} else {
|
||||||
@ -593,12 +593,12 @@ export function ClaimApproverSelectionStep({
|
|||||||
try {
|
try {
|
||||||
const response = await searchUsers(emailToAdd, 1);
|
const response = await searchUsers(emailToAdd, 1);
|
||||||
const searchOktaResults = response.data?.data || [];
|
const searchOktaResults = response.data?.data || [];
|
||||||
|
|
||||||
if (searchOktaResults.length === 0) {
|
if (searchOktaResults.length === 0) {
|
||||||
toast.error('User not found in organization directory. Please use @ to search for users.');
|
toast.error('User not found in organization directory. Please use @ to search for users.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundUser = searchOktaResults[0];
|
const foundUser = searchOktaResults[0];
|
||||||
await ensureUserExists({
|
await ensureUserExists({
|
||||||
userId: foundUser.userId,
|
userId: foundUser.userId,
|
||||||
@ -617,7 +617,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
secondEmail: foundUser.secondEmail,
|
secondEmail: foundUser.secondEmail,
|
||||||
location: foundUser.location
|
location: foundUser.location
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use found user - insert at integer level and shift subsequent approvers
|
// Use found user - insert at integer level and shift subsequent approvers
|
||||||
// insertLevel is already calculated above based on current shifted level
|
// insertLevel is already calculated above based on current shifted level
|
||||||
const newApprover: ClaimApprover = {
|
const newApprover: ClaimApprover = {
|
||||||
@ -631,7 +631,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
insertAfterLevel: addApproverInsertAfter, // Store original step level for reference
|
insertAfterLevel: addApproverInsertAfter, // Store original step level for reference
|
||||||
stepName: `Additional Approver - ${foundUser.displayName || foundUser.email}`,
|
stepName: `Additional Approver - ${foundUser.displayName || foundUser.email}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shift all approvers with level >= insertLevel up by 1 (both fixed and additional)
|
// Shift all approvers with level >= insertLevel up by 1 (both fixed and additional)
|
||||||
const updatedApprovers = approvers.map((a: ClaimApprover) => {
|
const updatedApprovers = approvers.map((a: ClaimApprover) => {
|
||||||
if (a.level >= insertLevel) {
|
if (a.level >= insertLevel) {
|
||||||
@ -639,13 +639,13 @@ export function ClaimApproverSelectionStep({
|
|||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert the new approver
|
// Insert the new approver
|
||||||
updatedApprovers.push(newApprover);
|
updatedApprovers.push(newApprover);
|
||||||
|
|
||||||
// Sort by level to maintain order
|
// Sort by level to maintain order
|
||||||
updatedApprovers.sort((a, b) => a.level - b.level);
|
updatedApprovers.sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
updateFormData('approvers', updatedApprovers);
|
updateFormData('approvers', updatedApprovers);
|
||||||
toast.success(`Additional approver added and subsequent steps shifted`);
|
toast.success(`Additional approver added and subsequent steps shifted`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -667,7 +667,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
insertAfterLevel: addApproverInsertAfter, // Store original step level for reference
|
insertAfterLevel: addApproverInsertAfter, // Store original step level for reference
|
||||||
stepName: `Additional Approver - ${selectedAddApproverUser.displayName || selectedAddApproverUser.email}`,
|
stepName: `Additional Approver - ${selectedAddApproverUser.displayName || selectedAddApproverUser.email}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Shift all approvers with level >= insertLevel up by 1 (both fixed and additional)
|
// Shift all approvers with level >= insertLevel up by 1 (both fixed and additional)
|
||||||
const updatedApprovers = approvers.map((a: ClaimApprover) => {
|
const updatedApprovers = approvers.map((a: ClaimApprover) => {
|
||||||
if (a.level >= insertLevel) {
|
if (a.level >= insertLevel) {
|
||||||
@ -675,13 +675,13 @@ export function ClaimApproverSelectionStep({
|
|||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert the new approver
|
// Insert the new approver
|
||||||
updatedApprovers.push(newApprover);
|
updatedApprovers.push(newApprover);
|
||||||
|
|
||||||
// Sort by level to maintain order
|
// Sort by level to maintain order
|
||||||
updatedApprovers.sort((a, b) => a.level - b.level);
|
updatedApprovers.sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
updateFormData('approvers', updatedApprovers);
|
updateFormData('approvers', updatedApprovers);
|
||||||
toast.success(`Additional approver added and subsequent steps shifted`);
|
toast.success(`Additional approver added and subsequent steps shifted`);
|
||||||
}
|
}
|
||||||
@ -699,12 +699,12 @@ export function ClaimApproverSelectionStep({
|
|||||||
const handleRemoveAdditionalApprover = (level: number) => {
|
const handleRemoveAdditionalApprover = (level: number) => {
|
||||||
const approvers = [...(formData.approvers || [])];
|
const approvers = [...(formData.approvers || [])];
|
||||||
const approverToRemove = approvers.find((a: ClaimApprover) => a.level === level);
|
const approverToRemove = approvers.find((a: ClaimApprover) => a.level === level);
|
||||||
|
|
||||||
if (!approverToRemove) return;
|
if (!approverToRemove) return;
|
||||||
|
|
||||||
// Remove the additional approver
|
// Remove the additional approver
|
||||||
const filtered = approvers.filter((a: ClaimApprover) => a.level !== level);
|
const filtered = approvers.filter((a: ClaimApprover) => a.level !== level);
|
||||||
|
|
||||||
// Shift all approvers with level > removed level down by 1
|
// Shift all approvers with level > removed level down by 1
|
||||||
const updatedApprovers = filtered.map((a: ClaimApprover) => {
|
const updatedApprovers = filtered.map((a: ClaimApprover) => {
|
||||||
if (a.level > level && !a.isAdditional) {
|
if (a.level > level && !a.isAdditional) {
|
||||||
@ -712,10 +712,10 @@ export function ClaimApproverSelectionStep({
|
|||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort by level to maintain order
|
// Sort by level to maintain order
|
||||||
updatedApprovers.sort((a, b) => a.level - b.level);
|
updatedApprovers.sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
updateFormData('approvers', updatedApprovers);
|
updateFormData('approvers', updatedApprovers);
|
||||||
toast.success('Additional approver removed and subsequent steps shifted back');
|
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) */}
|
{/* Dynamic Approver Cards - Filter out system steps (auto-processed) */}
|
||||||
{(() => {
|
{(() => {
|
||||||
// Count additional approvers before first step
|
// Count additional approvers before first step
|
||||||
const additionalBeforeFirst = sortedApprovers.filter((a: ClaimApprover) =>
|
const additionalBeforeFirst = sortedApprovers.filter((a: ClaimApprover) =>
|
||||||
a.isAdditional && a.insertAfterLevel === 0
|
a.isAdditional && a.insertAfterLevel === 0
|
||||||
);
|
);
|
||||||
|
|
||||||
let displayIndex = additionalBeforeFirst.length; // Start index after additional approvers before first step
|
let displayIndex = additionalBeforeFirst.length; // Start index after additional approvers before first step
|
||||||
|
|
||||||
return CLAIM_STEPS.filter((step) => !step.isAuto).map((step, index, filteredSteps) => {
|
return CLAIM_STEPS.filter((step) => !step.isAuto).map((step, index, filteredSteps) => {
|
||||||
// Find approver by originalStepLevel first, then fallback to level
|
// 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)
|
a.originalStepLevel === step.level || (!a.originalStepLevel && a.level === step.level && !a.isAdditional)
|
||||||
) || {
|
) || {
|
||||||
email: '',
|
email: '',
|
||||||
@ -856,17 +856,17 @@ export function ClaimApproverSelectionStep({
|
|||||||
// Additional approvers inserted after this step will have insertAfterLevel === step.level
|
// 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)
|
// and their level will be step.level + 1 (or higher if multiple are added)
|
||||||
const additionalApproversAfter = sortedApprovers.filter(
|
const additionalApproversAfter = sortedApprovers.filter(
|
||||||
(a: ClaimApprover) =>
|
(a: ClaimApprover) =>
|
||||||
a.isAdditional &&
|
a.isAdditional &&
|
||||||
a.insertAfterLevel === step.level
|
a.insertAfterLevel === step.level
|
||||||
).sort((a, b) => a.level - b.level);
|
).sort((a, b) => a.level - b.level);
|
||||||
|
|
||||||
// Calculate current step's display number
|
// Calculate current step's display number
|
||||||
const currentStepDisplayNumber = displayIndex + 1;
|
const currentStepDisplayNumber = displayIndex + 1;
|
||||||
|
|
||||||
// Increment display index for this step
|
// Increment display index for this step
|
||||||
displayIndex++;
|
displayIndex++;
|
||||||
|
|
||||||
// Increment display index for each additional approver after this step
|
// Increment display index for each additional approver after this step
|
||||||
displayIndex += additionalApproversAfter.length;
|
displayIndex += additionalApproversAfter.length;
|
||||||
|
|
||||||
@ -875,238 +875,259 @@ export function ClaimApproverSelectionStep({
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="w-px h-3 bg-gray-300"></div>
|
<div className="w-px h-3 bg-gray-300"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Render additional approvers before this step if any */}
|
{/* Render additional approvers before this step if any */}
|
||||||
{index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => {
|
{index === 0 && additionalBeforeFirst.map((addApprover: ClaimApprover, addIndex: number) => {
|
||||||
const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers
|
const addDisplayNumber = addIndex + 1; // Number from 1 for first additional approvers
|
||||||
return (
|
return (
|
||||||
<div key={`additional-${addApprover.level}`} className="space-y-1">
|
<div key={`additional-${addApprover.level}`} className="space-y-1">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<div className="w-px h-3 bg-gray-300"></div>
|
<div className="w-px h-3 bg-gray-300"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50">
|
<div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600">
|
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600">
|
||||||
<span className="text-white font-semibold text-sm">{addDisplayNumber}</span>
|
<span className="text-white font-semibold text-sm">{addDisplayNumber}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
<span className="font-semibold text-gray-900 text-sm">
|
<span className="font-semibold text-gray-900 text-sm">
|
||||||
Additional Approver
|
Additional Approver
|
||||||
</span>
|
</span>
|
||||||
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
|
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
|
||||||
ADDITIONAL
|
ADDITIONAL
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRemoveAdditionalApprover(addApprover.level)}
|
onClick={() => handleRemoveAdditionalApprover(addApprover.level)}
|
||||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
|
className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
>
|
>
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 mb-2">
|
<p className="text-xs text-gray-600 mb-2">
|
||||||
{addApprover.name || addApprover.email}
|
{addApprover.name || addApprover.email}
|
||||||
</p>
|
</p>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
<div>Email: {addApprover.email}</div>
|
<div>Email: {addApprover.email}</div>
|
||||||
<div>TAT: {addApprover.tat} {addApprover.tatType}</div>
|
<div>TAT: {addApprover.tat} {addApprover.tatType}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
|
<div className={`p-3 rounded-lg border-2 transition-all ${approver.email && approver.userId
|
||||||
<div className={`p-3 rounded-lg border-2 transition-all ${
|
? 'border-green-200 bg-green-50'
|
||||||
approver.email && approver.userId
|
|
||||||
? 'border-green-200 bg-green-50'
|
|
||||||
: isPreFilled
|
: isPreFilled
|
||||||
? 'border-blue-200 bg-blue-50'
|
? 'border-blue-200 bg-blue-50'
|
||||||
: 'border-gray-200 bg-gray-50'
|
: 'border-gray-200 bg-gray-50'
|
||||||
}`}>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
|
||||||
approver.email && approver.userId
|
|
||||||
? 'bg-green-600'
|
|
||||||
: isPreFilled
|
|
||||||
? 'bg-blue-600'
|
|
||||||
: 'bg-gray-400'
|
|
||||||
}`}>
|
}`}>
|
||||||
<span className="text-white font-semibold text-sm">{currentStepDisplayNumber}</span>
|
<div className="flex items-start gap-3">
|
||||||
</div>
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${approver.email && approver.userId
|
||||||
<div className="flex-1 min-w-0">
|
? 'bg-green-600'
|
||||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
: isPreFilled
|
||||||
<span className="font-semibold text-gray-900 text-sm">
|
? 'bg-blue-600'
|
||||||
{step.name}
|
: 'bg-gray-400'
|
||||||
</span>
|
}`}>
|
||||||
{isLast && (
|
<span className="text-white font-semibold text-sm">{currentStepDisplayNumber}</span>
|
||||||
<Badge variant="destructive" className="text-xs">FINAL</Badge>
|
|
||||||
)}
|
|
||||||
{isPreFilled && (
|
|
||||||
<Badge variant="outline" className="text-xs">PRE-FILLED</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 mb-2">{step.description}</p>
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
{isEditable && (
|
<span className="font-semibold text-gray-900 text-sm">
|
||||||
<div className="space-y-2">
|
{step.name}
|
||||||
<div>
|
</span>
|
||||||
<div className="flex items-center justify-between mb-1">
|
{isLast && (
|
||||||
<Label htmlFor={`approver-${step.level}`} className="text-xs font-medium">
|
<Badge variant="destructive" className="text-xs">FINAL</Badge>
|
||||||
Email Address {!isPreFilled && '*'}
|
)}
|
||||||
</Label>
|
{isPreFilled && (
|
||||||
{approver.email && approver.userId && (
|
<Badge variant="outline" className="text-xs">PRE-FILLED</Badge>
|
||||||
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
)}
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
</div>
|
||||||
Verified
|
<p className="text-xs text-gray-600 mb-2">{step.description}</p>
|
||||||
</Badge>
|
|
||||||
)}
|
{isEditable && (() => {
|
||||||
|
const isVerified = !!(approver.email && approver.userId);
|
||||||
|
const isEmpty = !approver.email && !isPreFilled;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<Label htmlFor={`approver-${step.level}`} className={`text-xs font-bold ${isEmpty ? 'text-blue-900' : isVerified ? 'text-green-900' : 'text-gray-900'
|
||||||
|
}`}>
|
||||||
|
Approver Email {!isPreFilled && '*'}
|
||||||
|
{isEmpty && <span className="ml-2 text-[10px] font-semibold italic text-blue-600">(Required)</span>}
|
||||||
|
</Label>
|
||||||
|
{isVerified && (
|
||||||
|
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
||||||
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
|
Verified
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id={`approver-${step.level}`}
|
||||||
|
type="text"
|
||||||
|
placeholder={isPreFilled ? approver.email : "@username or email..."}
|
||||||
|
value={approver.email || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
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) && (
|
||||||
|
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg">
|
||||||
|
{userSearchLoading[step.level - 1] ? (
|
||||||
|
<div className="p-2 text-xs text-gray-500">Searching...</div>
|
||||||
|
) : (
|
||||||
|
<ul className="max-h-56 overflow-auto divide-y">
|
||||||
|
{userSearchResults[step.level - 1]?.map((u) => (
|
||||||
|
<li
|
||||||
|
key={u.userId}
|
||||||
|
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
|
||||||
|
onClick={() => handleUserSelect(step.level, u)}
|
||||||
|
>
|
||||||
|
<div className="font-medium text-gray-900">{u.displayName || u.email}</div>
|
||||||
|
<div className="text-xs text-gray-600">{u.email}</div>
|
||||||
|
{u.department && (
|
||||||
|
<div className="text-xs text-gray-500">{u.department}</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{approver.name && (
|
||||||
|
<p className="text-xs text-green-600 mt-1">
|
||||||
|
Selected: <span className="font-semibold">{approver.name}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={`tat-${step.level}`} className={`text-xs font-bold ${isEmpty ? 'text-blue-900' : isVerified ? 'text-green-900' : 'text-gray-900'
|
||||||
|
}`}>
|
||||||
|
TAT (Turn Around Time) *
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<Input
|
||||||
|
id={`tat-${step.level}`}
|
||||||
|
type="number"
|
||||||
|
placeholder={approver.tatType === 'days' ? '7' : '24'}
|
||||||
|
min="1"
|
||||||
|
max={approver.tatType === 'days' ? '30' : '720'}
|
||||||
|
value={approver.tat || ''}
|
||||||
|
onChange={(e) => 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'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={approver.tatType || 'hours'}
|
||||||
|
onValueChange={(value) => handleTatTypeChange(step.level, value as 'hours' | 'days')}
|
||||||
|
disabled={step.isAuto}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={`w-20 h-9 border-2 transition-all text-sm ${isPreFilled
|
||||||
|
? 'bg-gray-100/80 border-gray-300 text-gray-700 cursor-not-allowed'
|
||||||
|
: isVerified
|
||||||
|
? 'bg-green-50/50 border-green-600 focus:border-green-700 focus:ring-1 focus:ring-green-100 text-gray-900 font-medium'
|
||||||
|
: 'bg-white border-blue-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-100 text-gray-900'
|
||||||
|
}`}>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="hours">Hours</SelectItem>
|
||||||
|
<SelectItem value="days">Days</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
);
|
||||||
<Input
|
})()}
|
||||||
id={`approver-${step.level}`}
|
</div>
|
||||||
type="text"
|
</div>
|
||||||
placeholder={isPreFilled ? approver.email : "@approver@royalenfield.com"}
|
</div>
|
||||||
value={approver.email || ''}
|
|
||||||
onChange={(e) => {
|
{/* Render additional approvers after this step */}
|
||||||
const newValue = e.target.value;
|
{additionalApproversAfter.map((addApprover: ClaimApprover, addIndex: number) => {
|
||||||
if (!isPreFilled) {
|
// Additional approvers come after the current step, so they should be numbered after it
|
||||||
handleApproverEmailChange(step.level, newValue);
|
const addDisplayNumber = currentStepDisplayNumber + addIndex + 1;
|
||||||
}
|
return (
|
||||||
}}
|
<div key={`additional-${addApprover.level}`} className="space-y-1">
|
||||||
disabled={isPreFilled || step.isAuto}
|
<div className="flex justify-center">
|
||||||
className="h-9 border-2 border-gray-300 focus:border-blue-500 mt-1 w-full text-sm"
|
<div className="w-px h-3 bg-gray-300"></div>
|
||||||
/>
|
</div>
|
||||||
{/* Search suggestions dropdown */}
|
<div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50">
|
||||||
{!isPreFilled && !step.isAuto && (userSearchLoading[step.level - 1] || (userSearchResults[step.level - 1]?.length || 0) > 0) && (
|
<div className="flex items-start gap-3">
|
||||||
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg">
|
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600">
|
||||||
{userSearchLoading[step.level - 1] ? (
|
<span className="text-white font-semibold text-sm">{addDisplayNumber}</span>
|
||||||
<div className="p-2 text-xs text-gray-500">Searching...</div>
|
</div>
|
||||||
) : (
|
<div className="flex-1 min-w-0">
|
||||||
<ul className="max-h-56 overflow-auto divide-y">
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||||
{userSearchResults[step.level - 1]?.map((u) => (
|
<span className="font-semibold text-gray-900 text-sm">
|
||||||
<li
|
{addApprover.stepName || 'Additional Approver'}
|
||||||
key={u.userId}
|
</span>
|
||||||
className="p-2 text-sm cursor-pointer hover:bg-gray-50"
|
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
|
||||||
onClick={() => handleUserSelect(step.level, u)}
|
ADDITIONAL
|
||||||
>
|
</Badge>
|
||||||
<div className="font-medium text-gray-900">{u.displayName || u.email}</div>
|
{addApprover.email && addApprover.userId && (
|
||||||
<div className="text-xs text-gray-600">{u.email}</div>
|
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
||||||
{u.department && (
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
<div className="text-xs text-gray-500">{u.department}</div>
|
Verified
|
||||||
)}
|
</Badge>
|
||||||
</li>
|
)}
|
||||||
))}
|
<Button
|
||||||
</ul>
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleRemoveAdditionalApprover(addApprover.level)}
|
||||||
|
className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 mb-2">
|
||||||
|
{addApprover.name || addApprover.email || 'No approver assigned'}
|
||||||
|
</p>
|
||||||
|
{addApprover.email && (
|
||||||
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
|
<div>Email: {addApprover.email}</div>
|
||||||
|
{addApprover.tat && (
|
||||||
|
<div>TAT: {addApprover.tat} {addApprover.tatType}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{approver.name && (
|
|
||||||
<p className="text-xs text-green-600 mt-1">
|
|
||||||
Selected: <span className="font-semibold">{approver.name}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor={`tat-${step.level}`} className="text-xs font-medium">
|
|
||||||
TAT (Turn Around Time) *
|
|
||||||
</Label>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<Input
|
|
||||||
id={`tat-${step.level}`}
|
|
||||||
type="number"
|
|
||||||
placeholder={approver.tatType === 'days' ? '7' : '24'}
|
|
||||||
min="1"
|
|
||||||
max={approver.tatType === 'days' ? '30' : '720'}
|
|
||||||
value={approver.tat || ''}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={approver.tatType || 'hours'}
|
|
||||||
onValueChange={(value) => handleTatTypeChange(step.level, value as 'hours' | 'days')}
|
|
||||||
disabled={step.isAuto}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-20 h-9 border-2 border-gray-300 text-sm">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="hours">Hours</SelectItem>
|
|
||||||
<SelectItem value="days">Days</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{/* 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 (
|
|
||||||
<div key={`additional-${addApprover.level}`} className="space-y-1">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<div className="w-px h-3 bg-gray-300"></div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 rounded-lg border-2 border-purple-200 bg-purple-50">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 bg-purple-600">
|
|
||||||
<span className="text-white font-semibold text-sm">{addDisplayNumber}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
|
||||||
<span className="font-semibold text-gray-900 text-sm">
|
|
||||||
{addApprover.stepName || 'Additional Approver'}
|
|
||||||
</span>
|
|
||||||
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-300">
|
|
||||||
ADDITIONAL
|
|
||||||
</Badge>
|
|
||||||
{addApprover.email && addApprover.userId && (
|
|
||||||
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-300">
|
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
|
||||||
Verified
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleRemoveAdditionalApprover(addApprover.level)}
|
|
||||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
<X className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-600 mb-2">
|
|
||||||
{addApprover.name || addApprover.email || 'No approver assigned'}
|
|
||||||
</p>
|
|
||||||
{addApprover.email && (
|
|
||||||
<div className="text-xs text-gray-500 space-y-1">
|
|
||||||
<div>Email: {addApprover.email}</div>
|
|
||||||
{addApprover.tat && (
|
|
||||||
<div>TAT: {addApprover.tat} {addApprover.tatType}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
})()}
|
})()}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -1125,17 +1146,17 @@ export function ClaimApproverSelectionStep({
|
|||||||
{sortedApprovers.map((approver: ClaimApprover) => {
|
{sortedApprovers.map((approver: ClaimApprover) => {
|
||||||
// Skip system/auto steps
|
// Skip system/auto steps
|
||||||
// Find step by originalStepLevel first, then fallback to level
|
// 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.originalStepLevel)
|
||||||
: CLAIM_STEPS.find(s => s.level === approver.level && !approver.isAdditional);
|
: CLAIM_STEPS.find(s => s.level === approver.level && !approver.isAdditional);
|
||||||
|
|
||||||
if (step?.isAuto) return null;
|
if (step?.isAuto) return null;
|
||||||
|
|
||||||
const tat = Number(approver.tat || 0);
|
const tat = Number(approver.tat || 0);
|
||||||
const tatType = approver.tatType || 'hours';
|
const tatType = approver.tatType || 'hours';
|
||||||
const hours = tatType === 'days' ? tat * 24 : tat;
|
const hours = tatType === 'days' ? tat * 24 : tat;
|
||||||
if (!tat) return null;
|
if (!tat) return null;
|
||||||
|
|
||||||
// Handle additional approvers
|
// Handle additional approvers
|
||||||
if (approver.isAdditional) {
|
if (approver.isAdditional) {
|
||||||
const afterStep = CLAIM_STEPS.find(s => s.level === approver.insertAfterLevel);
|
const afterStep = CLAIM_STEPS.find(s => s.level === approver.insertAfterLevel);
|
||||||
@ -1148,7 +1169,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={approver.level} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
<div key={approver.level} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||||
<span className="text-sm font-medium">{step?.name || 'Unknown'}</span>
|
<span className="text-sm font-medium">{step?.name || 'Unknown'}</span>
|
||||||
@ -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".
|
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".
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{/* Insert After Level Selection */}
|
{/* Insert After Level Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-sm font-medium">Insert After Step *</Label>
|
<Label className="text-sm font-medium">Insert After Step *</Label>
|
||||||
<Select
|
<Select
|
||||||
value={addApproverInsertAfter.toString()}
|
value={addApproverInsertAfter.toString()}
|
||||||
onValueChange={(value) => setAddApproverInsertAfter(Number(value))}
|
onValueChange={(value) => setAddApproverInsertAfter(Number(value))}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-11 border-gray-300">
|
<SelectTrigger className="h-11 border-gray-300">
|
||||||
@ -1211,7 +1232,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
<p className="text-xs text-amber-600 font-medium">
|
<p className="text-xs text-amber-600 font-medium">
|
||||||
⚠️ Additional approvers cannot be added after "Requestor Claim Approval" as it is considered final.
|
⚠️ Additional approvers cannot be added after "Requestor Claim Approval" as it is considered final.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Max Approval Levels Note */}
|
{/* Max Approval Levels Note */}
|
||||||
{maxApprovalLevels && (
|
{maxApprovalLevels && (
|
||||||
<p className="text-xs text-gray-600 mt-2">
|
<p className="text-xs text-gray-600 mt-2">
|
||||||
@ -1290,7 +1311,7 @@ export function ClaimApproverSelectionStep({
|
|||||||
className="pl-10 h-11 border-gray-300"
|
className="pl-10 h-11 border-gray-300"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Search Results Dropdown */}
|
{/* Search Results Dropdown */}
|
||||||
{(isSearchingApprover || addApproverSearchResults.length > 0) && (
|
{(isSearchingApprover || addApproverSearchResults.length > 0) && (
|
||||||
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg max-h-60 overflow-auto">
|
<div className="absolute left-0 right-0 top-full mt-1 z-50 border rounded-md bg-white shadow-lg max-h-60 overflow-auto">
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import { toast } from 'sonner';
|
|||||||
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, sendCreditNoteToDealer } from '@/services/dealerClaimApi';
|
import { submitProposal, updateIODetails, submitCompletion, updateEInvoice, sendCreditNoteToDealer } from '@/services/dealerClaimApi';
|
||||||
import { getWorkflowDetails, approveLevel, rejectLevel, handleInitiatorAction, getWorkflowHistory } from '@/services/workflowApi';
|
import { getWorkflowDetails, approveLevel, rejectLevel, handleInitiatorAction, getWorkflowHistory } from '@/services/workflowApi';
|
||||||
import { uploadDocument } from '@/services/documentApi';
|
import { uploadDocument } from '@/services/documentApi';
|
||||||
|
import { TokenManager } from '@/utils/tokenManager';
|
||||||
|
|
||||||
interface DealerClaimWorkflowTabProps {
|
interface DealerClaimWorkflowTabProps {
|
||||||
request: any;
|
request: any;
|
||||||
@ -69,6 +70,7 @@ interface WorkflowStep {
|
|||||||
};
|
};
|
||||||
einvoiceUrl?: string;
|
einvoiceUrl?: string;
|
||||||
emailTemplateUrl?: string;
|
emailTemplateUrl?: string;
|
||||||
|
levelName?: string;
|
||||||
versionHistory?: {
|
versionHistory?: {
|
||||||
current: any;
|
current: any;
|
||||||
previous: any;
|
previous: any;
|
||||||
@ -329,28 +331,50 @@ export function DealerClaimWorkflowTab({
|
|||||||
// Step title and description mapping based on actual step number (not array index)
|
// Step title and description mapping based on actual step number (not array index)
|
||||||
// This handles cases where approvers are added between steps
|
// This handles cases where approvers are added between steps
|
||||||
const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
const getStepTitle = (stepNumber: number, levelName?: string, approverName?: string): string => {
|
||||||
|
// Check if this is a legacy workflow (8 steps) or new workflow (5 steps)
|
||||||
|
// Legacy flows have system steps (Activity, E-Invoice, Credit Note) as approval levels
|
||||||
|
const isLegacyFlow = (request?.totalLevels || 0) > 5 || (request?.approvalLevels?.length || 0) > 5;
|
||||||
|
|
||||||
// Use levelName from backend if available (most accurate)
|
// Use levelName from backend if available (most accurate)
|
||||||
// Check if it's an "Additional Approver" - this indicates a dynamically added approver
|
// Check if it's an "Additional Approver" - this indicates a dynamically added approver
|
||||||
if (levelName && levelName.trim()) {
|
if (levelName && levelName.trim()) {
|
||||||
|
const levelNameLower = levelName.toLowerCase();
|
||||||
|
|
||||||
// If it starts with "Additional Approver", use it as-is (it's already formatted)
|
// If it starts with "Additional Approver", use it as-is (it's already formatted)
|
||||||
if (levelName.toLowerCase().includes('additional approver')) {
|
if (levelNameLower.includes('additional approver')) {
|
||||||
|
return levelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If levelName is NOT generic "Step X", return it
|
||||||
|
// This fixes the issue where backend sends "Step 1" instead of "Dealer Proposal Submission"
|
||||||
|
if (!/^step\s+\d+$/i.test(levelName)) {
|
||||||
return levelName;
|
return levelName;
|
||||||
}
|
}
|
||||||
// Otherwise use the levelName from backend (preserved from original step)
|
|
||||||
return levelName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to mapping based on step number
|
// Fallback to mapping based on step number and flow version
|
||||||
const stepTitleMap: Record<number, string> = {
|
const stepTitleMap: Record<number, string> = isLegacyFlow
|
||||||
1: 'Dealer - Proposal Submission',
|
? {
|
||||||
2: 'Requestor Evaluation & Confirmation',
|
// Legacy 8-step flow
|
||||||
3: 'Department Lead Approval',
|
1: 'Dealer - Proposal Submission',
|
||||||
4: 'Activity Creation',
|
2: 'Requestor Evaluation & Confirmation',
|
||||||
5: 'Dealer - Completion Documents',
|
3: 'Department Lead Approval',
|
||||||
6: 'Requestor - Claim Approval',
|
4: 'Activity Creation',
|
||||||
7: 'E-Invoice Generation',
|
5: 'Dealer - Completion Documents',
|
||||||
8: 'Credit Note from SAP',
|
6: 'Requestor - Claim Approval',
|
||||||
};
|
7: 'E-Invoice Generation',
|
||||||
|
8: 'Credit Note from SAP',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// New 5-step flow
|
||||||
|
1: 'Dealer - Proposal Submission',
|
||||||
|
2: 'Requestor Evaluation & Confirmation',
|
||||||
|
3: 'Department Lead Approval',
|
||||||
|
4: 'Dealer - Completion Documents',
|
||||||
|
5: 'Requestor - Claim Approval',
|
||||||
|
6: 'E-Invoice Generation',
|
||||||
|
7: 'Credit Note from SAP',
|
||||||
|
};
|
||||||
|
|
||||||
// If step number exists in map, use it
|
// If step number exists in map, use it
|
||||||
if (stepTitleMap[stepNumber]) {
|
if (stepTitleMap[stepNumber]) {
|
||||||
@ -377,6 +401,9 @@ export function DealerClaimWorkflowTab({
|
|||||||
return `Additional approver will review and approve this request.`;
|
return `Additional approver will review and approve this request.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a legacy workflow (8 steps) or new workflow (5 steps)
|
||||||
|
const isLegacyFlow = (request?.totalLevels || 0) > 5 || (request?.approvalLevels?.length || 0) > 5;
|
||||||
|
|
||||||
// Use levelName to determine description (handles shifted steps correctly)
|
// Use levelName to determine description (handles shifted steps correctly)
|
||||||
// This ensures descriptions shift with their steps when approvers are added
|
// This ensures descriptions shift with their steps when approvers are added
|
||||||
if (levelName && levelName.trim()) {
|
if (levelName && levelName.trim()) {
|
||||||
@ -392,6 +419,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
if (levelNameLower.includes('department lead')) {
|
if (levelNameLower.includes('department lead')) {
|
||||||
return 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)';
|
return 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)';
|
||||||
}
|
}
|
||||||
|
// Re-added for legacy support
|
||||||
if (levelNameLower.includes('activity creation')) {
|
if (levelNameLower.includes('activity creation')) {
|
||||||
return 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.';
|
return 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.';
|
||||||
}
|
}
|
||||||
@ -401,25 +429,37 @@ export function DealerClaimWorkflowTab({
|
|||||||
if (levelNameLower.includes('requestor') && (levelNameLower.includes('claim') || levelNameLower.includes('approval'))) {
|
if (levelNameLower.includes('requestor') && (levelNameLower.includes('claim') || levelNameLower.includes('approval'))) {
|
||||||
return 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.';
|
return 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.';
|
||||||
}
|
}
|
||||||
if (levelNameLower.includes('e-invoice') || levelNameLower.includes('invoice generation')) {
|
if (levelNameLower.includes('e-invoice') || levelNameLower.includes('invoice generation') || levelNameLower.includes('dms')) {
|
||||||
return 'E-invoice will be generated through DMS.';
|
return 'E-Invoice will be generated upon settlement initiation.';
|
||||||
}
|
}
|
||||||
if (levelNameLower.includes('credit note') || levelNameLower.includes('sap')) {
|
if (levelNameLower.includes('credit note') || levelNameLower.includes('sap')) {
|
||||||
return 'Got credit note from SAP. Review and send to dealer to complete the claim management process.';
|
return 'Got credit note from SAP. Review and send to dealer to complete the claim management process.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to step number mapping (for backwards compatibility)
|
// Fallback to step number mapping depending on flow version
|
||||||
const stepDescriptionMap: Record<number, string> = {
|
const stepDescriptionMap: Record<number, string> = isLegacyFlow
|
||||||
1: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
? {
|
||||||
2: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
// Legacy 8-step flow
|
||||||
3: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
1: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
||||||
4: 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.',
|
2: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
||||||
5: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
3: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
||||||
6: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
4: 'Activity is created. Activity confirmation email is auto-triggered to dealer / requestor / Lead. IO confirmation to be made.',
|
||||||
7: 'E-invoice will be generated through DMS.',
|
5: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
||||||
8: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
6: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
||||||
};
|
7: 'E-Invoice will be generated upon settlement initiation.',
|
||||||
|
8: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// New 5-step flow
|
||||||
|
1: 'Dealer submits the proposal for the activity with comments including proposal document with requested details, cost break-up, timeline for closure, and other requests',
|
||||||
|
2: 'Requestor evaluates the request and confirms with comments. Decision point: Confirms? (YES → Continue to Dept Lead / NO → Request is cancelled)',
|
||||||
|
3: 'Department Lead approval. Decision point: Approved? (YES → Budget is blocked in the respective IO for the activity / NO → More clarification required → Request is cancelled)',
|
||||||
|
4: 'Dealer submits the necessary documents upon completion of the activity including document attachments (Zip Folder) and brief description',
|
||||||
|
5: 'Requestor approves the claim in full or can modify the amount. If more information is required, can request additional details from dealer.',
|
||||||
|
6: 'E-Invoice will be generated upon settlement initiation.',
|
||||||
|
7: 'Got credit note from SAP. Review and send to dealer to complete the claim management process.',
|
||||||
|
};
|
||||||
|
|
||||||
if (stepDescriptionMap[stepNumber]) {
|
if (stepDescriptionMap[stepNumber]) {
|
||||||
return stepDescriptionMap[stepNumber];
|
return stepDescriptionMap[stepNumber];
|
||||||
@ -845,13 +885,26 @@ export function DealerClaimWorkflowTab({
|
|||||||
await uploadDocument(file, requestId, 'SUPPORTING');
|
await uploadDocument(file, requestId, 'SUPPORTING');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit proposal using dealer claim API
|
// Submit proposal using dealer claim API (calculate total from inclusive item totals)
|
||||||
const totalBudget = data.costBreakup.reduce((sum, item) => sum + item.amount, 0);
|
const totalBudget = data.costBreakup.reduce((sum, item: any) => sum + (item.totalAmt || item.amount || 0), 0);
|
||||||
await submitProposal(requestId, {
|
await submitProposal(requestId, {
|
||||||
proposalDocument: data.proposalDocument || undefined,
|
proposalDocument: data.proposalDocument || undefined,
|
||||||
costBreakup: data.costBreakup.map(item => ({
|
costBreakup: data.costBreakup.map((item: any) => ({
|
||||||
description: item.description,
|
description: item.description,
|
||||||
amount: item.amount,
|
amount: item.amount,
|
||||||
|
gstRate: item.gstRate,
|
||||||
|
gstAmt: item.gstAmt,
|
||||||
|
cgstRate: item.cgstRate,
|
||||||
|
cgstAmt: item.cgstAmt,
|
||||||
|
sgstRate: item.sgstRate,
|
||||||
|
sgstAmt: item.sgstAmt,
|
||||||
|
igstRate: item.igstRate,
|
||||||
|
igstAmt: item.igstAmt,
|
||||||
|
utgstRate: item.utgstRate,
|
||||||
|
utgstAmt: item.utgstAmt,
|
||||||
|
cessRate: item.cessRate,
|
||||||
|
cessAmt: item.cessAmt,
|
||||||
|
totalAmt: item.totalAmt
|
||||||
})),
|
})),
|
||||||
totalEstimatedBudget: totalBudget,
|
totalEstimatedBudget: totalBudget,
|
||||||
expectedCompletionDate: data.expectedCompletionDate,
|
expectedCompletionDate: data.expectedCompletionDate,
|
||||||
@ -1106,10 +1159,23 @@ export function DealerClaimWorkflowTab({
|
|||||||
|
|
||||||
const requestId = request.id || request.requestId;
|
const requestId = request.id || request.requestId;
|
||||||
|
|
||||||
// Transform expense items to match API format
|
// Transform expense items to match API format (include GST fields)
|
||||||
const closedExpenses = data.closedExpenses.map(item => ({
|
const closedExpenses = data.closedExpenses.map((item: any) => ({
|
||||||
description: item.description,
|
description: item.description,
|
||||||
amount: item.amount,
|
amount: item.amount,
|
||||||
|
gstRate: item.gstRate,
|
||||||
|
gstAmt: item.gstAmt,
|
||||||
|
cgstRate: item.cgstRate,
|
||||||
|
cgstAmt: item.cgstAmt,
|
||||||
|
sgstRate: item.sgstRate,
|
||||||
|
sgstAmt: item.sgstAmt,
|
||||||
|
igstRate: item.igstRate,
|
||||||
|
igstAmt: item.igstAmt,
|
||||||
|
utgstRate: item.utgstRate,
|
||||||
|
utgstAmt: item.utgstAmt,
|
||||||
|
cessRate: item.cessRate,
|
||||||
|
cessAmt: item.cessAmt,
|
||||||
|
totalAmt: item.totalAmt
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Submit completion documents using dealer claim API
|
// Submit completion documents using dealer claim API
|
||||||
@ -1145,7 +1211,7 @@ export function DealerClaimWorkflowTab({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle DMS push (Step 6)
|
// Handle E-Invoice generation (Step 6)
|
||||||
const handleDMSPush = async (_comments: string) => {
|
const handleDMSPush = async (_comments: string) => {
|
||||||
try {
|
try {
|
||||||
if (!request?.id && !request?.requestId) {
|
if (!request?.id && !request?.requestId) {
|
||||||
@ -1162,11 +1228,11 @@ export function DealerClaimWorkflowTab({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Activity is logged by backend service - no need to create work note
|
// Activity is logged by backend service - no need to create work note
|
||||||
toast.success('Pushed to DMS successfully. E-invoice will be generated automatically.');
|
toast.success('E-Invoice generation initiated successfully.');
|
||||||
handleRefresh();
|
handleRefresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DealerClaimWorkflowTab] Error pushing to DMS:', error);
|
console.error('[DealerClaimWorkflowTab] Error generating e-invoice:', error);
|
||||||
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to push to DMS. Please try again.';
|
const errorMessage = error?.response?.data?.message || error?.message || 'Failed to generate e-invoice. Please try again.';
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -1381,6 +1447,31 @@ export function DealerClaimWorkflowTab({
|
|||||||
loadCompletionDocuments();
|
loadCompletionDocuments();
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
|
const handlePreviewInvoice = async () => {
|
||||||
|
try {
|
||||||
|
const requestId = request.id || request.requestId;
|
||||||
|
if (!requestId) {
|
||||||
|
toast.error('Request ID not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if invoice exists
|
||||||
|
if (!request.invoice && !request.irn) {
|
||||||
|
toast.error('Invoice not generated yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = TokenManager.getAccessToken();
|
||||||
|
// Construct API URL for PDF preview
|
||||||
|
const previewUrl = `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1'}/dealer-claims/${requestId}/e-invoice/pdf?token=${token}`;
|
||||||
|
|
||||||
|
window.open(previewUrl, '_blank');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to preview invoice:', error);
|
||||||
|
toast.error('Failed to open invoice preview');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get dealer and activity info
|
// Get dealer and activity info
|
||||||
const dealerName = request?.claimDetails?.dealerName ||
|
const dealerName = request?.claimDetails?.dealerName ||
|
||||||
request?.dealerInfo?.name ||
|
request?.dealerInfo?.name ||
|
||||||
@ -1513,6 +1604,23 @@ export function DealerClaimWorkflowTab({
|
|||||||
<Download className="w-3.5 h-3.5 text-green-600" />
|
<Download className="w-3.5 h-3.5 text-green-600" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{/* Invoice Preview Button (Requestor Claim Approval) */}
|
||||||
|
{(() => {
|
||||||
|
const isRequestorClaimStep = (step.levelName || step.title || '').toLowerCase().includes('requestor claim') ||
|
||||||
|
(step.levelName || step.title || '').toLowerCase().includes('requestor - claim');
|
||||||
|
const hasInvoice = request?.invoice || (request?.irn && step.status === 'approved');
|
||||||
|
return isRequestorClaimStep && hasInvoice && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-amber-100"
|
||||||
|
title="Preview Invoice"
|
||||||
|
onClick={handlePreviewInvoice}
|
||||||
|
>
|
||||||
|
<Receipt className="w-3.5 h-3.5 text-amber-600" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">{step.approver}</p>
|
<p className="text-sm text-gray-600">{step.approver}</p>
|
||||||
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
<p className="text-sm text-gray-500 mt-2 italic">{step.description}</p>
|
||||||
@ -1721,10 +1829,10 @@ export function DealerClaimWorkflowTab({
|
|||||||
|
|
||||||
{/* Current Approver - Time Tracking */}
|
{/* Current Approver - Time Tracking */}
|
||||||
<div className={`border rounded-lg p-3 ${isPaused ? 'bg-gray-100 border-gray-300' :
|
<div className={`border rounded-lg p-3 ${isPaused ? 'bg-gray-100 border-gray-300' :
|
||||||
(approval.sla.percentageUsed || 0) >= 100 ? 'bg-red-50 border-red-200' :
|
(approval.sla.percentageUsed || 0) >= 100 ? 'bg-red-50 border-red-200' :
|
||||||
(approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' :
|
(approval.sla.percentageUsed || 0) >= 75 ? 'bg-orange-50 border-orange-200' :
|
||||||
(approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' :
|
(approval.sla.percentageUsed || 0) >= 50 ? 'bg-amber-50 border-amber-200' :
|
||||||
'bg-green-50 border-green-200'
|
'bg-green-50 border-green-200'
|
||||||
}`}>
|
}`}>
|
||||||
<p className="text-xs font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
<p className="text-xs font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||||
<Clock className="w-4 h-4" />
|
<Clock className="w-4 h-4" />
|
||||||
@ -1856,25 +1964,25 @@ export function DealerClaimWorkflowTab({
|
|||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Activity className="w-4 h-4 text-purple-600" />
|
<Activity className="w-4 h-4 text-purple-600" />
|
||||||
<p className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
<p className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
||||||
DMS Processing Details
|
E-Invoice & Settlement Details
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-gray-600">DMS Number:</span>
|
<span className="text-xs text-gray-600">Settlement ID:</span>
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
{step.dmsDetails.dmsNumber}
|
{step.dmsDetails.dmsNumber}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{step.dmsDetails.dmsRemarks && (
|
{step.dmsDetails.dmsRemarks && (
|
||||||
<div className="pt-1.5 border-t border-purple-100">
|
<div className="pt-1.5 border-t border-purple-100">
|
||||||
<p className="text-xs text-gray-600 mb-1">DMS Remarks:</p>
|
<p className="text-xs text-gray-600 mb-1">Settlement Remarks:</p>
|
||||||
<p className="text-sm text-gray-900">{step.dmsDetails.dmsRemarks}</p>
|
<p className="text-sm text-gray-900">{step.dmsDetails.dmsRemarks}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{step.dmsDetails.pushedAt && (
|
{step.dmsDetails.pushedAt && (
|
||||||
<div className="pt-1.5 border-t border-purple-100 text-xs text-gray-500">
|
<div className="pt-1.5 border-t border-purple-100 text-xs text-gray-500">
|
||||||
Pushed by {step.dmsDetails.pushedBy} on{' '}
|
Initiated by {step.dmsDetails.pushedBy} on{' '}
|
||||||
{formatDateSafe(step.dmsDetails.pushedAt)}
|
{formatDateSafe(step.dmsDetails.pushedAt)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1902,33 +2010,51 @@ export function DealerClaimWorkflowTab({
|
|||||||
})() && (
|
})() && (
|
||||||
<div className="mt-4 flex gap-2">
|
<div className="mt-4 flex gap-2">
|
||||||
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
|
{/* Step 1: Submit Proposal Button - Only for dealer or step 1 approver */}
|
||||||
{step.step === 1 && (isDealer || isStep1Approver) && (
|
{(() => {
|
||||||
<Button
|
// Check if this is Step 1 (Dealer Proposal Submission)
|
||||||
className="bg-purple-600 hover:bg-purple-700"
|
// Use levelName match or fallback to step 1
|
||||||
onClick={() => {
|
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
setShowProposalModal(true);
|
const isProposalStep = step.step === 1 ||
|
||||||
}}
|
levelName.includes('proposal') ||
|
||||||
>
|
levelName.includes('submission');
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
|
||||||
Submit Proposal
|
return isProposalStep && (isDealer || isStep1Approver);
|
||||||
</Button>
|
})() && (
|
||||||
)}
|
<Button
|
||||||
|
className="bg-purple-600 hover:bg-purple-700"
|
||||||
|
onClick={() => {
|
||||||
|
setShowProposalModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
Submit Proposal
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
||||||
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
||||||
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
||||||
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
{/* Use initiatorStepNumber to handle cases where approvers are added between steps */}
|
||||||
{step.step === initiatorStepNumber && (isInitiator || isStep2Approver) && (
|
{/* Step 2 (or shifted step): Review Request - Only for initiator or step approver */}
|
||||||
<Button
|
{(() => {
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
// Check if this is the Requestor Evaluation step
|
||||||
onClick={() => {
|
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
setShowApprovalModal(true);
|
const isEvaluationStep = levelName.includes('requestor evaluation') ||
|
||||||
}}
|
levelName.includes('confirmation') ||
|
||||||
>
|
step.step === initiatorStepNumber; // Fallback
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
Review Request
|
return isEvaluationStep && (isInitiator || isStep2Approver);
|
||||||
</Button>
|
})() && (
|
||||||
)}
|
<Button
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
|
onClick={() => {
|
||||||
|
setShowApprovalModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
Review Request
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Initiator Action Step: Show action buttons (REVISE, REOPEN) - Direct actions, no modal */}
|
{/* Initiator Action Step: Show action buttons (REVISE, REOPEN) - Direct actions, no modal */}
|
||||||
{(() => {
|
{(() => {
|
||||||
@ -2080,20 +2206,26 @@ export function DealerClaimWorkflowTab({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Activity className="w-4 h-4 mr-2" />
|
<Activity className="w-4 h-4 mr-2" />
|
||||||
Push to DMS
|
Generate E-Invoice & Sync
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Step 8: View & Send Credit Note - Only for finance approver or step 8 approver */}
|
{/* Step 8: View & Send Credit Note - Only for finance approver or step 8 approver */}
|
||||||
{step.step === 8 && (() => {
|
{(() => {
|
||||||
const step8Level = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === 8);
|
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
const step8ApproverEmail = (step8Level?.approverEmail || '').toLowerCase();
|
// Check for "Credit Note" or "SAP" in level name, or fallback to step 8 if it's the last step
|
||||||
const isStep8Approver = step8ApproverEmail && userEmail === step8ApproverEmail;
|
const isCreditNoteStep = levelName.includes('credit note') ||
|
||||||
|
levelName.includes('sap') ||
|
||||||
|
(step.step === 8 && !levelName.includes('additional'));
|
||||||
|
|
||||||
|
const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase();
|
||||||
|
const isStepApprover = stepApproverEmail && userEmail === stepApproverEmail;
|
||||||
// Also check if user has finance role
|
// Also check if user has finance role
|
||||||
const userRole = (user as any)?.role?.toUpperCase() || '';
|
const userRole = (user as any)?.role?.toUpperCase() || '';
|
||||||
const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN';
|
const isFinanceUser = userRole === 'FINANCE' || userRole === 'ADMIN';
|
||||||
return isStep8Approver || isFinanceUser;
|
|
||||||
|
return isCreditNoteStep && (isStepApprover || isFinanceUser);
|
||||||
})() && (
|
})() && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-green-600 hover:bg-green-700"
|
className="bg-green-600 hover:bg-green-700"
|
||||||
@ -2113,35 +2245,21 @@ export function DealerClaimWorkflowTab({
|
|||||||
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
const levelName = (stepLevel?.levelName || step.title || '').toLowerCase();
|
||||||
const isAdditionalApprover = levelName.includes('additional approver');
|
const isAdditionalApprover = levelName.includes('additional approver');
|
||||||
|
|
||||||
|
// Check if this step doesn't have any of the specific workflow action buttons above
|
||||||
// Check if this step doesn't have any of the specific workflow action buttons above
|
// Check if this step doesn't have any of the specific workflow action buttons above
|
||||||
const hasSpecificWorkflowAction =
|
const hasSpecificWorkflowAction =
|
||||||
step.step === 1 ||
|
// Proposal
|
||||||
step.step === initiatorStepNumber ||
|
(step.step === 1 || levelName.includes('proposal') || levelName.includes('submission')) ||
|
||||||
(() => {
|
// Evaluation
|
||||||
const deptLeadStepLevel = approvalFlow.find((l: any) => {
|
(levelName.includes('requestor evaluation') || levelName.includes('confirmation')) ||
|
||||||
const ln = (l.levelName || '').toLowerCase();
|
// Dept Lead
|
||||||
return ln.includes('department lead');
|
levelName.includes('department lead') ||
|
||||||
});
|
// Dealer Completion
|
||||||
return deptLeadStepLevel &&
|
(levelName.includes('dealer completion') || levelName.includes('completion documents')) ||
|
||||||
(step.step === (deptLeadStepLevel.step || deptLeadStepLevel.levelNumber || deptLeadStepLevel.level_number));
|
// Requestor Claim
|
||||||
})() ||
|
(levelName.includes('requestor claim') || levelName.includes('requestor - claim')) ||
|
||||||
(() => {
|
// Credit Note
|
||||||
const stepLevel = approvalFlow.find((l: any) => (l.step || l.levelNumber || l.level_number) === step.step);
|
(levelName.includes('credit note') || levelName.includes('sap'));
|
||||||
const stepApproverEmail = (stepLevel?.approverEmail || '').toLowerCase();
|
|
||||||
const isDealerForThisStep = isDealer && stepApproverEmail === dealerEmail;
|
|
||||||
const ln = (stepLevel?.levelName || step.title || '').toLowerCase();
|
|
||||||
const isDealerCompletionStep = ln.includes('dealer completion') || ln.includes('completion documents');
|
|
||||||
return isDealerForThisStep && isDealerCompletionStep;
|
|
||||||
})() ||
|
|
||||||
(() => {
|
|
||||||
const requestorClaimStepLevel = approvalFlow.find((l: any) => {
|
|
||||||
const ln = (l.levelName || '').toLowerCase();
|
|
||||||
return ln.includes('requestor claim') || ln.includes('requestor - claim');
|
|
||||||
});
|
|
||||||
return requestorClaimStepLevel &&
|
|
||||||
(step.step === (requestorClaimStepLevel.step || requestorClaimStepLevel.levelNumber || requestorClaimStepLevel.level_number));
|
|
||||||
})() ||
|
|
||||||
step.step === 8;
|
|
||||||
|
|
||||||
// Show "Review Request" button for additional approvers or steps without specific workflow actions
|
// Show "Review Request" button for additional approvers or steps without specific workflow actions
|
||||||
// Similar to the requestor approval step
|
// Similar to the requestor approval step
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* ProcessDetailsCard Component
|
* ProcessDetailsCard Component
|
||||||
* Displays process-related details: IO Number, DMS Number, Claim Amount, and Budget Breakdowns
|
* Displays process-related details: IO Number, E-Invoice, Claim Amount, and Budget Breakdowns
|
||||||
* Visibility controlled by user role
|
* Visibility controlled by user role
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -172,21 +172,18 @@ export function ProcessDetailsCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* DMS Details */}
|
{/* E-Invoice Details */}
|
||||||
{visibility.showDMSDetails && dmsDetails && (
|
{visibility.showDMSDetails && dmsDetails && (
|
||||||
<div className="bg-white rounded-lg p-3 border border-purple-200">
|
<div className="bg-white rounded-lg p-3 border border-purple-200">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Activity className="w-4 h-4 text-purple-600" />
|
<Activity className="w-4 h-4 text-purple-600" />
|
||||||
<Label className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
<Label className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
||||||
DMS & E-Invoice Details
|
E-Invoice Details
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 mb-2">
|
<div className="grid grid-cols-2 gap-3 mb-2">
|
||||||
<div>
|
|
||||||
<p className="text-[10px] text-gray-500 uppercase">DMS Number</p>
|
|
||||||
<p className="font-bold text-sm text-gray-900">{dmsDetails.dmsNumber || 'N/A'}</p>
|
|
||||||
</div>
|
|
||||||
{dmsDetails.ackNo && (
|
{dmsDetails.ackNo && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] text-gray-500 uppercase">Ack No</p>
|
<p className="text-[10px] text-gray-500 uppercase">Ack No</p>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ interface ProposalCostItem {
|
|||||||
interface ProposalDetails {
|
interface ProposalDetails {
|
||||||
costBreakup: ProposalCostItem[];
|
costBreakup: ProposalCostItem[];
|
||||||
estimatedBudgetTotal?: number | null;
|
estimatedBudgetTotal?: number | null;
|
||||||
|
totalEstimatedBudget?: number | null;
|
||||||
timelineForClosure?: string | null;
|
timelineForClosure?: string | null;
|
||||||
dealerComments?: string | null;
|
dealerComments?: string | null;
|
||||||
submittedOn?: string | null;
|
submittedOn?: string | null;
|
||||||
@ -35,8 +36,9 @@ interface ProposalDetailsCardProps {
|
|||||||
export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
|
export function ProposalDetailsCard({ proposalDetails, className }: ProposalDetailsCardProps) {
|
||||||
// Calculate estimated total from costBreakup if not provided
|
// Calculate estimated total from costBreakup if not provided
|
||||||
const calculateEstimatedTotal = () => {
|
const calculateEstimatedTotal = () => {
|
||||||
if (proposalDetails.estimatedBudgetTotal !== undefined && proposalDetails.estimatedBudgetTotal !== null) {
|
const total = proposalDetails.totalEstimatedBudget ?? proposalDetails.estimatedBudgetTotal;
|
||||||
return proposalDetails.estimatedBudgetTotal;
|
if (total !== undefined && total !== null) {
|
||||||
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate sum from costBreakup items
|
// Calculate sum from costBreakup items
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
.dms-push-modal {
|
.settlement-push-modal {
|
||||||
width: 90vw !important;
|
width: 90vw !important;
|
||||||
max-width: 90vw !important;
|
max-width: 1000px !important;
|
||||||
|
min-width: 320px !important;
|
||||||
max-height: 95vh !important;
|
max-height: 95vh !important;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile responsive */
|
/* Mobile responsive */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.dms-push-modal {
|
.settlement-push-modal {
|
||||||
width: 95vw !important;
|
width: 95vw !important;
|
||||||
max-width: 95vw !important;
|
max-width: 95vw !important;
|
||||||
max-height: 95vh !important;
|
max-height: 95vh !important;
|
||||||
@ -15,25 +19,48 @@
|
|||||||
|
|
||||||
/* Tablet and small desktop */
|
/* Tablet and small desktop */
|
||||||
@media (min-width: 641px) and (max-width: 1023px) {
|
@media (min-width: 641px) and (max-width: 1023px) {
|
||||||
.dms-push-modal {
|
.settlement-push-modal {
|
||||||
width: 90vw !important;
|
width: 90vw !important;
|
||||||
max-width: 90vw !important;
|
max-width: 900px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Large screens - fixed max-width for better readability */
|
/* Scrollable content area */
|
||||||
@media (min-width: 1024px) {
|
.settlement-push-modal .flex-1 {
|
||||||
.dms-push-modal {
|
overflow-y: auto;
|
||||||
width: 90vw !important;
|
padding-right: 4px;
|
||||||
max-width: 1000px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra large screens */
|
/* Custom scrollbar for the modal content */
|
||||||
@media (min-width: 1536px) {
|
.settlement-push-modal .flex-1::-webkit-scrollbar {
|
||||||
.dms-push-modal {
|
width: 6px;
|
||||||
width: 90vw !important;
|
|
||||||
max-width: 1000px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settlement-push-modal .flex-1::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-push-modal .flex-1::-webkit-scrollbar-thumb {
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-push-modal .flex-1::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-dialog {
|
||||||
|
width: 95vw !important;
|
||||||
|
max-width: 1200px !important;
|
||||||
|
max-height: 95vh !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,7 @@ import { downloadDocument } from '@/services/workflowApi';
|
|||||||
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
|
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
|
||||||
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
|
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
|
||||||
import { getSocket, joinUserRoom } from '@/utils/socket';
|
import { getSocket, joinUserRoom } from '@/utils/socket';
|
||||||
import { isClaimManagementRequest } from '@/utils/claimRequestUtils';
|
|
||||||
|
|
||||||
// Dealer Claim Components (import from index to get properly aliased exports)
|
// Dealer Claim Components (import from index to get properly aliased exports)
|
||||||
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
import { DealerClaimOverviewTab, DealerClaimWorkflowTab, IOTab } from '../index';
|
||||||
@ -153,7 +153,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
|
|
||||||
// Determine if user is department lead (find dynamically by levelName, not hardcoded step number)
|
// Determine if user is department lead (find dynamically by levelName, not hardcoded step number)
|
||||||
const currentUserId = (user as any)?.userId || '';
|
const currentUserId = (user as any)?.userId || '';
|
||||||
|
|
||||||
// IO tab visibility for dealer claims
|
// IO tab visibility for dealer claims
|
||||||
// Show IO tab for initiator (Requestor Evaluation level) - initiator can now fetch and block IO
|
// Show IO tab for initiator (Requestor Evaluation level) - initiator can now fetch and block IO
|
||||||
const showIOTab = isInitiator;
|
const showIOTab = isInitiator;
|
||||||
@ -177,7 +177,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
|
|
||||||
// State to temporarily store approval level for modal (used for additional approvers)
|
// State to temporarily store approval level for modal (used for additional approvers)
|
||||||
const [temporaryApprovalLevel, setTemporaryApprovalLevel] = useState<any>(null);
|
const [temporaryApprovalLevel, setTemporaryApprovalLevel] = useState<any>(null);
|
||||||
|
|
||||||
// Use temporary level if set, otherwise use currentApprovalLevel
|
// Use temporary level if set, otherwise use currentApprovalLevel
|
||||||
const effectiveApprovalLevel = temporaryApprovalLevel || currentApprovalLevel;
|
const effectiveApprovalLevel = temporaryApprovalLevel || currentApprovalLevel;
|
||||||
|
|
||||||
@ -220,7 +220,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
// Check both lowercase and uppercase status values
|
// Check both lowercase and uppercase status values
|
||||||
const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
|
const requestStatus = (request?.status || apiRequest?.status || '').toLowerCase();
|
||||||
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator;
|
const needsClosure = (requestStatus === 'approved' || requestStatus === 'rejected') && isInitiator;
|
||||||
|
|
||||||
// Closure check completed
|
// Closure check completed
|
||||||
const {
|
const {
|
||||||
conclusionRemark,
|
conclusionRemark,
|
||||||
@ -335,7 +335,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
try {
|
try {
|
||||||
setLoadingSummary(true);
|
setLoadingSummary(true);
|
||||||
const summary = await getSummaryByRequestId(apiRequest.requestId);
|
const summary = await getSummaryByRequestId(apiRequest.requestId);
|
||||||
|
|
||||||
if (summary?.summaryId) {
|
if (summary?.summaryId) {
|
||||||
setSummaryId(summary.summaryId);
|
setSummaryId(summary.summaryId);
|
||||||
try {
|
try {
|
||||||
@ -376,9 +376,9 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
|
|
||||||
const notifRequestId = notif.requestId || notif.request_id;
|
const notifRequestId = notif.requestId || notif.request_id;
|
||||||
const notifRequestNumber = notif.metadata?.requestNumber || notif.metadata?.request_number;
|
const notifRequestNumber = notif.metadata?.requestNumber || notif.metadata?.request_number;
|
||||||
if (notifRequestId !== apiRequest.requestId &&
|
if (notifRequestId !== apiRequest.requestId &&
|
||||||
notifRequestNumber !== requestIdentifier &&
|
notifRequestNumber !== requestIdentifier &&
|
||||||
notifRequestNumber !== apiRequest.requestNumber) return;
|
notifRequestNumber !== apiRequest.requestNumber) return;
|
||||||
|
|
||||||
// Check for credit note metadata
|
// Check for credit note metadata
|
||||||
if (notif.metadata?.creditNoteNumber || notif.metadata?.credit_note_number) {
|
if (notif.metadata?.creditNoteNumber || notif.metadata?.credit_note_number) {
|
||||||
@ -427,15 +427,15 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
{accessDenied.message}
|
{accessDenied.message}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3 justify-center">
|
<div className="flex gap-3 justify-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onBack || (() => window.history.back())}
|
onClick={onBack || (() => window.history.back())}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => window.location.href = '/dashboard'}
|
onClick={() => window.location.href = '/dashboard'}
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
@ -460,15 +460,15 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
The dealer claim request you're looking for doesn't exist or may have been deleted.
|
The dealer claim request you're looking for doesn't exist or may have been deleted.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3 justify-center">
|
<div className="flex gap-3 justify-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onBack || (() => window.history.back())}
|
onClick={onBack || (() => window.history.back())}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-4 h-4" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => window.location.href = '/dashboard'}
|
onClick={() => window.location.href = '/dashboard'}
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
@ -598,8 +598,8 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
|
|
||||||
{isClosed && (
|
{isClosed && (
|
||||||
<TabsContent value="summary" className="mt-0" data-testid="summary-tab-content">
|
<TabsContent value="summary" className="mt-0" data-testid="summary-tab-content">
|
||||||
<SummaryTab
|
<SummaryTab
|
||||||
summary={summaryDetails}
|
summary={summaryDetails}
|
||||||
loading={loadingSummary}
|
loading={loadingSummary}
|
||||||
onShare={handleShareSummary}
|
onShare={handleShareSummary}
|
||||||
isInitiator={isInitiator}
|
isInitiator={isInitiator}
|
||||||
@ -673,7 +673,7 @@ function DealerClaimRequestDetailInner({ requestId: propRequestId, onBack, dynam
|
|||||||
request={request}
|
request={request}
|
||||||
isInitiator={isInitiator}
|
isInitiator={isInitiator}
|
||||||
isSpectator={isSpectator}
|
isSpectator={isSpectator}
|
||||||
currentApprovalLevel={isClaimManagementRequest(apiRequest) ? null : currentApprovalLevel}
|
currentApprovalLevel={currentApprovalLevel}
|
||||||
onAddApprover={() => setShowAddApproverModal(true)}
|
onAddApprover={() => setShowAddApproverModal(true)}
|
||||||
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
onAddSpectator={() => setShowAddSpectatorModal(true)}
|
||||||
onApprove={() => setShowApproveModal(true)}
|
onApprove={() => setShowApproveModal(true)}
|
||||||
|
|||||||
@ -175,14 +175,14 @@ export function mapToClaimManagementRequest(
|
|||||||
// Get closed expenses breakdown from new completionExpenses table
|
// Get closed expenses breakdown from new completionExpenses table
|
||||||
const closedExpensesBreakdown = Array.isArray(completionExpenses) && completionExpenses.length > 0
|
const closedExpensesBreakdown = Array.isArray(completionExpenses) && completionExpenses.length > 0
|
||||||
? completionExpenses.map((exp: any) => ({
|
? completionExpenses.map((exp: any) => ({
|
||||||
description: exp.description || exp.itemDescription || '',
|
description: exp.description || exp.itemDescription || exp.item_description || '',
|
||||||
amount: Number(exp.amount) || 0,
|
amount: Number(exp.amount) || 0,
|
||||||
gstRate: exp.gstRate,
|
gstRate: exp.gstRate ?? exp.gst_rate,
|
||||||
gstAmt: exp.gstAmt,
|
gstAmt: exp.gstAmt ?? exp.gst_amt,
|
||||||
cgstAmt: exp.cgstAmt,
|
cgstAmt: exp.cgstAmt ?? exp.cgst_amt,
|
||||||
sgstAmt: exp.sgstAmt,
|
sgstAmt: exp.sgstAmt ?? exp.sgst_amt,
|
||||||
igstAmt: exp.igstAmt,
|
igstAmt: exp.igstAmt ?? exp.igst_amt,
|
||||||
totalAmt: exp.totalAmt
|
totalAmt: exp.totalAmt ?? exp.total_amt
|
||||||
}))
|
}))
|
||||||
: (completionDetails?.closedExpenses ||
|
: (completionDetails?.closedExpenses ||
|
||||||
completionDetails?.closed_expenses ||
|
completionDetails?.closed_expenses ||
|
||||||
@ -232,14 +232,14 @@ export function mapToClaimManagementRequest(
|
|||||||
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
proposalDocumentUrl: proposalDetails.proposalDocumentUrl || proposalDetails.proposal_document_url,
|
||||||
costBreakup: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup)
|
costBreakup: Array.isArray(proposalDetails.costBreakup || proposalDetails.cost_breakup)
|
||||||
? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({
|
? (proposalDetails.costBreakup || proposalDetails.cost_breakup).map((item: any) => ({
|
||||||
description: item.description || '',
|
description: item.description || item.itemDescription || item.item_description || '',
|
||||||
amount: Number(item.amount) || 0,
|
amount: Number(item.amount) || 0,
|
||||||
gstRate: item.gstRate,
|
gstRate: item.gstRate ?? item.gst_rate,
|
||||||
gstAmt: item.gstAmt,
|
gstAmt: item.gstAmt ?? item.gst_amt,
|
||||||
cgstAmt: item.cgstAmt,
|
cgstAmt: item.cgstAmt ?? item.cgst_amt,
|
||||||
sgstAmt: item.sgstAmt,
|
sgstAmt: item.sgstAmt ?? item.sgst_amt,
|
||||||
igstAmt: item.igstAmt,
|
igstAmt: item.igstAmt ?? item.igst_amt,
|
||||||
totalAmt: item.totalAmt
|
totalAmt: item.totalAmt ?? item.total_amt
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
|
totalEstimatedBudget: proposalDetails.totalEstimatedBudget || proposalDetails.total_estimated_budget || 0,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user