dealer claim history table recreated and enhanced the approval history versioning
This commit is contained in:
parent
e3bda6df15
commit
ee56dc8386
@ -1,2 +1,2 @@
|
|||||||
import{a as s}from"./index-r7_wqQ4e.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-KevyU1nl.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-AvM4PHvP.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
import{a as s}from"./index-F9w_cZ47.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-sjs6YRoy.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-AvM4PHvP.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-DG9VB6DM.js.map
|
//# sourceMappingURL=conclusionApi-BIX8LEl5.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-DG9VB6DM.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
|
{"version":3,"file":"conclusionApi-BIX8LEl5.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
|
||||||
1
build/assets/index-CPRbj7YF.css
Normal file
1
build/assets/index-CPRbj7YF.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
75
build/assets/index-F9w_cZ47.js
Normal file
75
build/assets/index-F9w_cZ47.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-F9w_cZ47.js.map
Normal file
1
build/assets/index-F9w_cZ47.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -52,15 +52,15 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-r7_wqQ4e.js"></script>
|
<script type="module" crossorigin src="/assets/index-F9w_cZ47.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Bme4E5cb.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Bme4E5cb.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DIkYAdWy.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DIkYAdWy.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-KevyU1nl.js">
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-sjs6YRoy.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-AvM4PHvP.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-AvM4PHvP.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DMIvOF3n.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CPRbj7YF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -1,38 +1,29 @@
|
|||||||
import { QueryInterface, DataTypes } from 'sequelize';
|
import { QueryInterface, DataTypes } from 'sequelize';
|
||||||
|
|
||||||
export const up = async (queryInterface: QueryInterface) => {
|
export const up = async (queryInterface: QueryInterface) => {
|
||||||
// 1. Drop the old dealer_claim_history table if it exists
|
// 1. Drop and recreate the enum type for snapshot_type to ensure all values are included
|
||||||
const tables = await queryInterface.showAllTables();
|
// This ensures APPROVE is always present when table is recreated
|
||||||
if (tables.includes('dealer_claim_history')) {
|
// Note: Table should be dropped manually before running this migration
|
||||||
await queryInterface.dropTable('dealer_claim_history');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create or update the enum type for snapshot_type
|
|
||||||
// Check if enum exists, if not create it, if yes update it
|
|
||||||
try {
|
try {
|
||||||
await queryInterface.sequelize.query(`
|
await queryInterface.sequelize.query(`
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_dealer_claim_history_snapshot_type') THEN
|
-- Drop enum if it exists (cascade will handle any dependencies)
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_dealer_claim_history_snapshot_type') THEN
|
||||||
|
DROP TYPE IF EXISTS enum_dealer_claim_history_snapshot_type CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Create enum with all values including APPROVE
|
||||||
CREATE TYPE enum_dealer_claim_history_snapshot_type AS ENUM ('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE');
|
CREATE TYPE enum_dealer_claim_history_snapshot_type AS ENUM ('PROPOSAL', 'COMPLETION', 'INTERNAL_ORDER', 'WORKFLOW', 'APPROVE');
|
||||||
ELSE
|
|
||||||
-- Check if APPROVE exists in the enum
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_enum
|
|
||||||
WHERE enumlabel = 'APPROVE'
|
|
||||||
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_dealer_claim_history_snapshot_type')
|
|
||||||
) THEN
|
|
||||||
ALTER TYPE enum_dealer_claim_history_snapshot_type ADD VALUE 'APPROVE';
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
END $$;
|
||||||
`);
|
`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If enum creation fails, try to continue (might already exist)
|
// If enum creation fails, log error but continue
|
||||||
console.warn('Enum creation/update warning:', error);
|
console.error('Enum creation error:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create new simplified level-based dealer_claim_history table
|
// 2. Create new simplified level-based dealer_claim_history table
|
||||||
await queryInterface.createTable('dealer_claim_history', {
|
await queryInterface.createTable('dealer_claim_history', {
|
||||||
history_id: {
|
history_id: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
@ -131,6 +122,13 @@ export const up = async (queryInterface: QueryInterface) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const down = async (queryInterface: QueryInterface) => {
|
export const down = async (queryInterface: QueryInterface) => {
|
||||||
// Drop the new table
|
// Note: Table should be dropped manually
|
||||||
await queryInterface.dropTable('dealer_claim_history');
|
// Drop the enum type
|
||||||
|
try {
|
||||||
|
await queryInterface.sequelize.query(`
|
||||||
|
DROP TYPE IF EXISTS enum_dealer_claim_history_snapshot_type CASCADE;
|
||||||
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Enum drop warning:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { ApprovalLevel } from '../models/ApprovalLevel';
|
|||||||
import { Participant } from '../models/Participant';
|
import { Participant } from '../models/Participant';
|
||||||
import { User } from '../models/User';
|
import { User } from '../models/User';
|
||||||
import { DealerClaimHistory, SnapshotType } from '../models/DealerClaimHistory';
|
import { DealerClaimHistory, SnapshotType } from '../models/DealerClaimHistory';
|
||||||
|
import { Document } from '../models/Document';
|
||||||
import { WorkflowService } from './workflow.service';
|
import { WorkflowService } from './workflow.service';
|
||||||
import { DealerClaimApprovalService } from './dealerClaimApproval.service';
|
import { DealerClaimApprovalService } from './dealerClaimApproval.service';
|
||||||
import { generateRequestNumber } from '../utils/helpers';
|
import { generateRequestNumber } from '../utils/helpers';
|
||||||
@ -1296,12 +1297,21 @@ export class DealerClaimService {
|
|||||||
? proposalData.dealerComments.trim()
|
? proposalData.dealerComments.trim()
|
||||||
: 'Dealer proposal submitted';
|
: 'Dealer proposal submitted';
|
||||||
|
|
||||||
// Save proposal history BEFORE approving
|
// Perform the approval action FIRST - only save snapshot if action succeeds
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
dealerProposalLevel.levelId,
|
||||||
|
{ action: 'APPROVE', comments: approvalComment },
|
||||||
|
actualDealerUserId || (request as any).initiatorId || 'system', // Use dealer or initiator ID
|
||||||
|
{ ipAddress: null, userAgent: null }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save proposal history AFTER approval succeeds (this is the only snapshot needed for dealer submission)
|
||||||
// Use dealer user ID if available, otherwise use initiator ID as fallback
|
// Use dealer user ID if available, otherwise use initiator ID as fallback
|
||||||
const historyUserId = actualDealerUserId || (request as any).initiatorId || null;
|
const historyUserId = actualDealerUserId || (request as any).initiatorId || null;
|
||||||
if (!historyUserId) {
|
if (!historyUserId) {
|
||||||
logger.warn(`[DealerClaimService] No user ID available for proposal history, skipping history save`);
|
logger.warn(`[DealerClaimService] No user ID available for proposal history, skipping history save`);
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
await this.saveProposalHistory(
|
await this.saveProposalHistory(
|
||||||
requestId,
|
requestId,
|
||||||
dealerProposalLevel.levelId,
|
dealerProposalLevel.levelId,
|
||||||
@ -1309,24 +1319,13 @@ export class DealerClaimService {
|
|||||||
`Proposal Submitted: ${approvalComment}`,
|
`Proposal Submitted: ${approvalComment}`,
|
||||||
historyUserId
|
historyUserId
|
||||||
);
|
);
|
||||||
|
// Note: We don't save workflow history here - proposal history is sufficient
|
||||||
// Save workflow history - dealer submitting document is also an action
|
// Workflow history will be saved when the level is approved and moves to next level
|
||||||
await this.saveWorkflowHistory(
|
} catch (snapshotError) {
|
||||||
requestId,
|
// Log error but don't fail the submission - snapshot is for audit, not critical
|
||||||
`Dealer submitted proposal document`,
|
logger.error(`[DealerClaimService] Failed to save proposal history snapshot (non-critical):`, snapshotError);
|
||||||
historyUserId,
|
}
|
||||||
dealerProposalLevel.levelId,
|
|
||||||
dealerProposalLevel.levelNumber,
|
|
||||||
dealerProposalLevel.levelName || undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.approvalService.approveLevel(
|
|
||||||
dealerProposalLevel.levelId,
|
|
||||||
{ action: 'APPROVE', comments: approvalComment },
|
|
||||||
actualDealerUserId || (request as any).initiatorId || 'system', // Use dealer or initiator ID
|
|
||||||
{ ipAddress: null, userAgent: null }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Dealer proposal submitted for request: ${requestId}`);
|
logger.info(`[DealerClaimService] Dealer proposal submitted for request: ${requestId}`);
|
||||||
@ -1445,12 +1444,21 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Perform the approval action FIRST - only save snapshot if action succeeds
|
||||||
|
await this.approvalService.approveLevel(
|
||||||
|
dealerCompletionLevel.levelId,
|
||||||
|
{ action: 'APPROVE', comments: approvalComment },
|
||||||
|
actualDealerUserId || (request as any).initiatorId || 'system',
|
||||||
|
{ ipAddress: null, userAgent: null }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save completion history AFTER approval succeeds (this is the only snapshot needed for dealer completion)
|
||||||
// Use dealer user ID if available, otherwise use initiator ID as fallback
|
// Use dealer user ID if available, otherwise use initiator ID as fallback
|
||||||
const historyUserId = actualDealerUserId || (request as any).initiatorId || null;
|
const historyUserId = actualDealerUserId || (request as any).initiatorId || null;
|
||||||
if (!historyUserId) {
|
if (!historyUserId) {
|
||||||
logger.warn(`[DealerClaimService] No user ID available for completion history, skipping history save`);
|
logger.warn(`[DealerClaimService] No user ID available for completion history, skipping history save`);
|
||||||
} else {
|
} else {
|
||||||
// Save completion history BEFORE approving
|
try {
|
||||||
await this.saveCompletionHistory(
|
await this.saveCompletionHistory(
|
||||||
requestId,
|
requestId,
|
||||||
dealerCompletionLevel.levelId,
|
dealerCompletionLevel.levelId,
|
||||||
@ -1458,24 +1466,13 @@ export class DealerClaimService {
|
|||||||
`Completion Submitted: ${approvalComment}`,
|
`Completion Submitted: ${approvalComment}`,
|
||||||
historyUserId
|
historyUserId
|
||||||
);
|
);
|
||||||
|
// Note: We don't save workflow history here - completion history is sufficient
|
||||||
// Save workflow history - dealer submitting completion document is also an action
|
// Workflow history will be saved when the level is approved and moves to next level
|
||||||
await this.saveWorkflowHistory(
|
} catch (snapshotError) {
|
||||||
requestId,
|
// Log error but don't fail the submission - snapshot is for audit, not critical
|
||||||
`Dealer submitted completion document`,
|
logger.error(`[DealerClaimService] Failed to save completion history snapshot (non-critical):`, snapshotError);
|
||||||
historyUserId,
|
}
|
||||||
dealerCompletionLevel.levelId,
|
|
||||||
dealerCompletionLevel.levelNumber,
|
|
||||||
dealerCompletionLevel.levelName || undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.approvalService.approveLevel(
|
|
||||||
dealerCompletionLevel.levelId,
|
|
||||||
{ action: 'APPROVE', comments: approvalComment },
|
|
||||||
actualDealerUserId || (request as any).initiatorId || 'system',
|
|
||||||
{ ipAddress: null, userAgent: null }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] Completion documents submitted for request: ${requestId}`);
|
logger.info(`[DealerClaimService] Completion documents submitted for request: ${requestId}`);
|
||||||
@ -1768,9 +1765,9 @@ export class DealerClaimService {
|
|||||||
where: { requestId, levelNumber: 3 }
|
where: { requestId, levelNumber: 3 }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ioLevel) {
|
|
||||||
// Get user ID for history - use organizedBy if it's a UUID, otherwise try to find user
|
// Get user ID for history - use organizedBy if it's a UUID, otherwise try to find user
|
||||||
let ioHistoryUserId: string | null = null;
|
let ioHistoryUserId: string | null = null;
|
||||||
|
if (ioLevel) {
|
||||||
if (organizedBy) {
|
if (organizedBy) {
|
||||||
// Check if organizedBy is a valid UUID
|
// Check if organizedBy is a valid UUID
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
@ -1790,21 +1787,9 @@ export class DealerClaimService {
|
|||||||
const request = await WorkflowRequest.findByPk(requestId);
|
const request = await WorkflowRequest.findByPk(requestId);
|
||||||
ioHistoryUserId = (request as any)?.initiatorId || null;
|
ioHistoryUserId = (request as any)?.initiatorId || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ioHistoryUserId) {
|
|
||||||
await this.saveIOHistory(
|
|
||||||
requestId,
|
|
||||||
ioLevel.levelId,
|
|
||||||
ioLevel.levelNumber,
|
|
||||||
`IO Blocked: ₹${finalBlockedAmount.toFixed(2)} blocked in SAP`,
|
|
||||||
ioHistoryUserId
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.warn(`[DealerClaimService] No user ID available for IO history, skipping history save`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update budget tracking with blocked amount
|
// Update budget tracking with blocked amount FIRST
|
||||||
await ClaimBudgetTracking.upsert({
|
await ClaimBudgetTracking.upsert({
|
||||||
requestId,
|
requestId,
|
||||||
ioBlockedAmount: finalBlockedAmount,
|
ioBlockedAmount: finalBlockedAmount,
|
||||||
@ -1813,6 +1798,24 @@ export class DealerClaimService {
|
|||||||
currency: 'INR',
|
currency: 'INR',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save IO history AFTER budget tracking update succeeds (only if ioLevel exists)
|
||||||
|
if (ioLevel && ioHistoryUserId) {
|
||||||
|
try {
|
||||||
|
await this.saveIOHistory(
|
||||||
|
requestId,
|
||||||
|
ioLevel.levelId,
|
||||||
|
ioLevel.levelNumber,
|
||||||
|
`IO Blocked: ₹${finalBlockedAmount.toFixed(2)} blocked in SAP`,
|
||||||
|
ioHistoryUserId
|
||||||
|
);
|
||||||
|
} catch (snapshotError) {
|
||||||
|
// Log error but don't fail the IO blocking - snapshot is for audit, not critical
|
||||||
|
logger.error(`[DealerClaimService] Failed to save IO history snapshot (non-critical):`, snapshotError);
|
||||||
|
}
|
||||||
|
} else if (ioLevel && !ioHistoryUserId) {
|
||||||
|
logger.warn(`[DealerClaimService] No user ID available for IO history, skipping history save`);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[DealerClaimService] IO blocked for request: ${requestId}`, {
|
logger.info(`[DealerClaimService] IO blocked for request: ${requestId}`, {
|
||||||
ioNumber: ioData.ioNumber,
|
ioNumber: ioData.ioNumber,
|
||||||
blockedAmount: finalBlockedAmount,
|
blockedAmount: finalBlockedAmount,
|
||||||
@ -2378,6 +2381,16 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch supporting documents
|
||||||
|
const supportingDocs = await Document.findAll({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
category: 'SUPPORTING',
|
||||||
|
isDeleted: false
|
||||||
|
},
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
const snapshotData = {
|
const snapshotData = {
|
||||||
documentUrl: proposalDetails.proposalDocumentUrl,
|
documentUrl: proposalDetails.proposalDocumentUrl,
|
||||||
totalBudget: Number(proposalDetails.totalEstimatedBudget || 0),
|
totalBudget: Number(proposalDetails.totalEstimatedBudget || 0),
|
||||||
@ -2387,6 +2400,13 @@ export class DealerClaimService {
|
|||||||
description: i.itemDescription,
|
description: i.itemDescription,
|
||||||
amount: Number(i.amount || 0),
|
amount: Number(i.amount || 0),
|
||||||
order: i.itemOrder
|
order: i.itemOrder
|
||||||
|
})),
|
||||||
|
otherDocuments: supportingDocs.map(doc => ({
|
||||||
|
documentId: doc.documentId,
|
||||||
|
fileName: doc.fileName,
|
||||||
|
originalFileName: doc.originalFileName,
|
||||||
|
storageUrl: doc.storageUrl,
|
||||||
|
uploadedAt: doc.uploadedAt
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2446,6 +2466,16 @@ export class DealerClaimService {
|
|||||||
});
|
});
|
||||||
const nextVersion = lastVersion ? lastVersion.version + 1 : 1;
|
const nextVersion = lastVersion ? lastVersion.version + 1 : 1;
|
||||||
|
|
||||||
|
// Fetch supporting documents for completion
|
||||||
|
const supportingDocs = await Document.findAll({
|
||||||
|
where: {
|
||||||
|
requestId,
|
||||||
|
category: 'SUPPORTING',
|
||||||
|
isDeleted: false
|
||||||
|
},
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
// Store all completion data in JSONB
|
// Store all completion data in JSONB
|
||||||
const snapshotData = {
|
const snapshotData = {
|
||||||
documentUrl: (completionDetails as any).completionDocumentUrl || null,
|
documentUrl: (completionDetails as any).completionDocumentUrl || null,
|
||||||
@ -2454,6 +2484,13 @@ export class DealerClaimService {
|
|||||||
expenses: expenses.map(e => ({
|
expenses: expenses.map(e => ({
|
||||||
description: e.description,
|
description: e.description,
|
||||||
amount: Number(e.amount || 0)
|
amount: Number(e.amount || 0)
|
||||||
|
})),
|
||||||
|
otherDocuments: supportingDocs.map(doc => ({
|
||||||
|
documentId: doc.documentId,
|
||||||
|
fileName: doc.fileName,
|
||||||
|
originalFileName: doc.originalFileName,
|
||||||
|
storageUrl: doc.storageUrl,
|
||||||
|
uploadedAt: doc.uploadedAt
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2582,6 +2619,8 @@ export class DealerClaimService {
|
|||||||
levelName: level.levelName
|
levelName: level.levelName
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build changeReason - will be updated later if moving to next level
|
||||||
|
// For now, just include the basic approval/rejection info
|
||||||
const changeReason = action === 'APPROVE'
|
const changeReason = action === 'APPROVE'
|
||||||
? `Approved by ${level.approverName || level.approverEmail}`
|
? `Approved by ${level.approverName || level.approverEmail}`
|
||||||
: `Rejected by ${level.approverName || level.approverEmail}`;
|
: `Rejected by ${level.approverName || level.approverEmail}`;
|
||||||
@ -2613,15 +2652,26 @@ export class DealerClaimService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
approvalLevelId?: string,
|
approvalLevelId?: string,
|
||||||
levelNumber?: number,
|
levelNumber?: number,
|
||||||
levelName?: string
|
levelName?: string,
|
||||||
|
approvalComment?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const wf = await WorkflowRequest.findByPk(requestId);
|
const wf = await WorkflowRequest.findByPk(requestId);
|
||||||
if (!wf) return;
|
if (!wf) return;
|
||||||
|
|
||||||
// Get next version for workflow-level snapshots
|
// Get next version for workflow-level snapshots PER LEVEL
|
||||||
|
// Each level should have its own version numbering starting from 1
|
||||||
|
// Filter by levelName or levelNumber to get versions for this specific level
|
||||||
const lastVersion = await DealerClaimHistory.findOne({
|
const lastVersion = await DealerClaimHistory.findOne({
|
||||||
where: {
|
where: levelName ? {
|
||||||
|
requestId,
|
||||||
|
levelName,
|
||||||
|
snapshotType: SnapshotType.WORKFLOW
|
||||||
|
} : levelNumber !== undefined ? {
|
||||||
|
requestId,
|
||||||
|
levelNumber,
|
||||||
|
snapshotType: SnapshotType.WORKFLOW
|
||||||
|
} : {
|
||||||
requestId,
|
requestId,
|
||||||
snapshotType: SnapshotType.WORKFLOW
|
snapshotType: SnapshotType.WORKFLOW
|
||||||
},
|
},
|
||||||
@ -2630,11 +2680,22 @@ export class DealerClaimService {
|
|||||||
const nextVersion = lastVersion ? lastVersion.version + 1 : 1;
|
const nextVersion = lastVersion ? lastVersion.version + 1 : 1;
|
||||||
|
|
||||||
// Store workflow data in JSONB
|
// Store workflow data in JSONB
|
||||||
const snapshotData = {
|
// Include level information for version tracking and comparison
|
||||||
|
// Include approval comment if provided (for approval actions)
|
||||||
|
const snapshotData: any = {
|
||||||
status: wf.status,
|
status: wf.status,
|
||||||
currentLevel: wf.currentLevel
|
currentLevel: wf.currentLevel,
|
||||||
|
// Include level info in snapshotData for completeness and version tracking
|
||||||
|
approvalLevelId: approvalLevelId || undefined,
|
||||||
|
levelNumber: levelNumber || undefined,
|
||||||
|
levelName: levelName || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add approval comment to snapshotData if provided
|
||||||
|
if (approvalComment) {
|
||||||
|
snapshotData.comments = approvalComment;
|
||||||
|
}
|
||||||
|
|
||||||
await DealerClaimHistory.create({
|
await DealerClaimHistory.create({
|
||||||
requestId,
|
requestId,
|
||||||
approvalLevelId: approvalLevelId || undefined,
|
approvalLevelId: approvalLevelId || undefined,
|
||||||
@ -3013,15 +3074,6 @@ export class DealerClaimService {
|
|||||||
where: { requestId, levelNumber: wf.currentLevel || 1 }
|
where: { requestId, levelNumber: wf.currentLevel || 1 }
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.saveWorkflowHistory(
|
|
||||||
requestId,
|
|
||||||
changeReason,
|
|
||||||
userId,
|
|
||||||
currentLevel?.levelId || undefined,
|
|
||||||
currentLevel?.levelNumber || wf.currentLevel || undefined,
|
|
||||||
currentLevel?.levelName || undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
await wf.update({
|
await wf.update({
|
||||||
status: WorkflowStatus.CLOSED,
|
status: WorkflowStatus.CLOSED,
|
||||||
closureDate: now
|
closureDate: now
|
||||||
@ -3059,13 +3111,14 @@ export class DealerClaimService {
|
|||||||
|
|
||||||
const deptLeadLevelNumber = deptLeadLevel.levelNumber;
|
const deptLeadLevelNumber = deptLeadLevel.levelNumber;
|
||||||
|
|
||||||
// Move back to Department Lead Approval level
|
// Move back to Department Lead Approval level FIRST
|
||||||
await wf.update({
|
await wf.update({
|
||||||
status: WorkflowStatus.PENDING,
|
status: WorkflowStatus.PENDING,
|
||||||
currentLevel: deptLeadLevelNumber
|
currentLevel: deptLeadLevelNumber
|
||||||
});
|
});
|
||||||
|
|
||||||
// Capture workflow snapshot AFTER moving to department lead level
|
// Capture workflow snapshot AFTER workflow update succeeds
|
||||||
|
try {
|
||||||
await this.saveWorkflowHistory(
|
await this.saveWorkflowHistory(
|
||||||
requestId,
|
requestId,
|
||||||
`Reopened and moved to Department Lead level (${deptLeadLevelNumber}) - ${changeReason}`,
|
`Reopened and moved to Department Lead level (${deptLeadLevelNumber}) - ${changeReason}`,
|
||||||
@ -3074,6 +3127,10 @@ export class DealerClaimService {
|
|||||||
deptLeadLevelNumber,
|
deptLeadLevelNumber,
|
||||||
deptLeadLevel.levelName || undefined
|
deptLeadLevel.levelName || undefined
|
||||||
);
|
);
|
||||||
|
} catch (snapshotError) {
|
||||||
|
// Log error but don't fail the reopen - snapshot is for audit, not critical
|
||||||
|
logger.error(`[DealerClaimService] Failed to save workflow history snapshot (non-critical):`, snapshotError);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset the found level status to IN_PROGRESS so Dept Lead can approve again
|
// Reset the found level status to IN_PROGRESS so Dept Lead can approve again
|
||||||
await deptLeadLevel.update({
|
await deptLeadLevel.update({
|
||||||
@ -3123,15 +3180,8 @@ export class DealerClaimService {
|
|||||||
return name.includes('dealer proposal') || l.levelNumber === 1;
|
return name.includes('dealer proposal') || l.levelNumber === 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save workflow history with dealer level information
|
// Note: DISCUSS action doesn't change workflow state, so no snapshot needed
|
||||||
await this.saveWorkflowHistory(
|
// The action is logged in activity log only
|
||||||
requestId,
|
|
||||||
changeReason,
|
|
||||||
userId,
|
|
||||||
dealerLevelDiscuss?.levelId || undefined,
|
|
||||||
dealerLevelDiscuss?.levelNumber || undefined,
|
|
||||||
dealerLevelDiscuss?.levelName || undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
await activityService.log({
|
await activityService.log({
|
||||||
requestId,
|
requestId,
|
||||||
@ -3184,13 +3234,14 @@ export class DealerClaimService {
|
|||||||
throw new Error('No previous level found to revise to');
|
throw new Error('No previous level found to revise to');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move back to previous level
|
// Move back to previous level FIRST
|
||||||
await wf.update({
|
await wf.update({
|
||||||
status: WorkflowStatus.PENDING,
|
status: WorkflowStatus.PENDING,
|
||||||
currentLevel: previousLevel.levelNumber
|
currentLevel: previousLevel.levelNumber
|
||||||
});
|
});
|
||||||
|
|
||||||
// Capture workflow snapshot when moving back to previous level
|
// Capture workflow snapshot AFTER workflow update succeeds
|
||||||
|
try {
|
||||||
await this.saveWorkflowHistory(
|
await this.saveWorkflowHistory(
|
||||||
requestId,
|
requestId,
|
||||||
`Moved back to previous level (${previousLevel.levelNumber}) - ${changeReason}`,
|
`Moved back to previous level (${previousLevel.levelNumber}) - ${changeReason}`,
|
||||||
@ -3199,6 +3250,10 @@ export class DealerClaimService {
|
|||||||
previousLevel.levelNumber,
|
previousLevel.levelNumber,
|
||||||
previousLevel.levelName || undefined
|
previousLevel.levelName || undefined
|
||||||
);
|
);
|
||||||
|
} catch (snapshotError) {
|
||||||
|
// Log error but don't fail the revise - snapshot is for audit, not critical
|
||||||
|
logger.error(`[DealerClaimService] Failed to save workflow history snapshot (non-critical):`, snapshotError);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset current level to PENDING
|
// Reset current level to PENDING
|
||||||
await currentLevel.update({
|
await currentLevel.update({
|
||||||
@ -3273,7 +3328,19 @@ export class DealerClaimService {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
return history;
|
|
||||||
|
// Map to plain objects and sort otherDocuments in snapshots
|
||||||
|
return history.map(item => {
|
||||||
|
const plain = item.get({ plain: true });
|
||||||
|
if (plain.snapshotData && plain.snapshotData.otherDocuments && Array.isArray(plain.snapshotData.otherDocuments)) {
|
||||||
|
plain.snapshotData.otherDocuments.sort((a: any, b: any) => {
|
||||||
|
const dateA = a.uploadedAt ? new Date(a.uploadedAt).getTime() : 0;
|
||||||
|
const dateB = b.uploadedAt ? new Date(b.uploadedAt).getTime() : 0;
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return plain;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -106,44 +106,49 @@ export class DealerClaimApprovalService {
|
|||||||
return await this.handleRejection(level, action, userId, requestMetadata, elapsedHours, tatPercentage, now);
|
return await this.handleRejection(level, action, userId, requestMetadata, elapsedHours, tatPercentage, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save approval history BEFORE updating level
|
logger.info(`[DealerClaimApproval] Approving level ${levelId} with action:`, JSON.stringify(action));
|
||||||
await this.getDealerClaimService().saveApprovalHistory(
|
|
||||||
level.requestId,
|
|
||||||
level.levelId,
|
|
||||||
level.levelNumber,
|
|
||||||
'APPROVE',
|
|
||||||
action.comments || '',
|
|
||||||
undefined,
|
|
||||||
userId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Capture workflow snapshot for approval action (before moving to next level)
|
// Robust comment extraction
|
||||||
// This captures the approval action itself, including initiator evaluation
|
const approvalComment = action.comments || (action as any).comment || '';
|
||||||
const levelName = (level.levelName || '').toLowerCase();
|
|
||||||
const isInitiatorEvaluation = levelName.includes('requestor') || levelName.includes('evaluation');
|
|
||||||
const approvalMessage = isInitiatorEvaluation
|
|
||||||
? `Initiator evaluated and approved (level ${level.levelNumber})`
|
|
||||||
: `Approved level ${level.levelNumber}`;
|
|
||||||
|
|
||||||
await this.getDealerClaimService().saveWorkflowHistory(
|
// Update level status and elapsed time for approval FIRST
|
||||||
level.requestId,
|
// Only save snapshot if the update succeeds
|
||||||
approvalMessage,
|
|
||||||
userId,
|
|
||||||
level.levelId,
|
|
||||||
level.levelNumber,
|
|
||||||
level.levelName || undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update level status and elapsed time for approval
|
|
||||||
await level.update({
|
await level.update({
|
||||||
status: ApprovalStatus.APPROVED,
|
status: ApprovalStatus.APPROVED,
|
||||||
actionDate: now,
|
actionDate: now,
|
||||||
levelEndTime: now,
|
levelEndTime: now,
|
||||||
elapsedHours: elapsedHours,
|
elapsedHours: elapsedHours,
|
||||||
tatPercentageUsed: tatPercentage,
|
tatPercentageUsed: tatPercentage,
|
||||||
comments: action.comments || undefined
|
comments: approvalComment || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if this is a dealer submission (proposal or completion) - these have their own snapshot types
|
||||||
|
const levelName = (level.levelName || '').toLowerCase();
|
||||||
|
const isDealerSubmission = levelName.includes('dealer proposal') || levelName.includes('dealer completion');
|
||||||
|
|
||||||
|
// Only save APPROVE snapshot for actual approver actions (not dealer submissions)
|
||||||
|
// Dealer submissions use PROPOSAL/COMPLETION snapshot types instead
|
||||||
|
if (!isDealerSubmission) {
|
||||||
|
try {
|
||||||
|
await this.getDealerClaimService().saveApprovalHistory(
|
||||||
|
level.requestId,
|
||||||
|
level.levelId,
|
||||||
|
level.levelNumber,
|
||||||
|
'APPROVE',
|
||||||
|
approvalComment,
|
||||||
|
undefined,
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
} catch (snapshotError) {
|
||||||
|
// Log error but don't fail the approval - snapshot is for audit, not critical
|
||||||
|
logger.error(`[DealerClaimApproval] Failed to save approval history snapshot (non-critical):`, snapshotError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We don't save workflow history for approval actions
|
||||||
|
// The approval history (saveApprovalHistory) is sufficient and includes comments
|
||||||
|
// Workflow movement information is included in the APPROVE snapshot's changeReason
|
||||||
|
|
||||||
// Check if this is the final approver
|
// Check if this is the final approver
|
||||||
const allLevels = await ApprovalLevel.findAll({
|
const allLevels = await ApprovalLevel.findAll({
|
||||||
where: { requestId: level.requestId }
|
where: { requestId: level.requestId }
|
||||||
@ -275,16 +280,37 @@ export class DealerClaimApprovalService {
|
|||||||
{ where: { requestId: level.requestId } }
|
{ where: { requestId: level.requestId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Capture workflow snapshot when moving to next level
|
// Update the APPROVE snapshot's changeReason to include movement information
|
||||||
// Include both the approved level and the next level in the message
|
// This ensures the approval snapshot shows both the approval and the movement
|
||||||
await this.getDealerClaimService().saveWorkflowHistory(
|
// We don't create a separate WORKFLOW snapshot for approvals - only APPROVE snapshot
|
||||||
level.requestId,
|
try {
|
||||||
`Level ${level.levelNumber} approved, moved to next level (${nextLevelNumber})`,
|
const { DealerClaimHistory } = await import('@models/DealerClaimHistory');
|
||||||
userId,
|
const { SnapshotType } = await import('@models/DealerClaimHistory');
|
||||||
nextLevel?.levelId || undefined, // Store next level's ID since we're moving to it
|
|
||||||
nextLevelNumber || undefined,
|
const approvalHistory = await DealerClaimHistory.findOne({
|
||||||
(nextLevel as any)?.levelName || undefined
|
where: {
|
||||||
);
|
requestId: level.requestId,
|
||||||
|
approvalLevelId: level.levelId,
|
||||||
|
snapshotType: SnapshotType.APPROVE
|
||||||
|
},
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (approvalHistory) {
|
||||||
|
// Use the robust approvalComment from outer scope
|
||||||
|
const updatedChangeReason = approvalComment
|
||||||
|
? `Approved by ${level.approverName || level.approverEmail}, moved to next level (${nextLevelNumber}). Comment: ${approvalComment}`
|
||||||
|
: `Approved by ${level.approverName || level.approverEmail}, moved to next level (${nextLevelNumber})`;
|
||||||
|
|
||||||
|
await approvalHistory.update({
|
||||||
|
changeReason: updatedChangeReason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (updateError) {
|
||||||
|
// Log error but don't fail - this is just updating the changeReason for better display
|
||||||
|
logger.warn(`[DealerClaimApproval] Failed to update approval history changeReason (non-critical):`, updateError);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`[DealerClaimApproval] Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
logger.info(`[DealerClaimApproval] Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,13 +667,8 @@ export class DealerClaimApprovalService {
|
|||||||
|
|
||||||
// Check if this is the Department Lead approval step (Step 3)
|
// Check if this is the Department Lead approval step (Step 3)
|
||||||
// Robust check: check level name for variations and level number as fallback
|
// Robust check: check level name for variations and level number as fallback
|
||||||
const levelName = (level.levelName || '').toLowerCase();
|
// Default rejection logic: Return to immediately previous approval step
|
||||||
const isDeptLeadResult =
|
logger.info(`[DealerClaimApproval] Rejection for request ${level.requestId} by level ${level.levelNumber}. Finding previous step to return to.`);
|
||||||
levelName.includes('department lead') ||
|
|
||||||
levelName.includes('dept lead');
|
|
||||||
|
|
||||||
if (isDeptLeadResult) {
|
|
||||||
logger.info(`[DealerClaimApproval] Department Lead rejected request ${level.requestId}. Circling back to initiator.`);
|
|
||||||
|
|
||||||
// Save approval history (rejection) BEFORE updating level
|
// Save approval history (rejection) BEFORE updating level
|
||||||
await this.getDealerClaimService().saveApprovalHistory(
|
await this.getDealerClaimService().saveApprovalHistory(
|
||||||
@ -660,115 +681,46 @@ export class DealerClaimApprovalService {
|
|||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update level status to REJECTED (but signifies a return at this level)
|
// Find all levels to determine previous step
|
||||||
|
const allLevels = await ApprovalLevel.findAll({
|
||||||
|
where: { requestId: level.requestId },
|
||||||
|
order: [['levelNumber', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the immediately previous approval level
|
||||||
|
const currentLevelNumber = level.levelNumber || 0;
|
||||||
|
const previousLevels = allLevels.filter(l => l.levelNumber < currentLevelNumber && l.levelNumber > 0);
|
||||||
|
const previousLevel = previousLevels[previousLevels.length - 1];
|
||||||
|
|
||||||
|
// Update level status - if returning to previous step, set this level to PENDING (reset)
|
||||||
|
// If no previous step (terminal rejection), set to REJECTED
|
||||||
|
const newStatus = previousLevel ? ApprovalStatus.PENDING : ApprovalStatus.REJECTED;
|
||||||
|
|
||||||
await level.update({
|
await level.update({
|
||||||
status: ApprovalStatus.REJECTED,
|
status: newStatus,
|
||||||
actionDate: rejectionNow,
|
// If resetting to PENDING, clear action details so it can be acted upon again later
|
||||||
levelEndTime: rejectionNow,
|
actionDate: previousLevel ? null : rejectionNow,
|
||||||
elapsedHours: elapsedHours || 0,
|
levelEndTime: previousLevel ? null : rejectionNow,
|
||||||
tatPercentageUsed: tatPercentage || 0,
|
elapsedHours: previousLevel ? 0 : (elapsedHours || 0),
|
||||||
comments: action.comments || action.rejectionReason || undefined
|
tatPercentageUsed: previousLevel ? 0 : (tatPercentage || 0),
|
||||||
});
|
comments: previousLevel ? null : (action.comments || action.rejectionReason || undefined)
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Create or activate initiator action level
|
// If no previous level found (this is the first step), close the workflow
|
||||||
const initiatorLevel = await this.getDealerClaimService().createOrActivateInitiatorLevel(
|
if (!previousLevel) {
|
||||||
level.requestId,
|
logger.info(`[DealerClaimApproval] No previous level found. This is the first step. Closing workflow.`);
|
||||||
(wf as any).initiatorId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update workflow status to REJECTED but DO NOT set closureDate
|
// Capture workflow snapshot for terminal rejection
|
||||||
// Set currentLevel to initiator level if created
|
|
||||||
const newCurrentLevel = initiatorLevel ? initiatorLevel.levelNumber : wf.currentLevel;
|
|
||||||
await WorkflowRequest.update(
|
|
||||||
{
|
|
||||||
status: WorkflowStatus.REJECTED,
|
|
||||||
currentLevel: newCurrentLevel
|
|
||||||
},
|
|
||||||
{ where: { requestId: level.requestId } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Capture workflow snapshot when moving back to initiator
|
|
||||||
// Include the rejected level information in the message
|
|
||||||
await this.getDealerClaimService().saveWorkflowHistory(
|
await this.getDealerClaimService().saveWorkflowHistory(
|
||||||
level.requestId,
|
level.requestId,
|
||||||
`Department Lead rejected (level ${level.levelNumber}) and moved back to initiator (level ${newCurrentLevel})`,
|
`Level ${level.levelNumber} rejected (terminal rejection - no previous step)`,
|
||||||
userId,
|
|
||||||
level.levelId, // Store the rejected level's ID
|
|
||||||
level.levelNumber, // Store the rejected level's number
|
|
||||||
level.levelName || undefined // Store the rejected level's name
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log activity
|
|
||||||
activityService.log({
|
|
||||||
requestId: level.requestId,
|
|
||||||
type: 'rejection',
|
|
||||||
user: { userId: level.approverId, name: level.approverName },
|
|
||||||
timestamp: rejectionNow.toISOString(),
|
|
||||||
action: 'Returned to Initiator',
|
|
||||||
details: `Request returned to initiator by ${level.approverName || level.approverEmail}. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
|
||||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
|
||||||
userAgent: requestMetadata?.userAgent || undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
// Notify ONLY the initiator
|
|
||||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
|
||||||
title: `Action Required: Request Returned - ${(wf as any).requestNumber}`,
|
|
||||||
body: `Your request "${(wf as any).title}" has been returned to you by the Department Lead for revision/discussion. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
|
||||||
requestNumber: (wf as any).requestNumber,
|
|
||||||
requestId: level.requestId,
|
|
||||||
url: `/request/${(wf as any).requestNumber}`,
|
|
||||||
type: 'rejection',
|
|
||||||
priority: 'HIGH',
|
|
||||||
actionRequired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit real-time update
|
|
||||||
emitToRequestRoom(level.requestId, 'request:updated', {
|
|
||||||
requestId: level.requestId,
|
|
||||||
requestNumber: (wf as any)?.requestNumber,
|
|
||||||
action: 'RETURN',
|
|
||||||
levelNumber: level.levelNumber,
|
|
||||||
timestamp: rejectionNow.toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default terminal rejection logic for other steps
|
|
||||||
logger.info(`[DealerClaimApproval] Standard rejection for request ${level.requestId} by level ${level.levelNumber}`);
|
|
||||||
|
|
||||||
// Save approval history (rejection) BEFORE updating level
|
|
||||||
await this.getDealerClaimService().saveApprovalHistory(
|
|
||||||
level.requestId,
|
|
||||||
level.levelId,
|
|
||||||
level.levelNumber,
|
|
||||||
'REJECT',
|
|
||||||
action.comments || '',
|
|
||||||
action.rejectionReason || undefined,
|
|
||||||
userId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Capture workflow snapshot for terminal rejection action
|
|
||||||
await this.getDealerClaimService().saveWorkflowHistory(
|
|
||||||
level.requestId,
|
|
||||||
`Level ${level.levelNumber} rejected (terminal rejection)`,
|
|
||||||
userId,
|
userId,
|
||||||
level.levelId,
|
level.levelId,
|
||||||
level.levelNumber,
|
level.levelNumber,
|
||||||
level.levelName || undefined
|
level.levelName || undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update level status
|
// Close workflow FIRST
|
||||||
await level.update({
|
|
||||||
status: ApprovalStatus.REJECTED,
|
|
||||||
actionDate: rejectionNow,
|
|
||||||
levelEndTime: rejectionNow,
|
|
||||||
elapsedHours: elapsedHours || 0,
|
|
||||||
tatPercentageUsed: tatPercentage || 0,
|
|
||||||
comments: action.comments || action.rejectionReason || undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close workflow
|
|
||||||
await WorkflowRequest.update(
|
await WorkflowRequest.update(
|
||||||
{
|
{
|
||||||
status: WorkflowStatus.REJECTED,
|
status: WorkflowStatus.REJECTED,
|
||||||
@ -777,19 +729,34 @@ export class DealerClaimApprovalService {
|
|||||||
{ where: { requestId: level.requestId } }
|
{ where: { requestId: level.requestId } }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log rejection activity
|
// Capture workflow snapshot AFTER workflow is closed successfully
|
||||||
|
try {
|
||||||
|
await this.getDealerClaimService().saveWorkflowHistory(
|
||||||
|
level.requestId,
|
||||||
|
`Level ${level.levelNumber} rejected (terminal rejection - no previous step)`,
|
||||||
|
userId,
|
||||||
|
level.levelId,
|
||||||
|
level.levelNumber,
|
||||||
|
level.levelName || undefined
|
||||||
|
);
|
||||||
|
} catch (snapshotError) {
|
||||||
|
// Log error but don't fail the rejection - snapshot is for audit, not critical
|
||||||
|
logger.error(`[DealerClaimApproval] Failed to save workflow history snapshot (non-critical):`, snapshotError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log rejection activity (terminal rejection)
|
||||||
activityService.log({
|
activityService.log({
|
||||||
requestId: level.requestId,
|
requestId: level.requestId,
|
||||||
type: 'rejection',
|
type: 'rejection',
|
||||||
user: { userId: level.approverId, name: level.approverName },
|
user: { userId: level.approverId, name: level.approverName },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: rejectionNow.toISOString(),
|
||||||
action: 'Rejected',
|
action: 'Rejected',
|
||||||
details: `Request rejected by ${level.approverName || level.approverEmail}. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
details: `Request rejected by ${level.approverName || level.approverEmail}. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||||
userAgent: requestMetadata?.userAgent || undefined
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify initiator and participants
|
// Notify initiator and participants (workflow is closed)
|
||||||
const participants = await import('@models/Participant').then(m => m.Participant.findAll({
|
const participants = await import('@models/Participant').then(m => m.Participant.findAll({
|
||||||
where: { requestId: level.requestId, isActive: true }
|
where: { requestId: level.requestId, isActive: true }
|
||||||
}));
|
}));
|
||||||
@ -812,6 +779,72 @@ export class DealerClaimApprovalService {
|
|||||||
type: 'rejection',
|
type: 'rejection',
|
||||||
priority: 'HIGH'
|
priority: 'HIGH'
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Return to previous step
|
||||||
|
logger.info(`[DealerClaimApproval] Returning to previous level ${previousLevel.levelNumber} (${previousLevel.levelName || 'unnamed'})`);
|
||||||
|
|
||||||
|
// Reset previous level to IN_PROGRESS so it can be acted upon again
|
||||||
|
await previousLevel.update({
|
||||||
|
status: ApprovalStatus.IN_PROGRESS,
|
||||||
|
levelStartTime: rejectionNow,
|
||||||
|
tatStartTime: rejectionNow,
|
||||||
|
actionDate: undefined,
|
||||||
|
levelEndTime: undefined,
|
||||||
|
comments: undefined,
|
||||||
|
elapsedHours: 0,
|
||||||
|
tatPercentageUsed: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update workflow status to IN_PROGRESS (remains active for rework)
|
||||||
|
// Set currentLevel to previous level
|
||||||
|
await WorkflowRequest.update(
|
||||||
|
{
|
||||||
|
status: WorkflowStatus.PENDING,
|
||||||
|
currentLevel: previousLevel.levelNumber
|
||||||
|
},
|
||||||
|
{ where: { requestId: level.requestId } }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Log rejection activity (returned to previous step)
|
||||||
|
activityService.log({
|
||||||
|
requestId: level.requestId,
|
||||||
|
type: 'rejection',
|
||||||
|
user: { userId: level.approverId, name: level.approverName },
|
||||||
|
timestamp: rejectionNow.toISOString(),
|
||||||
|
action: 'Returned to Previous Step',
|
||||||
|
details: `Request rejected by ${level.approverName || level.approverEmail} and returned to level ${previousLevel.levelNumber}. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||||
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||||
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify the approver of the previous level
|
||||||
|
if (previousLevel.approverId) {
|
||||||
|
await notificationService.sendToUsers([previousLevel.approverId], {
|
||||||
|
title: `Request Returned: ${(wf as any).requestNumber}`,
|
||||||
|
body: `Request "${(wf as any).title}" has been returned to your level for revision. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||||
|
requestNumber: (wf as any).requestNumber,
|
||||||
|
requestId: level.requestId,
|
||||||
|
url: `/request/${(wf as any).requestNumber}`,
|
||||||
|
type: 'assignment',
|
||||||
|
priority: 'HIGH',
|
||||||
|
actionRequired: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify initiator when request is returned (not closed)
|
||||||
|
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||||
|
title: `Request Returned: ${(wf as any).requestNumber}`,
|
||||||
|
body: `Request "${(wf as any).title}" has been returned to level ${previousLevel.levelNumber} for revision. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||||
|
requestNumber: (wf as any).requestNumber,
|
||||||
|
requestId: level.requestId,
|
||||||
|
url: `/request/${(wf as any).requestNumber}`,
|
||||||
|
type: 'rejection',
|
||||||
|
priority: 'HIGH',
|
||||||
|
actionRequired: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Emit real-time update to all users viewing this request
|
// Emit real-time update to all users viewing this request
|
||||||
emitToRequestRoom(level.requestId, 'request:updated', {
|
emitToRequestRoom(level.requestId, 'request:updated', {
|
||||||
|
|||||||
@ -206,7 +206,7 @@ class NotificationService {
|
|||||||
try {
|
try {
|
||||||
const rows = await Subscription.findAll({ where: { userId } });
|
const rows = await Subscription.findAll({ where: { userId } });
|
||||||
subs = rows.map((r: any) => ({ endpoint: r.endpoint, keys: { p256dh: r.p256dh, auth: r.auth } }));
|
subs = rows.map((r: any) => ({ endpoint: r.endpoint, keys: { p256dh: r.p256dh, auth: r.auth } }));
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subs.length > 0) {
|
if (subs.length > 0) {
|
||||||
@ -431,8 +431,15 @@ class NotificationService {
|
|||||||
order: [['levelNumber', 'ASC']]
|
order: [['levelNumber', 'ASC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find the level that matches this approver
|
// Find the level that matches this approver - PRIORITIZE PENDING LEVEL
|
||||||
const matchingLevel = allLevels.find((l: any) => l.approverId === userId);
|
// This ensures that if a user has multiple steps (e.g., Step 1 and Step 2),
|
||||||
|
// we pick the one that actually needs action (Step 2) rather than the first one (Step 1)
|
||||||
|
let matchingLevel = allLevels.find((l: any) => l.approverId === userId && l.status === 'PENDING');
|
||||||
|
|
||||||
|
// Fallback to any level if no pending level found (though for assignment there should be one)
|
||||||
|
if (!matchingLevel) {
|
||||||
|
matchingLevel = allLevels.find((l: any) => l.approverId === userId);
|
||||||
|
}
|
||||||
|
|
||||||
// Always reload from DB to ensure we have fresh levelName
|
// Always reload from DB to ensure we have fresh levelName
|
||||||
const currentLevel = matchingLevel
|
const currentLevel = matchingLevel
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user