uat and prod flow code rlated to admin template and minor updates now merged
This commit is contained in:
commit
b11e542a59
@ -1 +1,5 @@
|
||||
<<<<<<<< HEAD:build/assets/conclusionApi-BIX8LEl5.js.map
|
||||
{"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"}
|
||||
========
|
||||
{"version":3,"file":"conclusionApi-xBwvOJP0.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 */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"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,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||
>>>>>>>> f456fb8af9d2c635501f9b17ab153d5190750265:build/assets/conclusionApi-xBwvOJP0.js.map
|
||||
|
||||
2
build/assets/conclusionApi-xBwvOJP0.js
Normal file
2
build/assets/conclusionApi-xBwvOJP0.js
Normal file
@ -0,0 +1,2 @@
|
||||
import{a as t}from"./index-D5U31xpx.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||
//# sourceMappingURL=conclusionApi-xBwvOJP0.js.map
|
||||
5
build/assets/conclusionApi-xBwvOJP0.js.map
Normal file
5
build/assets/conclusionApi-xBwvOJP0.js.map
Normal file
@ -0,0 +1,5 @@
|
||||
<<<<<<<< HEAD:build/assets/conclusionApi-BIX8LEl5.js.map
|
||||
{"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"}
|
||||
========
|
||||
{"version":3,"file":"conclusionApi-xBwvOJP0.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 */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"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,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}
|
||||
>>>>>>>> f456fb8af9d2c635501f9b17ab153d5190750265:build/assets/conclusionApi-xBwvOJP0.js.map
|
||||
64
build/assets/index-D5U31xpx.js
Normal file
64
build/assets/index-D5U31xpx.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-D5U31xpx.js.map
Normal file
1
build/assets/index-D5U31xpx.js.map
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-DwXE9Ynd.css
Normal file
1
build/assets/index-DwXE9Ynd.css
Normal file
File diff suppressed because one or more lines are too long
@ -1,12 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- CSP: Allows blob URLs for file previews and cross-origin API calls during development -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: https: blob:; connect-src 'self' blob: data: http://localhost:5000 http://localhost:3000 ws://localhost:5000 ws://localhost:3000 wss://localhost:5000 wss://localhost:3000; frame-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self'; img-src 'self' data: https: blob:; connect-src 'self' blob: data: http://localhost:5000 http://localhost:3000 ws://localhost:5000 ws://localhost:3000 wss://localhost:5000 wss://localhost:3000; frame-src 'self' blob:; font-src 'self' https://fonts.gstatic.com data:; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
||||
<link rel="icon" type="image/svg+xml" href="/royal_enfield_logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
||||
<meta name="description"
|
||||
content="Royal Enfield Approval & Request Management Portal - Streamlined approval workflows for enterprise operations" />
|
||||
<meta name="theme-color" content="#2d4a3e" />
|
||||
<title>Royal Enfield | Approval Portal</title>
|
||||
|
||||
@ -52,18 +55,19 @@
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
<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/radix-vendor-DIkYAdWy.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-sjs6YRoy.js">
|
||||
<script type="module" crossorigin src="/assets/index-D5U31xpx.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-BmvKDhMD.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/router-vendor-AvM4PHvP.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CPRbj7YF.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-CRr9x_Jp.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DwXE9Ynd.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -4,8 +4,8 @@
|
||||
"description": "Royal Enfield Workflow Management System - Backend API (TypeScript)",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"start": "npm install && npm run setup && npm run build && npm run start:prod",
|
||||
"dev": "npm run setup && npm run migrate && nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
|
||||
"start": "npm run build && npm run start:prod && npm run setup",
|
||||
"dev": "npm run setup && nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
|
||||
"dev:no-setup": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
|
||||
"build": "tsc && tsc-alias",
|
||||
"build:watch": "tsc --watch",
|
||||
|
||||
12
src/app.ts
12
src/app.ts
@ -16,17 +16,7 @@ import path from 'path';
|
||||
// Load environment variables from .env file first
|
||||
dotenv.config();
|
||||
|
||||
// Initialize Google Secret Manager (async, but we'll wait for it in server.ts)
|
||||
// This will merge secrets from GCS into process.env if USE_GOOGLE_SECRET_MANAGER=true
|
||||
// Export initialization function so server.ts can await it before starting
|
||||
export async function initializeSecrets(): Promise<void> {
|
||||
try {
|
||||
await initializeGoogleSecretManager();
|
||||
} catch (error) {
|
||||
// Log error but don't throw - allow fallback to .env
|
||||
console.error('⚠️ Failed to initialize Google Secret Manager, using .env file:', error);
|
||||
}
|
||||
}
|
||||
// Secrets are now initialized in server.ts before app is imported
|
||||
|
||||
const app: express.Application = express();
|
||||
const userService = new UserService();
|
||||
|
||||
@ -19,6 +19,7 @@ export class TemplateController {
|
||||
}
|
||||
|
||||
const {
|
||||
// New fields
|
||||
templateName,
|
||||
templateCode,
|
||||
templateDescription,
|
||||
@ -30,20 +31,34 @@ export class TemplateController {
|
||||
userFieldMappings,
|
||||
dynamicApproverConfig,
|
||||
isActive,
|
||||
|
||||
// Legacy fields (from frontend)
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
approvers,
|
||||
suggestedSLA
|
||||
} = req.body;
|
||||
|
||||
if (!templateName) {
|
||||
// Map legacy to new
|
||||
const finalTemplateName = templateName || name;
|
||||
const finalTemplateDescription = templateDescription || description;
|
||||
const finalTemplateCategory = templateCategory || category;
|
||||
const finalApprovalLevelsConfig = approvalLevelsConfig || approvers;
|
||||
const finalDefaultTatHours = defaultTatHours || suggestedSLA;
|
||||
|
||||
if (!finalTemplateName) {
|
||||
return ResponseHandler.error(res, 'Template name is required', 400);
|
||||
}
|
||||
|
||||
const template = await this.templateService.createTemplate(userId, {
|
||||
templateName,
|
||||
templateName: finalTemplateName,
|
||||
templateCode,
|
||||
templateDescription,
|
||||
templateCategory,
|
||||
templateDescription: finalTemplateDescription,
|
||||
templateCategory: finalTemplateCategory,
|
||||
workflowType,
|
||||
approvalLevelsConfig,
|
||||
defaultTatHours: defaultTatHours ? parseFloat(defaultTatHours) : undefined,
|
||||
approvalLevelsConfig: finalApprovalLevelsConfig,
|
||||
defaultTatHours: finalDefaultTatHours ? parseFloat(finalDefaultTatHours) : undefined,
|
||||
formStepsConfig,
|
||||
userFieldMappings,
|
||||
dynamicApproverConfig,
|
||||
@ -149,14 +164,21 @@ export class TemplateController {
|
||||
userFieldMappings,
|
||||
dynamicApproverConfig,
|
||||
isActive,
|
||||
|
||||
// Legacy
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
approvers,
|
||||
suggestedSLA
|
||||
} = req.body;
|
||||
|
||||
const template = await this.templateService.updateTemplate(templateId, userId, {
|
||||
templateName,
|
||||
templateDescription,
|
||||
templateCategory,
|
||||
approvalLevelsConfig,
|
||||
defaultTatHours: defaultTatHours ? parseFloat(defaultTatHours) : undefined,
|
||||
templateName: templateName || name,
|
||||
templateDescription: templateDescription || description,
|
||||
templateCategory: templateCategory || category,
|
||||
approvalLevelsConfig: approvalLevelsConfig || approvers,
|
||||
defaultTatHours: (defaultTatHours || suggestedSLA) ? parseFloat(defaultTatHours || suggestedSLA) : undefined,
|
||||
formStepsConfig,
|
||||
userFieldMappings,
|
||||
dynamicApproverConfig,
|
||||
|
||||
130
src/controllers/workflowTemplate.controller.ts
Normal file
130
src/controllers/workflowTemplate.controller.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { WorkflowTemplate } from '../models';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
export const createTemplate = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { name, description, category, priority, estimatedTime, approvers, suggestedSLA } = req.body;
|
||||
const userId = (req as any).user?.userId;
|
||||
|
||||
const template = await WorkflowTemplate.create({
|
||||
templateName: name,
|
||||
templateDescription: description,
|
||||
templateCategory: category,
|
||||
approvalLevelsConfig: approvers,
|
||||
defaultTatHours: suggestedSLA,
|
||||
createdBy: userId,
|
||||
isActive: true,
|
||||
isSystemTemplate: false,
|
||||
usageCount: 0
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Workflow template created successfully',
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error creating workflow template:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to create workflow template',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getTemplates = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const templates = await WorkflowTemplate.findAll({
|
||||
where: { isActive: true },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: templates
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching workflow templates:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch workflow templates',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const updateTemplate = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, category, approvers, suggestedSLA, isActive } = req.body;
|
||||
|
||||
const updates: any = {};
|
||||
if (name) updates.templateName = name;
|
||||
if (description) updates.templateDescription = description;
|
||||
if (category) updates.templateCategory = category;
|
||||
if (approvers) updates.approvalLevelsConfig = approvers;
|
||||
if (suggestedSLA) updates.defaultTatHours = suggestedSLA;
|
||||
if (isActive !== undefined) updates.isActive = isActive;
|
||||
|
||||
const template = await WorkflowTemplate.findByPk(id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow template not found'
|
||||
});
|
||||
}
|
||||
|
||||
await template.update(updates);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Workflow template updated successfully',
|
||||
data: template
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error updating workflow template:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update workflow template',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTemplate = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const template = await WorkflowTemplate.findByPk(id);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Workflow template not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Hard delete or Soft delete based on preference.
|
||||
// Since we have isActive flag, let's use that (Soft Delete) or just destroy if it's unused.
|
||||
// For now, let's do a hard delete to match the expectation of "Delete" in the UI
|
||||
// unless there are FK constraints (which sequelize handles).
|
||||
// Actually, safer to Soft Delete by setting isActive = false if we want history,
|
||||
// but user asked for Delete. Let's do destroy.
|
||||
await template.destroy();
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Workflow template deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error deleting workflow template:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to delete workflow template',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
};
|
||||
115
src/migrations/20260123-fix-template-id-schema.ts
Normal file
115
src/migrations/20260123-fix-template-id-schema.ts
Normal file
@ -0,0 +1,115 @@
|
||||
|
||||
import { QueryInterface, DataTypes } from 'sequelize';
|
||||
|
||||
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||
try {
|
||||
const tableDescription = await queryInterface.describeTable('workflow_templates');
|
||||
|
||||
// 1. Rename id -> template_id
|
||||
if (tableDescription.id && !tableDescription.template_id) {
|
||||
console.log('Renaming id to template_id...');
|
||||
await queryInterface.renameColumn('workflow_templates', 'id', 'template_id');
|
||||
}
|
||||
|
||||
// 2. Rename name -> template_name
|
||||
if (tableDescription.name && !tableDescription.template_name) {
|
||||
console.log('Renaming name to template_name...');
|
||||
await queryInterface.renameColumn('workflow_templates', 'name', 'template_name');
|
||||
}
|
||||
|
||||
// 3. Rename description -> template_description
|
||||
if (tableDescription.description && !tableDescription.template_description) {
|
||||
console.log('Renaming description to template_description...');
|
||||
await queryInterface.renameColumn('workflow_templates', 'description', 'template_description');
|
||||
}
|
||||
|
||||
// 4. Rename category -> template_category
|
||||
if (tableDescription.category && !tableDescription.template_category) {
|
||||
console.log('Renaming category to template_category...');
|
||||
await queryInterface.renameColumn('workflow_templates', 'category', 'template_category');
|
||||
}
|
||||
|
||||
// 5. Rename suggested_sla -> default_tat_hours
|
||||
if (tableDescription.suggested_sla && !tableDescription.default_tat_hours) {
|
||||
console.log('Renaming suggested_sla to default_tat_hours...');
|
||||
await queryInterface.renameColumn('workflow_templates', 'suggested_sla', 'default_tat_hours');
|
||||
}
|
||||
|
||||
// 6. Add missing columns
|
||||
if (!tableDescription.template_code) {
|
||||
console.log('Adding template_code column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'template_code', {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
unique: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!tableDescription.workflow_type) {
|
||||
console.log('Adding workflow_type column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'workflow_type', {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!tableDescription.approval_levels_config) {
|
||||
console.log('Adding approval_levels_config column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'approval_levels_config', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!tableDescription.form_steps_config) {
|
||||
console.log('Adding form_steps_config column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'form_steps_config', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!tableDescription.user_field_mappings) {
|
||||
console.log('Adding user_field_mappings column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'user_field_mappings', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!tableDescription.dynamic_approver_config) {
|
||||
console.log('Adding dynamic_approver_config column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'dynamic_approver_config', {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!tableDescription.is_system_template) {
|
||||
console.log('Adding is_system_template column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'is_system_template', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
}
|
||||
|
||||
if (!tableDescription.usage_count) {
|
||||
console.log('Adding usage_count column...');
|
||||
await queryInterface.addColumn('workflow_templates', 'usage_count', {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ Schema validation/fix complete');
|
||||
} catch (error) {
|
||||
console.error('Error in schema fix migration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||
// Revert is complex/risky effectively, skipping for this fix-forward migration
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { DataTypes, Model, Optional } from 'sequelize';
|
||||
import { sequelize } from '@config/database';
|
||||
import { sequelize } from '../config/database';
|
||||
import { User } from './User';
|
||||
|
||||
interface WorkflowTemplateAttributes {
|
||||
@ -22,9 +22,9 @@ interface WorkflowTemplateAttributes {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface WorkflowTemplateCreationAttributes extends Optional<WorkflowTemplateAttributes, 'templateId' | 'templateCode' | 'templateDescription' | 'templateCategory' | 'workflowType' | 'approvalLevelsConfig' | 'defaultTatHours' | 'formStepsConfig' | 'userFieldMappings' | 'dynamicApproverConfig' | 'createdBy' | 'createdAt' | 'updatedAt'> {}
|
||||
interface WorkflowTemplateCreationAttributes extends Optional<WorkflowTemplateAttributes, 'templateId' | 'templateCode' | 'templateDescription' | 'templateCategory' | 'workflowType' | 'approvalLevelsConfig' | 'defaultTatHours' | 'formStepsConfig' | 'userFieldMappings' | 'dynamicApproverConfig' | 'createdBy' | 'createdAt' | 'updatedAt'> { }
|
||||
|
||||
class WorkflowTemplate extends Model<WorkflowTemplateAttributes, WorkflowTemplateCreationAttributes> implements WorkflowTemplateAttributes {
|
||||
export class WorkflowTemplate extends Model<WorkflowTemplateAttributes, WorkflowTemplateCreationAttributes> implements WorkflowTemplateAttributes {
|
||||
public templateId!: string;
|
||||
public templateName!: string;
|
||||
public templateCode?: string;
|
||||
@ -175,6 +175,3 @@ WorkflowTemplate.belongsTo(User, {
|
||||
foreignKey: 'createdBy',
|
||||
targetKey: 'userId'
|
||||
});
|
||||
|
||||
export { WorkflowTemplate };
|
||||
|
||||
|
||||
@ -20,12 +20,12 @@ import { DealerClaimDetails } from './DealerClaimDetails';
|
||||
import { DealerProposalDetails } from './DealerProposalDetails';
|
||||
import { DealerCompletionDetails } from './DealerCompletionDetails';
|
||||
import { DealerProposalCostItem } from './DealerProposalCostItem';
|
||||
import { WorkflowTemplate } from './WorkflowTemplate';
|
||||
import { InternalOrder } from './InternalOrder';
|
||||
import { ClaimBudgetTracking } from './ClaimBudgetTracking';
|
||||
import { Dealer } from './Dealer';
|
||||
import { ActivityType } from './ActivityType';
|
||||
import { DealerClaimHistory } from './DealerClaimHistory';
|
||||
import { WorkflowTemplate } from './WorkflowTemplate';
|
||||
|
||||
// Define associations
|
||||
const defineAssociations = () => {
|
||||
@ -170,11 +170,11 @@ export {
|
||||
ConclusionRemark,
|
||||
RequestSummary,
|
||||
SharedSummary,
|
||||
WorkflowTemplate,
|
||||
DealerClaimDetails,
|
||||
DealerProposalDetails,
|
||||
DealerCompletionDetails,
|
||||
DealerProposalCostItem,
|
||||
WorkflowTemplate,
|
||||
InternalOrder,
|
||||
ClaimBudgetTracking,
|
||||
Dealer,
|
||||
|
||||
16
src/routes/workflowTemplate.routes.ts
Normal file
16
src/routes/workflowTemplate.routes.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import { createTemplate, getTemplates, updateTemplate, deleteTemplate } from '../controllers/workflowTemplate.controller';
|
||||
import { authenticateToken } from '../middlewares/auth.middleware';
|
||||
import { requireAdmin } from '../middlewares/authorization.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Public route to get templates (authenticated users)
|
||||
router.get('/', authenticateToken, getTemplates);
|
||||
|
||||
// Admin only route to create templates
|
||||
router.post('/', authenticateToken, requireAdmin, createTemplate);
|
||||
router.put('/:id', authenticateToken, requireAdmin, updateTemplate);
|
||||
router.delete('/:id', authenticateToken, requireAdmin, deleteTemplate);
|
||||
|
||||
export default router;
|
||||
@ -11,8 +11,8 @@
|
||||
*/
|
||||
|
||||
import { Client } from 'pg';
|
||||
import { sequelize } from '../config/database';
|
||||
import { QueryTypes } from 'sequelize';
|
||||
import { initializeGoogleSecretManager } from '../services/googleSecretManager.service';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import dotenv from 'dotenv';
|
||||
@ -21,14 +21,15 @@ import path from 'path';
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const DB_HOST = process.env.DB_HOST || 'localhost';
|
||||
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
|
||||
const DB_USER = process.env.DB_USER || 'postgres';
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || '';
|
||||
const DB_NAME = process.env.DB_NAME || 'royal_enfield_workflow';
|
||||
// DB constants moved inside functions to ensure secrets are loaded first
|
||||
|
||||
async function checkAndCreateDatabase(): Promise<boolean> {
|
||||
const DB_HOST = process.env.DB_HOST || 'localhost';
|
||||
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
|
||||
const DB_USER = process.env.DB_USER || 'postgres';
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || '';
|
||||
const DB_NAME = process.env.DB_NAME || 'royal_enfield_workflow';
|
||||
|
||||
const client = new Client({
|
||||
host: DB_HOST,
|
||||
port: DB_PORT,
|
||||
@ -136,6 +137,7 @@ async function runMigrations(): Promise<void> {
|
||||
const m41 = require('../migrations/20250120-create-dealers-table');
|
||||
const m42 = require('../migrations/20250125-create-activity-types');
|
||||
const m43 = require('../migrations/20260113-redesign-dealer-claim-history');
|
||||
const m44 = require('../migrations/20260123-fix-template-id-schema');
|
||||
|
||||
const migrations = [
|
||||
{ name: '2025103000-create-users', module: m0 },
|
||||
@ -184,8 +186,11 @@ async function runMigrations(): Promise<void> {
|
||||
{ name: '20250120-create-dealers-table', module: m41 },
|
||||
{ name: '20250125-create-activity-types', module: m42 },
|
||||
{ name: '20260113-redesign-dealer-claim-history', module: m43 },
|
||||
{ name: '20260123-fix-template-id-schema', module: m44 },
|
||||
];
|
||||
|
||||
// Dynamically import sequelize after secrets are loaded
|
||||
const { sequelize } = require('../config/database');
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
// Ensure migrations tracking table exists
|
||||
@ -201,10 +206,10 @@ async function runMigrations(): Promise<void> {
|
||||
}
|
||||
|
||||
// Get already executed migrations
|
||||
const executedResults = await sequelize.query<{ name: string }>(
|
||||
const executedResults = await sequelize.query(
|
||||
'SELECT name FROM migrations ORDER BY id',
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
) as { name: string }[];
|
||||
const executedMigrations = executedResults.map(r => r.name);
|
||||
|
||||
// Find pending migrations
|
||||
@ -252,6 +257,7 @@ async function runMigrations(): Promise<void> {
|
||||
async function testConnection(): Promise<void> {
|
||||
try {
|
||||
console.log('🔌 Testing database connection...');
|
||||
const { sequelize } = require('../config/database');
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Database connection established!');
|
||||
} catch (error: any) {
|
||||
@ -266,6 +272,10 @@ async function autoSetup(): Promise<void> {
|
||||
console.log('========================================\n');
|
||||
|
||||
try {
|
||||
// Step 0: Initialize secrets
|
||||
console.log('🔐 Initializing secrets...');
|
||||
await initializeGoogleSecretManager();
|
||||
|
||||
// Step 1: Check and create database if needed
|
||||
const wasCreated = await checkAndCreateDatabase();
|
||||
|
||||
@ -282,6 +292,9 @@ async function autoSetup(): Promise<void> {
|
||||
console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.');
|
||||
console.log('📝 Note: Dealers table will be empty - import dealers using CSV import script.\n');
|
||||
|
||||
|
||||
console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.\n');
|
||||
|
||||
if (wasCreated) {
|
||||
console.log('💡 Next steps:');
|
||||
console.log(' 1. Server will start automatically');
|
||||
|
||||
19
src/scripts/check-db-schema.ts
Normal file
19
src/scripts/check-db-schema.ts
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
import { sequelize } from '../config/database';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Connection established');
|
||||
|
||||
const tableDescription = await sequelize.getQueryInterface().describeTable('workflow_templates');
|
||||
console.log('Current schema for workflow_templates:', JSON.stringify(tableDescription, null, 2));
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error:', error.message);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
31
src/scripts/force-fix-schema.ts
Normal file
31
src/scripts/force-fix-schema.ts
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
import { sequelize } from '../config/database';
|
||||
import { up } from '../migrations/20260123-fix-template-id-schema';
|
||||
|
||||
async function forceRun() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ Connected to DB');
|
||||
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
// 1. Remove from migrations table if exists (to keep track clean)
|
||||
await sequelize.query("DELETE FROM migrations WHERE name = '20260123-fix-template-id-schema'");
|
||||
console.log('DATA CLEANUP: Removed migration record to force re-run tracking.');
|
||||
|
||||
// 2. Run the migration up function directly
|
||||
console.log('🚀 Running migration manually...');
|
||||
await up(queryInterface);
|
||||
|
||||
// 3. Mark as executed
|
||||
await sequelize.query("INSERT INTO migrations (name) VALUES ('20260123-fix-template-id-schema')");
|
||||
console.log('✅ Migration applied and tracked successfully.');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error executing force migration:', error.message, error);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
forceRun();
|
||||
@ -1,5 +1,5 @@
|
||||
import { sequelize } from '../config/database';
|
||||
import { QueryInterface, QueryTypes } from 'sequelize';
|
||||
import { initializeGoogleSecretManager } from '../services/googleSecretManager.service';
|
||||
import * as m0 from '../migrations/2025103000-create-users';
|
||||
import * as m1 from '../migrations/2025103001-create-workflow-requests';
|
||||
import * as m2 from '../migrations/2025103002-create-approval-levels';
|
||||
@ -46,6 +46,7 @@ import * as m40 from '../migrations/20251218-fix-claim-invoice-credit-note-colum
|
||||
import * as m41 from '../migrations/20250120-create-dealers-table';
|
||||
import * as m42 from '../migrations/20250125-create-activity-types';
|
||||
import * as m43 from '../migrations/20260113-redesign-dealer-claim-history';
|
||||
import * as m44 from '../migrations/20260123-fix-template-id-schema';
|
||||
|
||||
interface Migration {
|
||||
name: string;
|
||||
@ -106,6 +107,7 @@ const migrations: Migration[] = [
|
||||
{ name: '20250120-create-dealers-table', module: m41 },
|
||||
{ name: '20250125-create-activity-types', module: m42 },
|
||||
{ name: '20260113-redesign-dealer-claim-history', module: m43 },
|
||||
{ name: '20260123-fix-template-id-schema', module: m44 },
|
||||
];
|
||||
|
||||
/**
|
||||
@ -134,12 +136,12 @@ async function ensureMigrationsTable(queryInterface: QueryInterface): Promise<vo
|
||||
/**
|
||||
* Get list of already executed migrations
|
||||
*/
|
||||
async function getExecutedMigrations(): Promise<string[]> {
|
||||
async function getExecutedMigrations(sequelize: any): Promise<string[]> {
|
||||
try {
|
||||
const results = await sequelize.query<{ name: string }>(
|
||||
const results = await sequelize.query(
|
||||
'SELECT name FROM migrations ORDER BY id',
|
||||
{ type: QueryTypes.SELECT }
|
||||
);
|
||||
) as { name: string }[];
|
||||
return results.map(r => r.name);
|
||||
} catch (error) {
|
||||
// Table might not exist yet
|
||||
@ -150,7 +152,7 @@ async function getExecutedMigrations(): Promise<string[]> {
|
||||
/**
|
||||
* Mark migration as executed
|
||||
*/
|
||||
async function markMigrationExecuted(name: string): Promise<void> {
|
||||
async function markMigrationExecuted(sequelize: any, name: string): Promise<void> {
|
||||
await sequelize.query(
|
||||
'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING',
|
||||
{
|
||||
@ -165,6 +167,12 @@ async function markMigrationExecuted(name: string): Promise<void> {
|
||||
*/
|
||||
async function run() {
|
||||
try {
|
||||
console.log('🔐 Initializing secrets...');
|
||||
await initializeGoogleSecretManager();
|
||||
|
||||
// Dynamically import sequelize after secrets are loaded
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
await sequelize.authenticate();
|
||||
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
@ -173,7 +181,7 @@ async function run() {
|
||||
await ensureMigrationsTable(queryInterface);
|
||||
|
||||
// Get already executed migrations
|
||||
const executedMigrations = await getExecutedMigrations();
|
||||
const executedMigrations = await getExecutedMigrations(sequelize);
|
||||
|
||||
// Find pending migrations
|
||||
const pendingMigrations = migrations.filter(
|
||||
@ -188,11 +196,12 @@ async function run() {
|
||||
|
||||
console.log(`🔄 Running ${pendingMigrations.length} migration(s)...`);
|
||||
|
||||
|
||||
// Run each pending migration
|
||||
for (const migration of pendingMigrations) {
|
||||
try {
|
||||
await migration.module.up(queryInterface);
|
||||
await markMigrationExecuted(migration.name);
|
||||
await markMigrationExecuted(sequelize, migration.name);
|
||||
console.log(`✅ ${migration.name}`);
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Migration failed: ${migration.name} - ${error.message}`);
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import http from 'http';
|
||||
import { initializeSecrets } from './app'; // Import initialization function
|
||||
import app from './app';
|
||||
import { initSocket } from './realtime/socket';
|
||||
import './queues/tatWorker'; // Initialize TAT worker
|
||||
import { logTatConfig } from './config/tat.config';
|
||||
import { logSystemConfig } from './config/system.config';
|
||||
import { initializeHolidaysCache } from './utils/tatTimeUtils';
|
||||
import { seedDefaultConfigurations } from './services/configSeed.service';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
// Load environment variables from .env file FIRST
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
import { initializeGoogleSecretManager } from './services/googleSecretManager.service';
|
||||
import { seedDefaultActivityTypes } from './services/activityTypeSeed.service';
|
||||
import { startPauseResumeJob } from './jobs/pauseResumeJob';
|
||||
import './queues/pauseResumeWorker'; // Initialize pause resume worker
|
||||
import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics';
|
||||
import { stopQueueMetrics } from './utils/queueMetrics';
|
||||
|
||||
// Dynamic imports will be used inside startServer to ensure secrets are loaded first
|
||||
import { emailService } from './services/email.service';
|
||||
|
||||
const PORT: number = parseInt(process.env.PORT || '5000', 10);
|
||||
@ -20,7 +18,30 @@ const startServer = async (): Promise<void> => {
|
||||
try {
|
||||
// Initialize Google Secret Manager before starting server
|
||||
// This will merge secrets from GCS into process.env if enabled
|
||||
await initializeSecrets();
|
||||
console.log('🔐 Initializing secrets...');
|
||||
await initializeGoogleSecretManager();
|
||||
|
||||
// Dynamically import everything else after secrets are loaded
|
||||
const app = require('./app').default;
|
||||
const { initSocket } = require('./realtime/socket');
|
||||
require('./queues/tatWorker'); // Initialize TAT worker
|
||||
const { logTatConfig } = require('./config/tat.config');
|
||||
const { logSystemConfig } = require('./config/system.config');
|
||||
const { initializeHolidaysCache } = require('./utils/tatTimeUtils');
|
||||
const { seedDefaultConfigurations } = require('./services/configSeed.service');
|
||||
const { startPauseResumeJob } = require('./jobs/pauseResumeJob');
|
||||
require('./queues/pauseResumeWorker'); // Initialize pause resume worker
|
||||
const { initializeQueueMetrics } = require('./utils/queueMetrics');
|
||||
const { emailService } = require('./services/email.service');
|
||||
|
||||
// Re-initialize email service after secrets are loaded (in case SMTP credentials were loaded)
|
||||
// This ensures the email service uses production SMTP if credentials are available
|
||||
try {
|
||||
await emailService.initialize();
|
||||
console.log('📧 Email service re-initialized after secrets loaded');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Email service re-initialization warning (will use test account if SMTP not configured):', error);
|
||||
}
|
||||
|
||||
// Re-initialize email service after secrets are loaded (in case SMTP credentials were loaded)
|
||||
// This ensures the email service uses production SMTP if credentials are available
|
||||
|
||||
@ -268,10 +268,13 @@ class AIService {
|
||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||
|
||||
// Trust AI's response - do not truncate anything
|
||||
// AI is instructed to stay within limit, but we accept whatever it generates
|
||||
// Trust AI's response - do not truncate anything
|
||||
// AI is instructed to stay within limit, but we accept whatever it generates
|
||||
if (remarkText.length > maxLength) {
|
||||
logger.info(`[AI Service] AI generated ${remarkText.length} characters (suggested limit: ${maxLength}). Full content preserved as-is.`);
|
||||
logger.info(`[AI Service] AI generated ${remarkText.length} characters (suggested limit: ${maxLength}). Full content preserved as-is.`);
|
||||
}
|
||||
|
||||
// Extract key points (look for bullet points or numbered items)
|
||||
|
||||
@ -119,7 +119,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getRequestCreatedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Created Successfully`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Created Successfully`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -189,7 +189,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getMultiApproverRequestEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Multi-Level Approval Request - Your Turn`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Multi-Level Approval Request - Your Turn`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -218,7 +218,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getApprovalRequestEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Approval Request - Action Required`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Approval Request - Action Required`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -272,7 +272,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getApprovalConfirmationEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Approved${isFinalApproval ? ' - All Approvals Complete' : ''}`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Approved${isFinalApproval ? ' - All Approvals Complete' : ''}`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -323,7 +323,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getRejectionNotificationEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Rejected`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Rejected`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -399,7 +399,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getTATReminderEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] TAT Reminder - ${tatInfo.thresholdPercentage}% Elapsed`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - TAT Reminder - ${tatInfo.thresholdPercentage}% Elapsed`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -469,7 +469,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getTATBreachedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] TAT BREACHED - Immediate Action Required`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - TAT BREACHED - Immediate Action Required`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -538,7 +538,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getWorkflowResumedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Workflow Resumed - Action Required`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed - Action Required`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: approverData.email,
|
||||
@ -607,7 +607,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getWorkflowResumedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Workflow Resumed`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -685,7 +685,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getRequestClosedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Request Closed`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Closed`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: recipientData.email,
|
||||
@ -754,7 +754,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getApproverSkippedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Approver Skipped`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Approver Skipped`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: skippedApproverData.email,
|
||||
@ -814,7 +814,7 @@ export class EmailNotificationService {
|
||||
};
|
||||
|
||||
const html = getWorkflowPausedEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Workflow Paused`;
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Paused`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: recipientData.email,
|
||||
|
||||
@ -1952,14 +1952,16 @@ export class WorkflowService {
|
||||
// Include PAUSED status so paused requests where user is the current approver are shown
|
||||
const pendingLevels = await ApprovalLevel.findAll({
|
||||
where: {
|
||||
status: { [Op.in]: [
|
||||
status: {
|
||||
[Op.in]: [
|
||||
ApprovalStatus.PENDING as any,
|
||||
(ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS',
|
||||
ApprovalStatus.PAUSED as any,
|
||||
'PENDING',
|
||||
'IN_PROGRESS',
|
||||
'PAUSED'
|
||||
] as any },
|
||||
] as any
|
||||
},
|
||||
},
|
||||
order: [['requestId', 'ASC'], ['levelNumber', 'ASC']],
|
||||
attributes: ['requestId', 'levelNumber', 'approverId'],
|
||||
@ -2028,7 +2030,8 @@ export class WorkflowService {
|
||||
baseConditions.push({
|
||||
[Op.or]: [
|
||||
{
|
||||
status: { [Op.in]: [
|
||||
status: {
|
||||
[Op.in]: [
|
||||
WorkflowStatus.PENDING as any,
|
||||
WorkflowStatus.APPROVED as any,
|
||||
WorkflowStatus.PAUSED as any,
|
||||
@ -2036,7 +2039,8 @@ export class WorkflowService {
|
||||
'IN_PROGRESS', // Legacy support - will be migrated to PENDING
|
||||
'APPROVED',
|
||||
'PAUSED'
|
||||
] as any }
|
||||
] as any
|
||||
}
|
||||
},
|
||||
// Also include requests with isPaused = true (even if status is PENDING)
|
||||
{
|
||||
@ -2063,10 +2067,12 @@ export class WorkflowService {
|
||||
baseConditions.push({
|
||||
[Op.and]: [
|
||||
{ status: statusUpper },
|
||||
{ [Op.or]: [
|
||||
{
|
||||
[Op.or]: [
|
||||
{ isPaused: { [Op.is]: null } },
|
||||
{ isPaused: false }
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
@ -2204,12 +2210,14 @@ export class WorkflowService {
|
||||
const levelRows = await ApprovalLevel.findAll({
|
||||
where: {
|
||||
approverId: userId,
|
||||
status: { [Op.in]: [
|
||||
status: {
|
||||
[Op.in]: [
|
||||
ApprovalStatus.APPROVED as any,
|
||||
(ApprovalStatus as any).REJECTED ?? 'REJECTED',
|
||||
'APPROVED',
|
||||
'REJECTED'
|
||||
] as any },
|
||||
] as any
|
||||
},
|
||||
},
|
||||
attributes: ['requestId'],
|
||||
});
|
||||
@ -2687,7 +2695,7 @@ export class WorkflowService {
|
||||
|
||||
// Reload with associations
|
||||
const workflow = await WorkflowRequest.findByPk(actualRequestId, {
|
||||
include: [ { association: 'initiator' } ]
|
||||
include: [{ association: 'initiator' }]
|
||||
});
|
||||
if (!workflow) return null;
|
||||
|
||||
@ -2783,7 +2791,7 @@ export class WorkflowService {
|
||||
// Use the actual UUID requestId for all queries
|
||||
const approvals = await ApprovalLevel.findAll({
|
||||
where: { requestId: actualRequestId },
|
||||
order: [['levelNumber','ASC']]
|
||||
order: [['levelNumber', 'ASC']]
|
||||
}) as any[];
|
||||
|
||||
const participants = await Participant.findAll({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user