Compare commits
2 Commits
e03049a861
...
bdfda74167
| Author | SHA1 | Date | |
|---|---|---|---|
| bdfda74167 | |||
| b925ee5217 |
@ -1,2 +1,2 @@
|
|||||||
import{a as s}from"./index-7JN9lLwu.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DbB0YGPu.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B1UBYWWO.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-x1JLuWho.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DbB0YGPu.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B1UBYWWO.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-CMghC3Jo.js.map
|
//# sourceMappingURL=conclusionApi-DX2Gwh6C.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-CMghC3Jo.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-DX2Gwh6C.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"}
|
||||||
File diff suppressed because one or more lines are too long
1
build/assets/index-CNlPctO6.css
Normal file
1
build/assets/index-CNlPctO6.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
File diff suppressed because one or more lines are too long
@ -52,7 +52,7 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-7JN9lLwu.js"></script>
|
<script type="module" crossorigin src="/assets/index-x1JLuWho.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">
|
||||||
@ -60,7 +60,7 @@
|
|||||||
<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-B1UBYWWO.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B1UBYWWO.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B-mLDzJe.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CNlPctO6.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -3,6 +3,16 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
// 1. Debugging: Print what the app actually sees
|
||||||
|
console.log('--- Database Config Debug ---');
|
||||||
|
console.log(`DB_HOST: ${process.env.DB_HOST}`);
|
||||||
|
console.log(`DB_SSL (Raw): '${process.env.DB_SSL}`); // Quotes help see trailing spaces
|
||||||
|
|
||||||
|
// 2. Fix: Trim whitespace to ensure "true " becomes "true"
|
||||||
|
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
|
||||||
|
console.log(`SSL Enabled: ${isSSL}`);
|
||||||
|
console.log('---------------------------');
|
||||||
|
|
||||||
const sequelize = new Sequelize({
|
const sequelize = new Sequelize({
|
||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||||
@ -10,7 +20,7 @@ const sequelize = new Sequelize({
|
|||||||
username: process.env.DB_USER || 'postgres',
|
username: process.env.DB_USER || 'postgres',
|
||||||
password: process.env.DB_PASSWORD || 'postgres',
|
password: process.env.DB_PASSWORD || 'postgres',
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
logging: false, // Disable SQL query logging for cleaner console output
|
logging: false,
|
||||||
pool: {
|
pool: {
|
||||||
min: parseInt(process.env.DB_POOL_MIN || '2', 10),
|
min: parseInt(process.env.DB_POOL_MIN || '2', 10),
|
||||||
max: parseInt(process.env.DB_POOL_MAX || '10', 10),
|
max: parseInt(process.env.DB_POOL_MAX || '10', 10),
|
||||||
@ -18,7 +28,8 @@ const sequelize = new Sequelize({
|
|||||||
idle: 10000,
|
idle: 10000,
|
||||||
},
|
},
|
||||||
dialectOptions: {
|
dialectOptions: {
|
||||||
ssl: process.env.DB_SSL === 'true' ? {
|
// 3. Use the robust boolean we calculated above
|
||||||
|
ssl: isSSL ? {
|
||||||
require: true,
|
require: true,
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
} : false,
|
} : false,
|
||||||
|
|||||||
@ -236,6 +236,7 @@ export class WorkflowController {
|
|||||||
priority: validated.priority as Priority,
|
priority: validated.priority as Priority,
|
||||||
approvalLevels: enrichedApprovalLevels,
|
approvalLevels: enrichedApprovalLevels,
|
||||||
participants: autoGeneratedParticipants,
|
participants: autoGeneratedParticipants,
|
||||||
|
isDraft: parsed.isDraft === true, // Submit by default unless isDraft is explicitly true
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const requestMeta = getRequestMetadata(req);
|
const requestMeta = getRequestMetadata(req);
|
||||||
@ -682,7 +683,10 @@ export class WorkflowController {
|
|||||||
}
|
}
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
const validated = validateUpdateWorkflow(parsed);
|
const validated = validateUpdateWorkflow(parsed);
|
||||||
const updateData: UpdateWorkflowRequest = { ...validated } as any;
|
const updateData: UpdateWorkflowRequest = {
|
||||||
|
...validated,
|
||||||
|
isDraft: parsed.isDraft !== undefined ? (parsed.isDraft === true) : undefined
|
||||||
|
} as any;
|
||||||
if (validated.priority) {
|
if (validated.priority) {
|
||||||
updateData.priority = validated.priority === 'EXPRESS' ? Priority.EXPRESS : Priority.STANDARD;
|
updateData.priority = validated.priority === 'EXPRESS' ? Priority.EXPRESS : Priority.STANDARD;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
// DB constants moved inside functions to ensure secrets are loaded first
|
// DB constants moved inside functions to ensure secrets are loaded first
|
||||||
|
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
|
||||||
async function checkAndCreateDatabase(): Promise<boolean> {
|
async function checkAndCreateDatabase(): Promise<boolean> {
|
||||||
const DB_HOST = process.env.DB_HOST || 'localhost';
|
const DB_HOST = process.env.DB_HOST || 'localhost';
|
||||||
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
|
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
|
||||||
@ -36,6 +36,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
user: DB_USER,
|
user: DB_USER,
|
||||||
password: DB_PASSWORD,
|
password: DB_PASSWORD,
|
||||||
database: 'postgres', // Connect to default postgres database
|
database: 'postgres', // Connect to default postgres database
|
||||||
|
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -64,6 +65,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
user: DB_USER,
|
user: DB_USER,
|
||||||
password: DB_PASSWORD,
|
password: DB_PASSWORD,
|
||||||
database: DB_NAME,
|
database: DB_NAME,
|
||||||
|
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await newDbClient.connect();
|
await newDbClient.connect();
|
||||||
|
|||||||
@ -337,7 +337,7 @@ class GoogleSecretManagerService {
|
|||||||
private getDefaultSecretNames(): string[] {
|
private getDefaultSecretNames(): string[] {
|
||||||
return [
|
return [
|
||||||
// Database
|
// Database
|
||||||
//'DB_PASSWORD',
|
'DB_PASSWORD',
|
||||||
|
|
||||||
// JWT & Session
|
// JWT & Session
|
||||||
'JWT_SECRET',
|
'JWT_SECRET',
|
||||||
|
|||||||
@ -2450,6 +2450,9 @@ export class WorkflowService {
|
|||||||
try {
|
try {
|
||||||
const requestNumber = await generateRequestNumber();
|
const requestNumber = await generateRequestNumber();
|
||||||
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
|
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
|
||||||
|
const isDraftRequested = workflowData.isDraft === true;
|
||||||
|
const initialStatus = isDraftRequested ? WorkflowStatus.DRAFT : WorkflowStatus.PENDING;
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
const workflow = await WorkflowRequest.create({
|
const workflow = await WorkflowRequest.create({
|
||||||
requestNumber,
|
requestNumber,
|
||||||
@ -2461,9 +2464,10 @@ export class WorkflowService {
|
|||||||
currentLevel: 1,
|
currentLevel: 1,
|
||||||
totalLevels: workflowData.approvalLevels.length,
|
totalLevels: workflowData.approvalLevels.length,
|
||||||
totalTatHours,
|
totalTatHours,
|
||||||
status: WorkflowStatus.DRAFT,
|
status: initialStatus,
|
||||||
isDraft: true,
|
isDraft: isDraftRequested,
|
||||||
isDeleted: false
|
isDeleted: false,
|
||||||
|
submissionDate: isDraftRequested ? undefined : now
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create approval levels
|
// Create approval levels
|
||||||
@ -2549,15 +2553,18 @@ export class WorkflowService {
|
|||||||
type: 'created',
|
type: 'created',
|
||||||
user: { userId: initiatorId, name: initiatorName },
|
user: { userId: initiatorId, name: initiatorName },
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
action: 'Initial request submitted',
|
action: isDraftRequested ? 'Draft request created' : 'Initial request submitted',
|
||||||
details: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
|
details: isDraftRequested
|
||||||
|
? `Draft request "${workflowData.title}" created by ${initiatorName}`
|
||||||
|
: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
|
||||||
ipAddress: requestMetadata?.ipAddress || undefined,
|
ipAddress: requestMetadata?.ipAddress || undefined,
|
||||||
userAgent: requestMetadata?.userAgent || undefined
|
userAgent: requestMetadata?.userAgent || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: Notifications are NOT sent here because workflows are created as DRAFTS
|
// If not a draft, initiate the workflow (approvals, notifications, etc.)
|
||||||
// Notifications will be sent in submitWorkflow() when the draft is actually submitted
|
if (!isDraftRequested) {
|
||||||
// This prevents approvers from being notified about draft requests
|
return await this.internalSubmitWorkflow(workflow, now);
|
||||||
|
}
|
||||||
|
|
||||||
return workflow;
|
return workflow;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -3112,6 +3119,9 @@ export class WorkflowService {
|
|||||||
// Only allow full updates (approval levels, participants) for DRAFT workflows
|
// Only allow full updates (approval levels, participants) for DRAFT workflows
|
||||||
const isDraft = (workflow as any).status === WorkflowStatus.DRAFT || (workflow as any).isDraft;
|
const isDraft = (workflow as any).status === WorkflowStatus.DRAFT || (workflow as any).isDraft;
|
||||||
|
|
||||||
|
// Determine if this is a transition from draft to submitted
|
||||||
|
const isTransitioningToSubmitted = updateData.isDraft === false && (workflow as any).isDraft;
|
||||||
|
|
||||||
// Update basic workflow fields
|
// Update basic workflow fields
|
||||||
const basicUpdate: any = {};
|
const basicUpdate: any = {};
|
||||||
if (updateData.title) basicUpdate.title = updateData.title;
|
if (updateData.title) basicUpdate.title = updateData.title;
|
||||||
@ -3119,6 +3129,13 @@ export class WorkflowService {
|
|||||||
if (updateData.priority) basicUpdate.priority = updateData.priority;
|
if (updateData.priority) basicUpdate.priority = updateData.priority;
|
||||||
if (updateData.status) basicUpdate.status = updateData.status;
|
if (updateData.status) basicUpdate.status = updateData.status;
|
||||||
if (updateData.conclusionRemark !== undefined) basicUpdate.conclusionRemark = updateData.conclusionRemark;
|
if (updateData.conclusionRemark !== undefined) basicUpdate.conclusionRemark = updateData.conclusionRemark;
|
||||||
|
if (updateData.isDraft !== undefined) basicUpdate.isDraft = updateData.isDraft;
|
||||||
|
|
||||||
|
// If transitioning, ensure status and submissionDate are set
|
||||||
|
if (isTransitioningToSubmitted) {
|
||||||
|
basicUpdate.status = WorkflowStatus.PENDING;
|
||||||
|
basicUpdate.submissionDate = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
await workflow.update(basicUpdate);
|
await workflow.update(basicUpdate);
|
||||||
|
|
||||||
@ -3267,8 +3284,13 @@ export class WorkflowService {
|
|||||||
logger.info(`Marked ${deleteResult[0]} documents as deleted in database (out of ${updateData.deleteDocumentIds.length} requested)`);
|
logger.info(`Marked ${deleteResult[0]} documents as deleted in database (out of ${updateData.deleteDocumentIds.length} requested)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the workflow instance to get latest data (without associations to avoid the error)
|
// If transitioning, call the internal submission logic (notifications, TAT, etc.)
|
||||||
// The associations issue occurs when trying to include them, so we skip that
|
if (isTransitioningToSubmitted) {
|
||||||
|
logger.info(`[Workflow] Transitioning draft ${actualRequestId} to submitted state`);
|
||||||
|
return await this.internalSubmitWorkflow(workflow, (workflow as any).submissionDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the workflow instance to get latest data
|
||||||
const refreshed = await WorkflowRequest.findByPk(actualRequestId);
|
const refreshed = await WorkflowRequest.findByPk(actualRequestId);
|
||||||
return refreshed;
|
return refreshed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -3290,160 +3312,169 @@ export class WorkflowService {
|
|||||||
const workflow = await this.findWorkflowByIdentifier(requestId);
|
const workflow = await this.findWorkflowByIdentifier(requestId);
|
||||||
if (!workflow) return null;
|
if (!workflow) return null;
|
||||||
|
|
||||||
// Get the actual requestId (UUID) - handle both UUID and requestNumber cases
|
|
||||||
const actualRequestId = (workflow as any).getDataValue
|
|
||||||
? (workflow as any).getDataValue('requestId')
|
|
||||||
: (workflow as any).requestId;
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const updated = await workflow.update({
|
return await this.internalSubmitWorkflow(workflow, now);
|
||||||
status: WorkflowStatus.PENDING,
|
|
||||||
isDraft: false,
|
|
||||||
submissionDate: now
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get initiator details for activity logging
|
|
||||||
const initiatorId = (updated as any).initiatorId;
|
|
||||||
const initiator = initiatorId ? await User.findByPk(initiatorId) : null;
|
|
||||||
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
|
|
||||||
const workflowTitle = (updated as any).title || 'Request';
|
|
||||||
const requestNumber = (updated as any).requestNumber;
|
|
||||||
|
|
||||||
// Check if this was a previously saved draft (has activity history before submission)
|
|
||||||
// or a direct submission (createWorkflow + submitWorkflow in same flow)
|
|
||||||
const { Activity } = require('@models/Activity');
|
|
||||||
const existingActivities = await Activity.count({
|
|
||||||
where: { requestId: actualRequestId }
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only log "Request submitted" if this is a draft being submitted (has prior activities)
|
|
||||||
// For direct submissions, createWorkflow already logs "Initial request submitted"
|
|
||||||
if (existingActivities > 1) {
|
|
||||||
// This is a saved draft being submitted later
|
|
||||||
activityService.log({
|
|
||||||
requestId: actualRequestId,
|
|
||||||
type: 'submitted',
|
|
||||||
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'Draft submitted',
|
|
||||||
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Direct submission - just update the status, createWorkflow already logged the activity
|
|
||||||
activityService.log({
|
|
||||||
requestId: actualRequestId,
|
|
||||||
type: 'submitted',
|
|
||||||
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'Request submitted',
|
|
||||||
details: `Request "${workflowTitle}" submitted for approval`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = await ApprovalLevel.findOne({
|
|
||||||
where: { requestId: actualRequestId, levelNumber: (updated as any).currentLevel || 1 }
|
|
||||||
});
|
|
||||||
if (current) {
|
|
||||||
// Set the first level's start time and schedule TAT jobs
|
|
||||||
await current.update({
|
|
||||||
levelStartTime: now,
|
|
||||||
tatStartTime: now,
|
|
||||||
status: ApprovalStatus.IN_PROGRESS
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log assignment activity for the first approver (similar to createWorkflow)
|
|
||||||
activityService.log({
|
|
||||||
requestId: actualRequestId,
|
|
||||||
type: 'assignment',
|
|
||||||
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
action: 'Assigned to approver',
|
|
||||||
details: `Request assigned to ${(current as any).approverName || (current as any).approverEmail || 'approver'} for review`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schedule TAT notification jobs for the first level
|
|
||||||
try {
|
|
||||||
const workflowPriority = (updated as any).priority || 'STANDARD';
|
|
||||||
await tatSchedulerService.scheduleTatJobs(
|
|
||||||
actualRequestId,
|
|
||||||
(current as any).levelId,
|
|
||||||
(current as any).approverId,
|
|
||||||
Number((current as any).tatHours),
|
|
||||||
now,
|
|
||||||
workflowPriority // Pass workflow priority (EXPRESS = 24/7, STANDARD = working hours)
|
|
||||||
);
|
|
||||||
logger.info(`[Workflow] TAT jobs scheduled for first level of request ${requestNumber} (Priority: ${workflowPriority})`);
|
|
||||||
} catch (tatError) {
|
|
||||||
logger.error(`[Workflow] Failed to schedule TAT jobs:`, tatError);
|
|
||||||
// Don't fail the submission if TAT scheduling fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notifications when workflow is submitted (not when created as draft)
|
|
||||||
// Send notification to INITIATOR confirming submission
|
|
||||||
await notificationService.sendToUsers([initiatorId], {
|
|
||||||
title: 'Request Submitted Successfully',
|
|
||||||
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
|
|
||||||
requestNumber: requestNumber,
|
|
||||||
requestId: actualRequestId,
|
|
||||||
url: `/request/${requestNumber}`,
|
|
||||||
type: 'request_submitted',
|
|
||||||
priority: 'MEDIUM'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send notification to FIRST APPROVER for assignment
|
|
||||||
await notificationService.sendToUsers([(current as any).approverId], {
|
|
||||||
title: 'New Request Assigned',
|
|
||||||
body: `${workflowTitle}`,
|
|
||||||
requestNumber: requestNumber,
|
|
||||||
requestId: actualRequestId,
|
|
||||||
url: `/request/${requestNumber}`,
|
|
||||||
type: 'assignment',
|
|
||||||
priority: 'HIGH',
|
|
||||||
actionRequired: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notifications to SPECTATORS (in-app, email, and web push)
|
|
||||||
// Moved outside the if(current) block to ensure spectators are always notified on submission
|
|
||||||
try {
|
|
||||||
logger.info(`[Workflow] Querying spectators for request ${requestNumber} (requestId: ${actualRequestId})`);
|
|
||||||
const spectators = await Participant.findAll({
|
|
||||||
where: {
|
|
||||||
requestId: actualRequestId, // Use the actual UUID requestId
|
|
||||||
participantType: ParticipantType.SPECTATOR,
|
|
||||||
isActive: true,
|
|
||||||
notificationEnabled: true
|
|
||||||
},
|
|
||||||
attributes: ['userId', 'userEmail', 'userName']
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`[Workflow] Found ${spectators.length} active spectators for request ${requestNumber}`);
|
|
||||||
|
|
||||||
if (spectators.length > 0) {
|
|
||||||
const spectatorUserIds = spectators.map((s: any) => s.userId);
|
|
||||||
logger.info(`[Workflow] Sending notifications to ${spectatorUserIds.length} spectators: ${spectatorUserIds.join(', ')}`);
|
|
||||||
|
|
||||||
await notificationService.sendToUsers(spectatorUserIds, {
|
|
||||||
title: 'Added to Request',
|
|
||||||
body: `You have been added as a spectator to request ${requestNumber}: ${workflowTitle}`,
|
|
||||||
requestNumber: requestNumber,
|
|
||||||
requestId: actualRequestId,
|
|
||||||
url: `/request/${requestNumber}`,
|
|
||||||
type: 'spectator_added',
|
|
||||||
priority: 'MEDIUM'
|
|
||||||
});
|
|
||||||
logger.info(`[Workflow] Successfully sent notifications to ${spectators.length} spectators for request ${requestNumber}`);
|
|
||||||
} else {
|
|
||||||
logger.info(`[Workflow] No active spectators found for request ${requestNumber} (requestId: ${actualRequestId})`);
|
|
||||||
}
|
|
||||||
} catch (spectatorError) {
|
|
||||||
logger.error(`[Workflow] Failed to send spectator notifications for request ${requestNumber} (requestId: ${actualRequestId}):`, spectatorError);
|
|
||||||
// Don't fail the submission if spectator notifications fail
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to submit workflow ${requestId}:`, error);
|
logger.error(`Failed to submit workflow ${requestId}:`, error);
|
||||||
throw new Error('Failed to submit workflow');
|
throw new Error('Failed to submit workflow');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to handle workflow submission logic (status update, notifications, TAT scheduling)
|
||||||
|
* Centralized here to support both direct creation-submission and draft-to-submission flows.
|
||||||
|
*/
|
||||||
|
private async internalSubmitWorkflow(workflow: WorkflowRequest, now: Date): Promise<WorkflowRequest> {
|
||||||
|
// Get the actual requestId (UUID) - handle both UUID and requestNumber cases
|
||||||
|
const actualRequestId = (workflow as any).getDataValue
|
||||||
|
? (workflow as any).getDataValue('requestId')
|
||||||
|
: (workflow as any).requestId;
|
||||||
|
|
||||||
|
const updated = await workflow.update({
|
||||||
|
status: WorkflowStatus.PENDING,
|
||||||
|
isDraft: false,
|
||||||
|
submissionDate: now
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get initiator details for activity logging
|
||||||
|
const initiatorId = (updated as any).initiatorId;
|
||||||
|
const initiator = initiatorId ? await User.findByPk(initiatorId) : null;
|
||||||
|
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
|
||||||
|
const workflowTitle = (updated as any).title || 'Request';
|
||||||
|
const requestNumber = (updated as any).requestNumber;
|
||||||
|
|
||||||
|
// Check if this was a previously saved draft (has activity history before submission)
|
||||||
|
// or a direct submission (createWorkflow + submitWorkflow in same flow)
|
||||||
|
const { Activity } = require('@models/Activity');
|
||||||
|
const existingActivities = await Activity.count({
|
||||||
|
where: { requestId: actualRequestId }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only log "Request submitted" if this is a draft being submitted (has prior activities)
|
||||||
|
// For direct submissions, createWorkflow already logs "Initial request submitted"
|
||||||
|
if (existingActivities > 1) {
|
||||||
|
// This is a saved draft being submitted later
|
||||||
|
activityService.log({
|
||||||
|
requestId: actualRequestId,
|
||||||
|
type: 'submitted',
|
||||||
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
action: 'Draft submitted',
|
||||||
|
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Direct submission - just update the status, createWorkflow already logged the activity
|
||||||
|
activityService.log({
|
||||||
|
requestId: actualRequestId,
|
||||||
|
type: 'submitted',
|
||||||
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
action: 'Request submitted',
|
||||||
|
details: `Request "${workflowTitle}" submitted for approval`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = await ApprovalLevel.findOne({
|
||||||
|
where: { requestId: actualRequestId, levelNumber: (updated as any).currentLevel || 1 }
|
||||||
|
});
|
||||||
|
if (current) {
|
||||||
|
// Set the first level's start time and schedule TAT jobs
|
||||||
|
await current.update({
|
||||||
|
levelStartTime: now,
|
||||||
|
tatStartTime: now,
|
||||||
|
status: ApprovalStatus.IN_PROGRESS
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log assignment activity for the first approver (similar to createWorkflow)
|
||||||
|
activityService.log({
|
||||||
|
requestId: actualRequestId,
|
||||||
|
type: 'assignment',
|
||||||
|
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
action: 'Assigned to approver',
|
||||||
|
details: `Request assigned to ${(current as any).approverName || (current as any).approverEmail || 'approver'} for review`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule TAT notification jobs for the first level
|
||||||
|
try {
|
||||||
|
const workflowPriority = (updated as any).priority || 'STANDARD';
|
||||||
|
await tatSchedulerService.scheduleTatJobs(
|
||||||
|
actualRequestId,
|
||||||
|
(current as any).levelId,
|
||||||
|
(current as any).approverId,
|
||||||
|
Number((current as any).tatHours),
|
||||||
|
now,
|
||||||
|
workflowPriority // Pass workflow priority (EXPRESS = 24/7, STANDARD = working hours)
|
||||||
|
);
|
||||||
|
logger.info(`[Workflow] TAT jobs scheduled for first level of request ${requestNumber} (Priority: ${workflowPriority})`);
|
||||||
|
} catch (tatError) {
|
||||||
|
logger.error(`[Workflow] Failed to schedule TAT jobs:`, tatError);
|
||||||
|
// Don't fail the submission if TAT scheduling fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notifications when workflow is submitted (not when created as draft)
|
||||||
|
// Send notification to INITIATOR confirming submission
|
||||||
|
await notificationService.sendToUsers([initiatorId], {
|
||||||
|
title: 'Request Submitted Successfully',
|
||||||
|
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`,
|
||||||
|
requestNumber: requestNumber,
|
||||||
|
requestId: actualRequestId,
|
||||||
|
url: `/request/${requestNumber}`,
|
||||||
|
type: 'request_submitted',
|
||||||
|
priority: 'MEDIUM'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send notification to FIRST APPROVER for assignment
|
||||||
|
await notificationService.sendToUsers([(current as any).approverId], {
|
||||||
|
title: 'New Request Assigned',
|
||||||
|
body: `${workflowTitle}`,
|
||||||
|
requestNumber: requestNumber,
|
||||||
|
requestId: actualRequestId,
|
||||||
|
url: `/request/${requestNumber}`,
|
||||||
|
type: 'assignment',
|
||||||
|
priority: 'HIGH',
|
||||||
|
actionRequired: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notifications to SPECTATORS (in-app, email, and web push)
|
||||||
|
// Moved outside the if(current) block to ensure spectators are always notified on submission
|
||||||
|
try {
|
||||||
|
logger.info(`[Workflow] Querying spectators for request ${requestNumber} (requestId: ${actualRequestId})`);
|
||||||
|
const spectators = await Participant.findAll({
|
||||||
|
where: {
|
||||||
|
requestId: actualRequestId, // Use the actual UUID requestId
|
||||||
|
participantType: ParticipantType.SPECTATOR,
|
||||||
|
isActive: true,
|
||||||
|
notificationEnabled: true
|
||||||
|
},
|
||||||
|
attributes: ['userId', 'userEmail', 'userName']
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[Workflow] Found ${spectators.length} active spectators for request ${requestNumber}`);
|
||||||
|
|
||||||
|
if (spectators.length > 0) {
|
||||||
|
const spectatorUserIds = spectators.map((s: any) => s.userId);
|
||||||
|
logger.info(`[Workflow] Sending notifications to ${spectatorUserIds.length} spectators: ${spectatorUserIds.join(', ')}`);
|
||||||
|
|
||||||
|
await notificationService.sendToUsers(spectatorUserIds, {
|
||||||
|
title: 'Added to Request',
|
||||||
|
body: `You have been added as a spectator to request ${requestNumber}: ${workflowTitle}`,
|
||||||
|
requestNumber: requestNumber,
|
||||||
|
requestId: actualRequestId,
|
||||||
|
url: `/request/${requestNumber}`,
|
||||||
|
type: 'spectator_added',
|
||||||
|
priority: 'MEDIUM'
|
||||||
|
});
|
||||||
|
logger.info(`[Workflow] Successfully sent notifications to ${spectators.length} spectators for request ${requestNumber}`);
|
||||||
|
} else {
|
||||||
|
logger.info(`[Workflow] No active spectators found for request ${requestNumber} (requestId: ${actualRequestId})`);
|
||||||
|
}
|
||||||
|
} catch (spectatorError) {
|
||||||
|
logger.error(`[Workflow] Failed to send spectator notifications for request ${requestNumber} (requestId: ${actualRequestId}):`, spectatorError);
|
||||||
|
// Don't fail the submission if spectator notifications fail
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export interface CreateWorkflowRequest {
|
|||||||
priority: Priority;
|
priority: Priority;
|
||||||
approvalLevels: CreateApprovalLevel[];
|
approvalLevels: CreateApprovalLevel[];
|
||||||
participants?: CreateParticipant[];
|
participants?: CreateParticipant[];
|
||||||
|
isDraft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateWorkflowRequest {
|
export interface UpdateWorkflowRequest {
|
||||||
@ -42,6 +43,7 @@ export interface UpdateWorkflowRequest {
|
|||||||
participants?: CreateParticipant[];
|
participants?: CreateParticipant[];
|
||||||
// Document updates (add new documents via multipart, delete via IDs)
|
// Document updates (add new documents via multipart, delete via IDs)
|
||||||
deleteDocumentIds?: string[];
|
deleteDocumentIds?: string[];
|
||||||
|
isDraft?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateApprovalLevel {
|
export interface CreateApprovalLevel {
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export const createWorkflowSchema = z.object({
|
|||||||
priorityUi: z.string().optional(),
|
priorityUi: z.string().optional(),
|
||||||
templateId: z.string().optional(),
|
templateId: z.string().optional(),
|
||||||
ccList: z.array(z.any()).optional(),
|
ccList: z.array(z.any()).optional(),
|
||||||
|
isDraft: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateWorkflowSchema = z.object({
|
export const updateWorkflowSchema = z.object({
|
||||||
@ -73,6 +74,7 @@ export const updateWorkflowSchema = z.object({
|
|||||||
notificationEnabled: z.boolean().optional(),
|
notificationEnabled: z.boolean().optional(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
deleteDocumentIds: z.array(z.string().uuid()).optional(),
|
deleteDocumentIds: z.array(z.string().uuid()).optional(),
|
||||||
|
isDraft: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper to validate UUID or requestNumber format
|
// Helper to validate UUID or requestNumber format
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user