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:
laxmanhalaki 2026-02-10 20:12:26 +05:30
parent 7ae9133b98
commit 80ed407cd8
8 changed files with 1101 additions and 933 deletions

View File

@ -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">

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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%;
}

View File

@ -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)}

View File

@ -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,