Compare commits
7 Commits
e7e97f92e3
...
de469ab2c6
| Author | SHA1 | Date | |
|---|---|---|---|
| de469ab2c6 | |||
| b11e542a59 | |||
| f456fb8af9 | |||
| 088ac173a7 | |||
| be220bbb0c | |||
| d1ae0ffaec | |||
| 9285c97d4b |
@ -1,2 +1,2 @@
|
|||||||
import{a as s}from"./index-F9w_cZ47.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-sjs6YRoy.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-AvM4PHvP.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
import{a as s}from"./index-D-8iFw5e.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-BIX8LEl5.js.map
|
//# sourceMappingURL=conclusionApi-DFaefruY.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"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-DFaefruY.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
|
||||||
1
build/assets/index-B-mLDzJe.css
Normal file
1
build/assets/index-B-mLDzJe.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
75
build/assets/index-D-8iFw5e.js
Normal file
75
build/assets/index-D-8iFw5e.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-D-8iFw5e.js.map
Normal file
1
build/assets/index-D-8iFw5e.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13
build/assets/router-vendor-B1UBYWWO.js
Normal file
13
build/assets/router-vendor-B1UBYWWO.js
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
1
build/assets/ui-vendor-DbB0YGPu.js.map
Normal file
1
build/assets/ui-vendor-DbB0YGPu.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -52,15 +52,15 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-F9w_cZ47.js"></script>
|
<script type="module" crossorigin src="/assets/index-D-8iFw5e.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Bme4E5cb.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Bme4E5cb.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DIkYAdWy.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DIkYAdWy.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-sjs6YRoy.js">
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-DbB0YGPu.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-AvM4PHvP.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B1UBYWWO.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CPRbj7YF.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B-mLDzJe.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
"description": "Royal Enfield Workflow Management System - Backend API (TypeScript)",
|
"description": "Royal Enfield Workflow Management System - Backend API (TypeScript)",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm install && npm run setup && npm run build && npm run start:prod",
|
"start": "npm run build && npm run start:prod && npm run setup",
|
||||||
"dev": "npm run setup && npm run migrate && nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
|
"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",
|
"dev:no-setup": "nodemon --exec ts-node -r tsconfig-paths/register src/server.ts",
|
||||||
"build": "tsc && tsc-alias",
|
"build": "tsc && tsc-alias",
|
||||||
"build:watch": "tsc --watch",
|
"build:watch": "tsc --watch",
|
||||||
@ -92,4 +92,4 @@
|
|||||||
"node": ">=22.0.0",
|
"node": ">=22.0.0",
|
||||||
"npm": ">=10.0.0"
|
"npm": ">=10.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
src/app.ts
28
src/app.ts
@ -16,17 +16,7 @@ import path from 'path';
|
|||||||
// Load environment variables from .env file first
|
// Load environment variables from .env file first
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// Initialize Google Secret Manager (async, but we'll wait for it in server.ts)
|
// Secrets are now initialized in server.ts before app is imported
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const app: express.Application = express();
|
const app: express.Application = express();
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
@ -123,8 +113,8 @@ app.use(createMetricsRouter());
|
|||||||
|
|
||||||
// Health check endpoint (before API routes)
|
// Health check endpoint (before API routes)
|
||||||
app.get('/health', (_req: express.Request, res: express.Response) => {
|
app.get('/health', (_req: express.Request, res: express.Response) => {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
environment: process.env.NODE_ENV || 'development'
|
environment: process.env.NODE_ENV || 'development'
|
||||||
@ -142,7 +132,7 @@ app.use('/uploads', express.static(UPLOAD_DIR));
|
|||||||
app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise<void> => {
|
app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const ssoData: SSOUserData = req.body;
|
const ssoData: SSOUserData = req.body;
|
||||||
|
|
||||||
// Validate required fields - email and oktaSub are required
|
// Validate required fields - email and oktaSub are required
|
||||||
if (!ssoData.email || !ssoData.oktaSub) {
|
if (!ssoData.email || !ssoData.oktaSub) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
@ -155,7 +145,7 @@ app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.
|
|||||||
|
|
||||||
// Create or update user
|
// Create or update user
|
||||||
const user = await userService.createOrUpdateUser(ssoData);
|
const user = await userService.createOrUpdateUser(ssoData);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'User processed successfully',
|
message: 'User processed successfully',
|
||||||
@ -193,7 +183,7 @@ app.post('/api/v1/auth/sso-callback', async (req: express.Request, res: express.
|
|||||||
app.get('/api/v1/users', async (_req: express.Request, res: express.Response): Promise<void> => {
|
app.get('/api/v1/users', async (_req: express.Request, res: express.Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const users = await userService.getAllUsers();
|
const users = await userService.getAllUsers();
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Users retrieved successfully',
|
message: 'Users retrieved successfully',
|
||||||
@ -254,7 +244,7 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Catch-all handler: serve React app for all non-API routes
|
// Catch-all handler: serve React app for all non-API routes
|
||||||
// This must be AFTER all API routes to avoid intercepting API requests
|
// This must be AFTER all API routes to avoid intercepting API requests
|
||||||
app.get('*', (req: express.Request, res: express.Response): void => {
|
app.get('*', (req: express.Request, res: express.Response): void => {
|
||||||
@ -267,7 +257,7 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve React app for all other routes (SPA routing)
|
// Serve React app for all other routes (SPA routing)
|
||||||
// This handles client-side routing in React Router
|
// This handles client-side routing in React Router
|
||||||
// CSP headers from Helmet will be applied to this response
|
// CSP headers from Helmet will be applied to this response
|
||||||
@ -284,7 +274,7 @@ if (reactBuildPath && fs.existsSync(path.join(reactBuildPath, "index.html"))) {
|
|||||||
note: 'React build not found. API is available at /api/v1'
|
note: 'React build not found. API is available at /api/v1'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Standard 404 handler for non-existent routes
|
// Standard 404 handler for non-existent routes
|
||||||
app.use((req: express.Request, res: express.Response): void => {
|
app.use((req: express.Request, res: express.Response): void => {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export class TemplateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
// New fields
|
||||||
templateName,
|
templateName,
|
||||||
templateCode,
|
templateCode,
|
||||||
templateDescription,
|
templateDescription,
|
||||||
@ -30,20 +31,34 @@ export class TemplateController {
|
|||||||
userFieldMappings,
|
userFieldMappings,
|
||||||
dynamicApproverConfig,
|
dynamicApproverConfig,
|
||||||
isActive,
|
isActive,
|
||||||
|
|
||||||
|
// Legacy fields (from frontend)
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
approvers,
|
||||||
|
suggestedSLA
|
||||||
} = req.body;
|
} = 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);
|
return ResponseHandler.error(res, 'Template name is required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = await this.templateService.createTemplate(userId, {
|
const template = await this.templateService.createTemplate(userId, {
|
||||||
templateName,
|
templateName: finalTemplateName,
|
||||||
templateCode,
|
templateCode,
|
||||||
templateDescription,
|
templateDescription: finalTemplateDescription,
|
||||||
templateCategory,
|
templateCategory: finalTemplateCategory,
|
||||||
workflowType,
|
workflowType,
|
||||||
approvalLevelsConfig,
|
approvalLevelsConfig: finalApprovalLevelsConfig,
|
||||||
defaultTatHours: defaultTatHours ? parseFloat(defaultTatHours) : undefined,
|
defaultTatHours: finalDefaultTatHours ? parseFloat(finalDefaultTatHours) : undefined,
|
||||||
formStepsConfig,
|
formStepsConfig,
|
||||||
userFieldMappings,
|
userFieldMappings,
|
||||||
dynamicApproverConfig,
|
dynamicApproverConfig,
|
||||||
@ -149,14 +164,21 @@ export class TemplateController {
|
|||||||
userFieldMappings,
|
userFieldMappings,
|
||||||
dynamicApproverConfig,
|
dynamicApproverConfig,
|
||||||
isActive,
|
isActive,
|
||||||
|
|
||||||
|
// Legacy
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
approvers,
|
||||||
|
suggestedSLA
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const template = await this.templateService.updateTemplate(templateId, userId, {
|
const template = await this.templateService.updateTemplate(templateId, userId, {
|
||||||
templateName,
|
templateName: templateName || name,
|
||||||
templateDescription,
|
templateDescription: templateDescription || description,
|
||||||
templateCategory,
|
templateCategory: templateCategory || category,
|
||||||
approvalLevelsConfig,
|
approvalLevelsConfig: approvalLevelsConfig || approvers,
|
||||||
defaultTatHours: defaultTatHours ? parseFloat(defaultTatHours) : undefined,
|
defaultTatHours: (defaultTatHours || suggestedSLA) ? parseFloat(defaultTatHours || suggestedSLA) : undefined,
|
||||||
formStepsConfig,
|
formStepsConfig,
|
||||||
userFieldMappings,
|
userFieldMappings,
|
||||||
dynamicApproverConfig,
|
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,180 +1,177 @@
|
|||||||
import { DataTypes, Model, Optional } from 'sequelize';
|
import { DataTypes, Model, Optional } from 'sequelize';
|
||||||
import { sequelize } from '@config/database';
|
import { sequelize } from '../config/database';
|
||||||
import { User } from './User';
|
import { User } from './User';
|
||||||
|
|
||||||
interface WorkflowTemplateAttributes {
|
interface WorkflowTemplateAttributes {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
templateName: string;
|
templateName: string;
|
||||||
templateCode?: string;
|
templateCode?: string;
|
||||||
templateDescription?: string;
|
templateDescription?: string;
|
||||||
templateCategory?: string;
|
templateCategory?: string;
|
||||||
workflowType?: string;
|
workflowType?: string;
|
||||||
approvalLevelsConfig?: any;
|
approvalLevelsConfig?: any;
|
||||||
defaultTatHours?: number;
|
defaultTatHours?: number;
|
||||||
formStepsConfig?: any;
|
formStepsConfig?: any;
|
||||||
userFieldMappings?: any;
|
userFieldMappings?: any;
|
||||||
dynamicApproverConfig?: any;
|
dynamicApproverConfig?: any;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isSystemTemplate: boolean;
|
isSystemTemplate: boolean;
|
||||||
usageCount: number;
|
usageCount: number;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
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 templateId!: string;
|
||||||
public templateName!: string;
|
public templateName!: string;
|
||||||
public templateCode?: string;
|
public templateCode?: string;
|
||||||
public templateDescription?: string;
|
public templateDescription?: string;
|
||||||
public templateCategory?: string;
|
public templateCategory?: string;
|
||||||
public workflowType?: string;
|
public workflowType?: string;
|
||||||
public approvalLevelsConfig?: any;
|
public approvalLevelsConfig?: any;
|
||||||
public defaultTatHours?: number;
|
public defaultTatHours?: number;
|
||||||
public formStepsConfig?: any;
|
public formStepsConfig?: any;
|
||||||
public userFieldMappings?: any;
|
public userFieldMappings?: any;
|
||||||
public dynamicApproverConfig?: any;
|
public dynamicApproverConfig?: any;
|
||||||
public isActive!: boolean;
|
public isActive!: boolean;
|
||||||
public isSystemTemplate!: boolean;
|
public isSystemTemplate!: boolean;
|
||||||
public usageCount!: number;
|
public usageCount!: number;
|
||||||
public createdBy?: string;
|
public createdBy?: string;
|
||||||
public createdAt!: Date;
|
public createdAt!: Date;
|
||||||
public updatedAt!: Date;
|
public updatedAt!: Date;
|
||||||
|
|
||||||
// Associations
|
// Associations
|
||||||
public creator?: User;
|
public creator?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
WorkflowTemplate.init(
|
WorkflowTemplate.init(
|
||||||
{
|
{
|
||||||
templateId: {
|
templateId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
field: 'template_id'
|
field: 'template_id'
|
||||||
|
},
|
||||||
|
templateName: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: false,
|
||||||
|
field: 'template_name'
|
||||||
|
},
|
||||||
|
templateCode: {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: true,
|
||||||
|
unique: true,
|
||||||
|
field: 'template_code'
|
||||||
|
},
|
||||||
|
templateDescription: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'template_description'
|
||||||
|
},
|
||||||
|
templateCategory: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'template_category'
|
||||||
|
},
|
||||||
|
workflowType: {
|
||||||
|
type: DataTypes.STRING(50),
|
||||||
|
allowNull: true,
|
||||||
|
field: 'workflow_type'
|
||||||
|
},
|
||||||
|
approvalLevelsConfig: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'approval_levels_config'
|
||||||
|
},
|
||||||
|
defaultTatHours: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 24,
|
||||||
|
field: 'default_tat_hours'
|
||||||
|
},
|
||||||
|
formStepsConfig: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'form_steps_config'
|
||||||
|
},
|
||||||
|
userFieldMappings: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'user_field_mappings'
|
||||||
|
},
|
||||||
|
dynamicApproverConfig: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'dynamic_approver_config'
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: true,
|
||||||
|
field: 'is_active'
|
||||||
|
},
|
||||||
|
isSystemTemplate: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
field: 'is_system_template'
|
||||||
|
},
|
||||||
|
usageCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
field: 'usage_count'
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
field: 'created_by',
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'user_id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'created_at'
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
field: 'updated_at'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
templateName: {
|
{
|
||||||
type: DataTypes.STRING(200),
|
sequelize,
|
||||||
allowNull: false,
|
modelName: 'WorkflowTemplate',
|
||||||
field: 'template_name'
|
tableName: 'workflow_templates',
|
||||||
},
|
timestamps: true,
|
||||||
templateCode: {
|
createdAt: 'created_at',
|
||||||
type: DataTypes.STRING(50),
|
updatedAt: 'updated_at',
|
||||||
allowNull: true,
|
indexes: [
|
||||||
unique: true,
|
{
|
||||||
field: 'template_code'
|
unique: true,
|
||||||
},
|
fields: ['template_code']
|
||||||
templateDescription: {
|
},
|
||||||
type: DataTypes.TEXT,
|
{
|
||||||
allowNull: true,
|
fields: ['workflow_type']
|
||||||
field: 'template_description'
|
},
|
||||||
},
|
{
|
||||||
templateCategory: {
|
fields: ['is_active']
|
||||||
type: DataTypes.STRING(100),
|
}
|
||||||
allowNull: true,
|
]
|
||||||
field: 'template_category'
|
|
||||||
},
|
|
||||||
workflowType: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: true,
|
|
||||||
field: 'workflow_type'
|
|
||||||
},
|
|
||||||
approvalLevelsConfig: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'approval_levels_config'
|
|
||||||
},
|
|
||||||
defaultTatHours: {
|
|
||||||
type: DataTypes.DECIMAL(10, 2),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 24,
|
|
||||||
field: 'default_tat_hours'
|
|
||||||
},
|
|
||||||
formStepsConfig: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'form_steps_config'
|
|
||||||
},
|
|
||||||
userFieldMappings: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'user_field_mappings'
|
|
||||||
},
|
|
||||||
dynamicApproverConfig: {
|
|
||||||
type: DataTypes.JSONB,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'dynamic_approver_config'
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
field: 'is_active'
|
|
||||||
},
|
|
||||||
isSystemTemplate: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
field: 'is_system_template'
|
|
||||||
},
|
|
||||||
usageCount: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 0,
|
|
||||||
field: 'usage_count'
|
|
||||||
},
|
|
||||||
createdBy: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'created_by',
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'user_id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'created_at'
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
field: 'updated_at'
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
modelName: 'WorkflowTemplate',
|
|
||||||
tableName: 'workflow_templates',
|
|
||||||
timestamps: true,
|
|
||||||
createdAt: 'created_at',
|
|
||||||
updatedAt: 'updated_at',
|
|
||||||
indexes: [
|
|
||||||
{
|
|
||||||
unique: true,
|
|
||||||
fields: ['template_code']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['workflow_type']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fields: ['is_active']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Associations
|
// Associations
|
||||||
WorkflowTemplate.belongsTo(User, {
|
WorkflowTemplate.belongsTo(User, {
|
||||||
as: 'creator',
|
as: 'creator',
|
||||||
foreignKey: 'createdBy',
|
foreignKey: 'createdBy',
|
||||||
targetKey: 'userId'
|
targetKey: 'userId'
|
||||||
});
|
});
|
||||||
|
|
||||||
export { WorkflowTemplate };
|
|
||||||
|
|
||||||
|
|||||||
@ -20,12 +20,12 @@ import { DealerClaimDetails } from './DealerClaimDetails';
|
|||||||
import { DealerProposalDetails } from './DealerProposalDetails';
|
import { DealerProposalDetails } from './DealerProposalDetails';
|
||||||
import { DealerCompletionDetails } from './DealerCompletionDetails';
|
import { DealerCompletionDetails } from './DealerCompletionDetails';
|
||||||
import { DealerProposalCostItem } from './DealerProposalCostItem';
|
import { DealerProposalCostItem } from './DealerProposalCostItem';
|
||||||
import { WorkflowTemplate } from './WorkflowTemplate';
|
|
||||||
import { InternalOrder } from './InternalOrder';
|
import { InternalOrder } from './InternalOrder';
|
||||||
import { ClaimBudgetTracking } from './ClaimBudgetTracking';
|
import { ClaimBudgetTracking } from './ClaimBudgetTracking';
|
||||||
import { Dealer } from './Dealer';
|
import { Dealer } from './Dealer';
|
||||||
import { ActivityType } from './ActivityType';
|
import { ActivityType } from './ActivityType';
|
||||||
import { DealerClaimHistory } from './DealerClaimHistory';
|
import { DealerClaimHistory } from './DealerClaimHistory';
|
||||||
|
import { WorkflowTemplate } from './WorkflowTemplate';
|
||||||
|
|
||||||
// Define associations
|
// Define associations
|
||||||
const defineAssociations = () => {
|
const defineAssociations = () => {
|
||||||
@ -170,11 +170,11 @@ export {
|
|||||||
ConclusionRemark,
|
ConclusionRemark,
|
||||||
RequestSummary,
|
RequestSummary,
|
||||||
SharedSummary,
|
SharedSummary,
|
||||||
|
WorkflowTemplate,
|
||||||
DealerClaimDetails,
|
DealerClaimDetails,
|
||||||
DealerProposalDetails,
|
DealerProposalDetails,
|
||||||
DealerCompletionDetails,
|
DealerCompletionDetails,
|
||||||
DealerProposalCostItem,
|
DealerProposalCostItem,
|
||||||
WorkflowTemplate,
|
|
||||||
InternalOrder,
|
InternalOrder,
|
||||||
ClaimBudgetTracking,
|
ClaimBudgetTracking,
|
||||||
Dealer,
|
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 { Client } from 'pg';
|
||||||
import { sequelize } from '../config/database';
|
|
||||||
import { QueryTypes } from 'sequelize';
|
import { QueryTypes } from 'sequelize';
|
||||||
|
import { initializeGoogleSecretManager } from '../services/googleSecretManager.service';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
@ -21,14 +21,15 @@ import path from 'path';
|
|||||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
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
|
||||||
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';
|
|
||||||
|
|
||||||
async function checkAndCreateDatabase(): Promise<boolean> {
|
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({
|
const client = new Client({
|
||||||
host: DB_HOST,
|
host: DB_HOST,
|
||||||
port: DB_PORT,
|
port: DB_PORT,
|
||||||
@ -49,13 +50,13 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
console.log(`📦 Database '${DB_NAME}' not found. Creating...`);
|
console.log(`📦 Database '${DB_NAME}' not found. Creating...`);
|
||||||
|
|
||||||
// Create database
|
// Create database
|
||||||
await client.query(`CREATE DATABASE "${DB_NAME}"`);
|
await client.query(`CREATE DATABASE "${DB_NAME}"`);
|
||||||
console.log(`✅ Database '${DB_NAME}' created successfully!`);
|
console.log(`✅ Database '${DB_NAME}' created successfully!`);
|
||||||
|
|
||||||
await client.end();
|
await client.end();
|
||||||
|
|
||||||
// Connect to new database and install extensions
|
// Connect to new database and install extensions
|
||||||
const newDbClient = new Client({
|
const newDbClient = new Client({
|
||||||
host: DB_HOST,
|
host: DB_HOST,
|
||||||
@ -64,13 +65,13 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
password: DB_PASSWORD,
|
password: DB_PASSWORD,
|
||||||
database: DB_NAME,
|
database: DB_NAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
await newDbClient.connect();
|
await newDbClient.connect();
|
||||||
console.log('📦 Installing uuid-ossp extension...');
|
console.log('📦 Installing uuid-ossp extension...');
|
||||||
await newDbClient.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
await newDbClient.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
||||||
console.log('✅ Extension installed!');
|
console.log('✅ Extension installed!');
|
||||||
await newDbClient.end();
|
await newDbClient.end();
|
||||||
|
|
||||||
return true; // Database was created
|
return true; // Database was created
|
||||||
} else {
|
} else {
|
||||||
console.log(`✅ Database '${DB_NAME}' already exists.`);
|
console.log(`✅ Database '${DB_NAME}' already exists.`);
|
||||||
@ -87,7 +88,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
async function runMigrations(): Promise<void> {
|
async function runMigrations(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Checking and running pending migrations...');
|
console.log('🔄 Checking and running pending migrations...');
|
||||||
|
|
||||||
// Import all migrations using require for CommonJS compatibility
|
// Import all migrations using require for CommonJS compatibility
|
||||||
// Some migrations use module.exports, others use export
|
// Some migrations use module.exports, others use export
|
||||||
const m0 = require('../migrations/2025103000-create-users');
|
const m0 = require('../migrations/2025103000-create-users');
|
||||||
@ -136,7 +137,8 @@ async function runMigrations(): Promise<void> {
|
|||||||
const m41 = require('../migrations/20250120-create-dealers-table');
|
const m41 = require('../migrations/20250120-create-dealers-table');
|
||||||
const m42 = require('../migrations/20250125-create-activity-types');
|
const m42 = require('../migrations/20250125-create-activity-types');
|
||||||
const m43 = require('../migrations/20260113-redesign-dealer-claim-history');
|
const m43 = require('../migrations/20260113-redesign-dealer-claim-history');
|
||||||
|
const m44 = require('../migrations/20260123-fix-template-id-schema');
|
||||||
|
|
||||||
const migrations = [
|
const migrations = [
|
||||||
{ name: '2025103000-create-users', module: m0 },
|
{ name: '2025103000-create-users', module: m0 },
|
||||||
{ name: '2025103001-create-workflow-requests', module: m1 },
|
{ name: '2025103001-create-workflow-requests', module: m1 },
|
||||||
@ -184,10 +186,13 @@ async function runMigrations(): Promise<void> {
|
|||||||
{ name: '20250120-create-dealers-table', module: m41 },
|
{ name: '20250120-create-dealers-table', module: m41 },
|
||||||
{ name: '20250125-create-activity-types', module: m42 },
|
{ name: '20250125-create-activity-types', module: m42 },
|
||||||
{ name: '20260113-redesign-dealer-claim-history', module: m43 },
|
{ 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();
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
|
|
||||||
// Ensure migrations tracking table exists
|
// Ensure migrations tracking table exists
|
||||||
const tables = await queryInterface.showAllTables();
|
const tables = await queryInterface.showAllTables();
|
||||||
if (!tables.includes('migrations')) {
|
if (!tables.includes('migrations')) {
|
||||||
@ -199,34 +204,34 @@ async function runMigrations(): Promise<void> {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get already executed migrations
|
// Get already executed migrations
|
||||||
const executedResults = await sequelize.query<{ name: string }>(
|
const executedResults = await sequelize.query(
|
||||||
'SELECT name FROM migrations ORDER BY id',
|
'SELECT name FROM migrations ORDER BY id',
|
||||||
{ type: QueryTypes.SELECT }
|
{ type: QueryTypes.SELECT }
|
||||||
);
|
) as { name: string }[];
|
||||||
const executedMigrations = executedResults.map(r => r.name);
|
const executedMigrations = executedResults.map(r => r.name);
|
||||||
|
|
||||||
// Find pending migrations
|
// Find pending migrations
|
||||||
const pendingMigrations = migrations.filter(
|
const pendingMigrations = migrations.filter(
|
||||||
m => !executedMigrations.includes(m.name)
|
m => !executedMigrations.includes(m.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pendingMigrations.length === 0) {
|
if (pendingMigrations.length === 0) {
|
||||||
console.log('✅ Migrations up-to-date');
|
console.log('✅ Migrations up-to-date');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔄 Running ${pendingMigrations.length} pending migration(s)...`);
|
console.log(`🔄 Running ${pendingMigrations.length} pending migration(s)...`);
|
||||||
|
|
||||||
// Run each pending migration
|
// Run each pending migration
|
||||||
for (const migration of pendingMigrations) {
|
for (const migration of pendingMigrations) {
|
||||||
try {
|
try {
|
||||||
console.log(` → ${migration.name}`);
|
console.log(` → ${migration.name}`);
|
||||||
|
|
||||||
// Call the up function - works for both module.exports and export styles
|
// Call the up function - works for both module.exports and export styles
|
||||||
await migration.module.up(queryInterface);
|
await migration.module.up(queryInterface);
|
||||||
|
|
||||||
// Mark as executed
|
// Mark as executed
|
||||||
await sequelize.query(
|
await sequelize.query(
|
||||||
'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING',
|
'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING',
|
||||||
@ -241,7 +246,7 @@ async function runMigrations(): Promise<void> {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ Applied ${pendingMigrations.length} migration(s)`);
|
console.log(`✅ Applied ${pendingMigrations.length} migration(s)`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Migration failed:', error.message);
|
console.error('❌ Migration failed:', error.message);
|
||||||
@ -252,6 +257,7 @@ async function runMigrations(): Promise<void> {
|
|||||||
async function testConnection(): Promise<void> {
|
async function testConnection(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('🔌 Testing database connection...');
|
console.log('🔌 Testing database connection...');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
console.log('✅ Database connection established!');
|
console.log('✅ Database connection established!');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -266,6 +272,10 @@ async function autoSetup(): Promise<void> {
|
|||||||
console.log('========================================\n');
|
console.log('========================================\n');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Step 0: Initialize secrets
|
||||||
|
console.log('🔐 Initializing secrets...');
|
||||||
|
await initializeGoogleSecretManager();
|
||||||
|
|
||||||
// Step 1: Check and create database if needed
|
// Step 1: Check and create database if needed
|
||||||
const wasCreated = await checkAndCreateDatabase();
|
const wasCreated = await checkAndCreateDatabase();
|
||||||
|
|
||||||
@ -278,10 +288,13 @@ async function autoSetup(): Promise<void> {
|
|||||||
console.log('\n========================================');
|
console.log('\n========================================');
|
||||||
console.log('✅ Setup completed successfully!');
|
console.log('✅ Setup completed successfully!');
|
||||||
console.log('========================================\n');
|
console.log('========================================\n');
|
||||||
|
|
||||||
console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.');
|
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: 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) {
|
if (wasCreated) {
|
||||||
console.log('💡 Next steps:');
|
console.log('💡 Next steps:');
|
||||||
console.log(' 1. Server will start automatically');
|
console.log(' 1. Server will start automatically');
|
||||||
@ -289,7 +302,7 @@ async function autoSetup(): Promise<void> {
|
|||||||
console.log(' 3. Run this SQL to make yourself admin:');
|
console.log(' 3. Run this SQL to make yourself admin:');
|
||||||
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`);
|
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('\n========================================');
|
console.error('\n========================================');
|
||||||
console.error('❌ Setup failed!');
|
console.error('❌ Setup failed!');
|
||||||
|
|||||||
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 { QueryInterface, QueryTypes } from 'sequelize';
|
||||||
|
import { initializeGoogleSecretManager } from '../services/googleSecretManager.service';
|
||||||
import * as m0 from '../migrations/2025103000-create-users';
|
import * as m0 from '../migrations/2025103000-create-users';
|
||||||
import * as m1 from '../migrations/2025103001-create-workflow-requests';
|
import * as m1 from '../migrations/2025103001-create-workflow-requests';
|
||||||
import * as m2 from '../migrations/2025103002-create-approval-levels';
|
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 m41 from '../migrations/20250120-create-dealers-table';
|
||||||
import * as m42 from '../migrations/20250125-create-activity-types';
|
import * as m42 from '../migrations/20250125-create-activity-types';
|
||||||
import * as m43 from '../migrations/20260113-redesign-dealer-claim-history';
|
import * as m43 from '../migrations/20260113-redesign-dealer-claim-history';
|
||||||
|
import * as m44 from '../migrations/20260123-fix-template-id-schema';
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
@ -106,6 +107,7 @@ const migrations: Migration[] = [
|
|||||||
{ name: '20250120-create-dealers-table', module: m41 },
|
{ name: '20250120-create-dealers-table', module: m41 },
|
||||||
{ name: '20250125-create-activity-types', module: m42 },
|
{ name: '20250125-create-activity-types', module: m42 },
|
||||||
{ name: '20260113-redesign-dealer-claim-history', module: m43 },
|
{ 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
|
* Get list of already executed migrations
|
||||||
*/
|
*/
|
||||||
async function getExecutedMigrations(): Promise<string[]> {
|
async function getExecutedMigrations(sequelize: any): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const results = await sequelize.query<{ name: string }>(
|
const results = await sequelize.query(
|
||||||
'SELECT name FROM migrations ORDER BY id',
|
'SELECT name FROM migrations ORDER BY id',
|
||||||
{ type: QueryTypes.SELECT }
|
{ type: QueryTypes.SELECT }
|
||||||
);
|
) as { name: string }[];
|
||||||
return results.map(r => r.name);
|
return results.map(r => r.name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Table might not exist yet
|
// Table might not exist yet
|
||||||
@ -150,7 +152,7 @@ async function getExecutedMigrations(): Promise<string[]> {
|
|||||||
/**
|
/**
|
||||||
* Mark migration as executed
|
* Mark migration as executed
|
||||||
*/
|
*/
|
||||||
async function markMigrationExecuted(name: string): Promise<void> {
|
async function markMigrationExecuted(sequelize: any, name: string): Promise<void> {
|
||||||
await sequelize.query(
|
await sequelize.query(
|
||||||
'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING',
|
'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() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
|
console.log('🔐 Initializing secrets...');
|
||||||
|
await initializeGoogleSecretManager();
|
||||||
|
|
||||||
|
// Dynamically import sequelize after secrets are loaded
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
|
|
||||||
const queryInterface = sequelize.getQueryInterface();
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
@ -173,7 +181,7 @@ async function run() {
|
|||||||
await ensureMigrationsTable(queryInterface);
|
await ensureMigrationsTable(queryInterface);
|
||||||
|
|
||||||
// Get already executed migrations
|
// Get already executed migrations
|
||||||
const executedMigrations = await getExecutedMigrations();
|
const executedMigrations = await getExecutedMigrations(sequelize);
|
||||||
|
|
||||||
// Find pending migrations
|
// Find pending migrations
|
||||||
const pendingMigrations = migrations.filter(
|
const pendingMigrations = migrations.filter(
|
||||||
@ -188,11 +196,12 @@ async function run() {
|
|||||||
|
|
||||||
console.log(`🔄 Running ${pendingMigrations.length} migration(s)...`);
|
console.log(`🔄 Running ${pendingMigrations.length} migration(s)...`);
|
||||||
|
|
||||||
|
|
||||||
// Run each pending migration
|
// Run each pending migration
|
||||||
for (const migration of pendingMigrations) {
|
for (const migration of pendingMigrations) {
|
||||||
try {
|
try {
|
||||||
await migration.module.up(queryInterface);
|
await migration.module.up(queryInterface);
|
||||||
await markMigrationExecuted(migration.name);
|
await markMigrationExecuted(sequelize, migration.name);
|
||||||
console.log(`✅ ${migration.name}`);
|
console.log(`✅ ${migration.name}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(`❌ Migration failed: ${migration.name} - ${error.message}`);
|
console.error(`❌ Migration failed: ${migration.name} - ${error.message}`);
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { initializeSecrets } from './app'; // Import initialization function
|
import dotenv from 'dotenv';
|
||||||
import app from './app';
|
import path from 'path';
|
||||||
import { initSocket } from './realtime/socket';
|
|
||||||
import './queues/tatWorker'; // Initialize TAT worker
|
// Load environment variables from .env file FIRST
|
||||||
import { logTatConfig } from './config/tat.config';
|
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||||
import { logSystemConfig } from './config/system.config';
|
import { initializeGoogleSecretManager } from './services/googleSecretManager.service';
|
||||||
import { initializeHolidaysCache } from './utils/tatTimeUtils';
|
|
||||||
import { seedDefaultConfigurations } from './services/configSeed.service';
|
|
||||||
import { seedDefaultActivityTypes } from './services/activityTypeSeed.service';
|
import { seedDefaultActivityTypes } from './services/activityTypeSeed.service';
|
||||||
import { startPauseResumeJob } from './jobs/pauseResumeJob';
|
import { stopQueueMetrics } from './utils/queueMetrics';
|
||||||
import './queues/pauseResumeWorker'; // Initialize pause resume worker
|
|
||||||
import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics';
|
// Dynamic imports will be used inside startServer to ensure secrets are loaded first
|
||||||
import { emailService } from './services/email.service';
|
import { emailService } from './services/email.service';
|
||||||
|
|
||||||
const PORT: number = parseInt(process.env.PORT || '5000', 10);
|
const PORT: number = parseInt(process.env.PORT || '5000', 10);
|
||||||
@ -20,8 +18,22 @@ const startServer = async (): Promise<void> => {
|
|||||||
try {
|
try {
|
||||||
// Initialize Google Secret Manager before starting server
|
// Initialize Google Secret Manager before starting server
|
||||||
// This will merge secrets from GCS into process.env if enabled
|
// 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)
|
// 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
|
// This ensures the email service uses production SMTP if credentials are available
|
||||||
try {
|
try {
|
||||||
@ -30,37 +42,46 @@ const startServer = async (): Promise<void> => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('⚠️ Email service re-initialization warning (will use test account if SMTP not configured):', 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
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
initSocket(server);
|
initSocket(server);
|
||||||
|
|
||||||
// Seed default configurations if table is empty
|
// Seed default configurations if table is empty
|
||||||
try {
|
try {
|
||||||
await seedDefaultConfigurations();
|
await seedDefaultConfigurations();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('⚠️ Configuration seeding error:', error);
|
console.error('⚠️ Configuration seeding error:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed default activity types if table is empty
|
// Seed default activity types if table is empty
|
||||||
try {
|
try {
|
||||||
await seedDefaultActivityTypes();
|
await seedDefaultActivityTypes();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('⚠️ Activity type seeding error:', error);
|
console.error('⚠️ Activity type seeding error:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize holidays cache for TAT calculations
|
// Initialize holidays cache for TAT calculations
|
||||||
try {
|
try {
|
||||||
await initializeHolidaysCache();
|
await initializeHolidaysCache();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Silently fall back to weekends-only TAT calculation
|
// Silently fall back to weekends-only TAT calculation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start scheduled jobs
|
// Start scheduled jobs
|
||||||
startPauseResumeJob();
|
startPauseResumeJob();
|
||||||
|
|
||||||
// Initialize queue metrics collection for Prometheus
|
// Initialize queue metrics collection for Prometheus
|
||||||
initializeQueueMetrics();
|
initializeQueueMetrics();
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`🚀 Server running on port ${PORT} | ${process.env.NODE_ENV || 'development'}`);
|
console.log(`🚀 Server running on port ${PORT} | ${process.env.NODE_ENV || 'development'}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -28,7 +28,7 @@ class AIService {
|
|||||||
// Check if AI is enabled from config
|
// Check if AI is enabled from config
|
||||||
const { getConfigBoolean } = require('./configReader.service');
|
const { getConfigBoolean } = require('./configReader.service');
|
||||||
const enabled = await getConfigBoolean('AI_ENABLED', true);
|
const enabled = await getConfigBoolean('AI_ENABLED', true);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
logger.warn('[AI Service] AI features disabled in admin configuration');
|
logger.warn('[AI Service] AI features disabled in admin configuration');
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
@ -54,7 +54,7 @@ class AIService {
|
|||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('[AI Service] Failed to initialize Vertex AI:', error);
|
logger.error('[AI Service] Failed to initialize Vertex AI:', error);
|
||||||
|
|
||||||
if (error.code === 'MODULE_NOT_FOUND') {
|
if (error.code === 'MODULE_NOT_FOUND') {
|
||||||
logger.warn('[AI Service] @google-cloud/vertexai package not installed. Run: npm install @google-cloud/vertexai');
|
logger.warn('[AI Service] @google-cloud/vertexai package not installed. Run: npm install @google-cloud/vertexai');
|
||||||
} else if (error.message?.includes('ENOENT') || error.message?.includes('not found')) {
|
} else if (error.message?.includes('ENOENT') || error.message?.includes('not found')) {
|
||||||
@ -65,7 +65,7 @@ class AIService {
|
|||||||
} else {
|
} else {
|
||||||
logger.error(`[AI Service] Initialization error: ${error.message}`);
|
logger.error(`[AI Service] Initialization error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isInitialized = true; // Mark as initialized even if failed to prevent infinite loops
|
this.isInitialized = true; // Mark as initialized even if failed to prevent infinite loops
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,7 +115,7 @@ class AIService {
|
|||||||
|
|
||||||
const streamingResp = await generativeModel.generateContent(request);
|
const streamingResp = await generativeModel.generateContent(request);
|
||||||
const response = streamingResp.response;
|
const response = streamingResp.response;
|
||||||
|
|
||||||
// Log full response structure for debugging if empty
|
// Log full response structure for debugging if empty
|
||||||
if (!response.candidates || response.candidates.length === 0) {
|
if (!response.candidates || response.candidates.length === 0) {
|
||||||
logger.error('[AI Service] No candidates in Vertex AI response:', {
|
logger.error('[AI Service] No candidates in Vertex AI response:', {
|
||||||
@ -125,12 +125,12 @@ class AIService {
|
|||||||
});
|
});
|
||||||
throw new Error('Vertex AI returned no candidates. The response may have been blocked by safety filters.');
|
throw new Error('Vertex AI returned no candidates. The response may have been blocked by safety filters.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidate = response.candidates[0];
|
const candidate = response.candidates[0];
|
||||||
|
|
||||||
// Check for safety ratings or blocked reasons
|
// Check for safety ratings or blocked reasons
|
||||||
if (candidate.safetyRatings && candidate.safetyRatings.length > 0) {
|
if (candidate.safetyRatings && candidate.safetyRatings.length > 0) {
|
||||||
const blockedRatings = candidate.safetyRatings.filter((rating: any) =>
|
const blockedRatings = candidate.safetyRatings.filter((rating: any) =>
|
||||||
rating.probability === 'HIGH' || rating.probability === 'MEDIUM'
|
rating.probability === 'HIGH' || rating.probability === 'MEDIUM'
|
||||||
);
|
);
|
||||||
if (blockedRatings.length > 0) {
|
if (blockedRatings.length > 0) {
|
||||||
@ -143,7 +143,7 @@ class AIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check finish reason
|
// Check finish reason
|
||||||
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
|
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
|
||||||
logger.warn('[AI Service] Vertex AI finish reason:', {
|
logger.warn('[AI Service] Vertex AI finish reason:', {
|
||||||
@ -151,10 +151,10 @@ class AIService {
|
|||||||
safetyRatings: candidate.safetyRatings
|
safetyRatings: candidate.safetyRatings
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract text from response
|
// Extract text from response
|
||||||
const text = candidate.content?.parts?.[0]?.text || '';
|
const text = candidate.content?.parts?.[0]?.text || '';
|
||||||
|
|
||||||
// Handle MAX_TOKENS finish reason - accept whatever response we got
|
// Handle MAX_TOKENS finish reason - accept whatever response we got
|
||||||
// We trust the AI's response - no truncation on our side
|
// We trust the AI's response - no truncation on our side
|
||||||
if (candidate.finishReason === 'MAX_TOKENS' && text) {
|
if (candidate.finishReason === 'MAX_TOKENS' && text) {
|
||||||
@ -167,7 +167,7 @@ class AIService {
|
|||||||
// Return the response without any truncation - trust what AI generated
|
// Return the response without any truncation - trust what AI generated
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
// Log detailed response structure for debugging
|
// Log detailed response structure for debugging
|
||||||
logger.error('[AI Service] Empty text in Vertex AI response:', {
|
logger.error('[AI Service] Empty text in Vertex AI response:', {
|
||||||
@ -178,7 +178,7 @@ class AIService {
|
|||||||
promptPreview: prompt.substring(0, 200) + '...',
|
promptPreview: prompt.substring(0, 200) + '...',
|
||||||
model: this.model
|
model: this.model
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provide more helpful error message
|
// Provide more helpful error message
|
||||||
if (candidate.finishReason === 'SAFETY') {
|
if (candidate.finishReason === 'SAFETY') {
|
||||||
throw new Error('Vertex AI blocked the response due to safety filters. The prompt may contain content that violates safety policies.');
|
throw new Error('Vertex AI blocked the response due to safety filters. The prompt may contain content that violates safety policies.');
|
||||||
@ -194,7 +194,7 @@ class AIService {
|
|||||||
return text;
|
return text;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('[AI Service] Vertex AI generation error:', error);
|
logger.error('[AI Service] Vertex AI generation error:', error);
|
||||||
|
|
||||||
// Provide more specific error messages
|
// Provide more specific error messages
|
||||||
if (error.message?.includes('Model was not found')) {
|
if (error.message?.includes('Model was not found')) {
|
||||||
throw new Error(`Model ${this.model} not found or not available in region ${LOCATION}. Please check model name and region.`);
|
throw new Error(`Model ${this.model} not found or not available in region ${LOCATION}. Please check model name and region.`);
|
||||||
@ -203,7 +203,7 @@ class AIService {
|
|||||||
} else if (error.message?.includes('API not enabled')) {
|
} else if (error.message?.includes('API not enabled')) {
|
||||||
throw new Error('Vertex AI API is not enabled. Please enable it in Google Cloud Console.');
|
throw new Error('Vertex AI API is not enabled. Please enable it in Google Cloud Console.');
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Vertex AI generation failed: ${error.message}`);
|
throw new Error(`Vertex AI generation failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -268,10 +268,13 @@ class AIService {
|
|||||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
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
|
// Trust AI's response - do not truncate anything
|
||||||
// AI is instructed to stay within limit, but we accept whatever it generates
|
// AI is instructed to stay within limit, but we accept whatever it generates
|
||||||
if (remarkText.length > maxLength) {
|
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.`);
|
||||||
|
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)
|
// Extract key points (look for bullet points or numbered items)
|
||||||
@ -315,7 +318,7 @@ class AIService {
|
|||||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||||
const targetWordCount = Math.floor(maxLength / 6); // Approximate words (avg 6 chars per word)
|
const targetWordCount = Math.floor(maxLength / 6); // Approximate words (avg 6 chars per word)
|
||||||
|
|
||||||
logger.info(`[AI Service] Using max remark length: ${maxLength} characters (≈${targetWordCount} words) from admin config`);
|
logger.info(`[AI Service] Using max remark length: ${maxLength} characters (≈${targetWordCount} words) from admin config`);
|
||||||
|
|
||||||
// Check if this is a rejected request
|
// Check if this is a rejected request
|
||||||
@ -333,11 +336,11 @@ class AIService {
|
|||||||
const approvalSummary = approvalFlow
|
const approvalSummary = approvalFlow
|
||||||
.filter((a: any) => a.status === 'APPROVED' || a.status === 'REJECTED')
|
.filter((a: any) => a.status === 'APPROVED' || a.status === 'REJECTED')
|
||||||
.map((a: any) => {
|
.map((a: any) => {
|
||||||
const tatPercentage = a.tatPercentageUsed !== undefined && a.tatPercentageUsed !== null
|
const tatPercentage = a.tatPercentageUsed !== undefined && a.tatPercentageUsed !== null
|
||||||
? Number(a.tatPercentageUsed)
|
? Number(a.tatPercentageUsed)
|
||||||
: (a.elapsedHours && a.tatHours ? (Number(a.elapsedHours) / Number(a.tatHours)) * 100 : 0);
|
: (a.elapsedHours && a.tatHours ? (Number(a.elapsedHours) / Number(a.tatHours)) * 100 : 0);
|
||||||
const riskStatus = getTATRiskStatus(tatPercentage);
|
const riskStatus = getTATRiskStatus(tatPercentage);
|
||||||
const tatInfo = a.elapsedHours && a.tatHours
|
const tatInfo = a.elapsedHours && a.tatHours
|
||||||
? ` (completed in ${a.elapsedHours.toFixed(1)}h of ${a.tatHours}h TAT, ${tatPercentage.toFixed(1)}% used)`
|
? ` (completed in ${a.elapsedHours.toFixed(1)}h of ${a.tatHours}h TAT, ${tatPercentage.toFixed(1)}% used)`
|
||||||
: '';
|
: '';
|
||||||
const riskInfo = riskStatus !== 'ON_TRACK' ? ` [${riskStatus}]` : '';
|
const riskInfo = riskStatus !== 'ON_TRACK' ? ` [${riskStatus}]` : '';
|
||||||
@ -358,7 +361,7 @@ class AIService {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
// Build rejection context if applicable
|
// Build rejection context if applicable
|
||||||
const rejectionContext = isRejected
|
const rejectionContext = isRejected
|
||||||
? `\n**Rejection Details:**\n- Rejected by: ${rejectedBy || 'Approver'}\n- Rejection reason: ${rejectionReason || 'Not specified'}`
|
? `\n**Rejection Details:**\n- Rejected by: ${rejectedBy || 'Approver'}\n- Rejection reason: ${rejectionReason || 'Not specified'}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@ -380,8 +383,8 @@ ${documentSummary || 'No documents'}
|
|||||||
|
|
||||||
**YOUR TASK:**
|
**YOUR TASK:**
|
||||||
Write a brief, professional conclusion (approximately ${targetWordCount} words, max ${maxLength} characters) that:
|
Write a brief, professional conclusion (approximately ${targetWordCount} words, max ${maxLength} characters) that:
|
||||||
${isRejected
|
${isRejected
|
||||||
? `- Summarizes what was requested and explains that it was rejected
|
? `- Summarizes what was requested and explains that it was rejected
|
||||||
- Mentions who rejected it and the rejection reason
|
- Mentions who rejected it and the rejection reason
|
||||||
- Notes the outcome and any learnings or next steps
|
- Notes the outcome and any learnings or next steps
|
||||||
- Mentions if any approval levels were AT_RISK, CRITICAL, or BREACHED (if applicable)
|
- Mentions if any approval levels were AT_RISK, CRITICAL, or BREACHED (if applicable)
|
||||||
@ -389,7 +392,7 @@ ${isRejected
|
|||||||
- Is suitable for permanent archiving and future reference
|
- Is suitable for permanent archiving and future reference
|
||||||
- Sounds natural and human-written (not AI-generated)
|
- Sounds natural and human-written (not AI-generated)
|
||||||
- Maintains a professional and constructive tone even for rejections`
|
- Maintains a professional and constructive tone even for rejections`
|
||||||
: `- Summarizes what was requested and the final decision
|
: `- Summarizes what was requested and the final decision
|
||||||
- Mentions who approved it and any key comments
|
- Mentions who approved it and any key comments
|
||||||
- Mentions if any approval levels were AT_RISK, CRITICAL, or BREACHED (if applicable)
|
- Mentions if any approval levels were AT_RISK, CRITICAL, or BREACHED (if applicable)
|
||||||
- Notes the outcome and next steps (if applicable)
|
- Notes the outcome and next steps (if applicable)
|
||||||
@ -450,13 +453,13 @@ Write the conclusion now in HTML format. STRICT LIMIT: ${maxLength} characters m
|
|||||||
*/
|
*/
|
||||||
private extractKeyPoints(remark: string): string[] {
|
private extractKeyPoints(remark: string): string[] {
|
||||||
const keyPoints: string[] = [];
|
const keyPoints: string[] = [];
|
||||||
|
|
||||||
// Look for bullet points (-, •, *) or numbered items (1., 2., etc.)
|
// Look for bullet points (-, •, *) or numbered items (1., 2., etc.)
|
||||||
const lines = remark.split('\n');
|
const lines = remark.split('\n');
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
// Match bullet points
|
// Match bullet points
|
||||||
if (trimmed.match(/^[-•*]\s+(.+)$/)) {
|
if (trimmed.match(/^[-•*]\s+(.+)$/)) {
|
||||||
const point = trimmed.replace(/^[-•*]\s+/, '');
|
const point = trimmed.replace(/^[-•*]\s+/, '');
|
||||||
@ -464,7 +467,7 @@ Write the conclusion now in HTML format. STRICT LIMIT: ${maxLength} characters m
|
|||||||
keyPoints.push(point);
|
keyPoints.push(point);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match numbered items
|
// Match numbered items
|
||||||
if (trimmed.match(/^\d+\.\s+(.+)$/)) {
|
if (trimmed.match(/^\d+\.\s+(.+)$/)) {
|
||||||
const point = trimmed.replace(/^\d+\.\s+/, '');
|
const point = trimmed.replace(/^\d+\.\s+/, '');
|
||||||
@ -473,13 +476,13 @@ Write the conclusion now in HTML format. STRICT LIMIT: ${maxLength} characters m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no bullet points found, extract first few sentences
|
// If no bullet points found, extract first few sentences
|
||||||
if (keyPoints.length === 0) {
|
if (keyPoints.length === 0) {
|
||||||
const sentences = remark.split(/[.!?]+/).filter(s => s.trim().length > 20);
|
const sentences = remark.split(/[.!?]+/).filter(s => s.trim().length > 20);
|
||||||
keyPoints.push(...sentences.slice(0, 3).map(s => s.trim()));
|
keyPoints.push(...sentences.slice(0, 3).map(s => s.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyPoints.slice(0, 5); // Max 5 key points
|
return keyPoints.slice(0, 5); // Max 5 key points
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,22 +491,22 @@ Write the conclusion now in HTML format. STRICT LIMIT: ${maxLength} characters m
|
|||||||
*/
|
*/
|
||||||
private calculateConfidence(remark: string, context: any): number {
|
private calculateConfidence(remark: string, context: any): number {
|
||||||
let score = 0.6; // Base score
|
let score = 0.6; // Base score
|
||||||
|
|
||||||
// Check if remark has good length (100-400 chars - more realistic)
|
// Check if remark has good length (100-400 chars - more realistic)
|
||||||
if (remark.length >= 100 && remark.length <= 400) {
|
if (remark.length >= 100 && remark.length <= 400) {
|
||||||
score += 0.2;
|
score += 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if remark mentions key elements
|
// Check if remark mentions key elements
|
||||||
if (remark.toLowerCase().includes('approv')) {
|
if (remark.toLowerCase().includes('approv')) {
|
||||||
score += 0.1;
|
score += 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if remark is not too generic
|
// Check if remark is not too generic
|
||||||
if (remark.length > 80 && !remark.toLowerCase().includes('lorem ipsum')) {
|
if (remark.length > 80 && !remark.toLowerCase().includes('lorem ipsum')) {
|
||||||
score += 0.1;
|
score += 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.min(1.0, score);
|
return Math.min(1.0, score);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export class EmailService {
|
|||||||
private async initializeTestAccount(): Promise<void> {
|
private async initializeTestAccount(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.testAccountInfo = await nodemailer.createTestAccount();
|
this.testAccountInfo = await nodemailer.createTestAccount();
|
||||||
|
|
||||||
this.transporter = nodemailer.createTransport({
|
this.transporter = nodemailer.createTransport({
|
||||||
host: this.testAccountInfo.smtp.host,
|
host: this.testAccountInfo.smtp.host,
|
||||||
port: this.testAccountInfo.smtp.port,
|
port: this.testAccountInfo.smtp.port,
|
||||||
@ -111,7 +111,7 @@ export class EmailService {
|
|||||||
const smtpHost = process.env.SMTP_HOST;
|
const smtpHost = process.env.SMTP_HOST;
|
||||||
const smtpUser = process.env.SMTP_USER;
|
const smtpUser = process.env.SMTP_USER;
|
||||||
const smtpPassword = process.env.SMTP_PASSWORD;
|
const smtpPassword = process.env.SMTP_PASSWORD;
|
||||||
|
|
||||||
if (smtpHost && smtpUser && smtpPassword) {
|
if (smtpHost && smtpUser && smtpPassword) {
|
||||||
logger.info('📧 SMTP credentials detected - re-initializing email service with production SMTP');
|
logger.info('📧 SMTP credentials detected - re-initializing email service with production SMTP');
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
@ -149,11 +149,11 @@ export class EmailService {
|
|||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const info = await this.transporter!.sendMail(mailOptions);
|
const info = await this.transporter!.sendMail(mailOptions);
|
||||||
|
|
||||||
if (!info || !info.messageId) {
|
if (!info || !info.messageId) {
|
||||||
throw new Error('Email sent but no messageId returned');
|
throw new Error('Email sent but no messageId returned');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: { messageId: string; previewUrl?: string } = {
|
const result: { messageId: string; previewUrl?: string } = {
|
||||||
messageId: info.messageId
|
messageId: info.messageId
|
||||||
};
|
};
|
||||||
@ -162,10 +162,10 @@ export class EmailService {
|
|||||||
if (this.useTestAccount) {
|
if (this.useTestAccount) {
|
||||||
try {
|
try {
|
||||||
const previewUrl = nodemailer.getTestMessageUrl(info);
|
const previewUrl = nodemailer.getTestMessageUrl(info);
|
||||||
|
|
||||||
if (previewUrl) {
|
if (previewUrl) {
|
||||||
result.previewUrl = previewUrl;
|
result.previewUrl = previewUrl;
|
||||||
|
|
||||||
// Always log to console for visibility
|
// Always log to console for visibility
|
||||||
console.log('\n' + '='.repeat(80));
|
console.log('\n' + '='.repeat(80));
|
||||||
console.log(`📧 EMAIL PREVIEW (${options.subject})`);
|
console.log(`📧 EMAIL PREVIEW (${options.subject})`);
|
||||||
@ -176,7 +176,7 @@ export class EmailService {
|
|||||||
console.log(`Preview URL: ${previewUrl}`);
|
console.log(`Preview URL: ${previewUrl}`);
|
||||||
console.log(`Message ID: ${info.messageId}`);
|
console.log(`Message ID: ${info.messageId}`);
|
||||||
console.log('='.repeat(80) + '\n');
|
console.log('='.repeat(80) + '\n');
|
||||||
|
|
||||||
logger.info(`✅ Email sent (TEST MODE) to ${recipients}`);
|
logger.info(`✅ Email sent (TEST MODE) to ${recipients}`);
|
||||||
logger.info(`📧 Preview URL: ${previewUrl}`);
|
logger.info(`📧 Preview URL: ${previewUrl}`);
|
||||||
} else {
|
} else {
|
||||||
@ -198,7 +198,7 @@ export class EmailService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
logger.error(`❌ Email send attempt ${attempt}/${maxRetries} failed:`, error);
|
logger.error(`❌ Email send attempt ${attempt}/${maxRetries} failed:`, error);
|
||||||
|
|
||||||
if (attempt < maxRetries) {
|
if (attempt < maxRetries) {
|
||||||
const delay = parseInt(process.env.EMAIL_RETRY_DELAY || '5000') * attempt;
|
const delay = parseInt(process.env.EMAIL_RETRY_DELAY || '5000') * attempt;
|
||||||
logger.info(`⏳ Retrying in ${delay}ms...`);
|
logger.info(`⏳ Retrying in ${delay}ms...`);
|
||||||
@ -217,22 +217,22 @@ export class EmailService {
|
|||||||
*/
|
*/
|
||||||
async sendBatch(emails: EmailOptions[]): Promise<void> {
|
async sendBatch(emails: EmailOptions[]): Promise<void> {
|
||||||
logger.info(`📧 Sending batch of ${emails.length} emails`);
|
logger.info(`📧 Sending batch of ${emails.length} emails`);
|
||||||
|
|
||||||
const batchSize = parseInt(process.env.EMAIL_BATCH_SIZE || '10');
|
const batchSize = parseInt(process.env.EMAIL_BATCH_SIZE || '10');
|
||||||
|
|
||||||
for (let i = 0; i < emails.length; i += batchSize) {
|
for (let i = 0; i < emails.length; i += batchSize) {
|
||||||
const batch = emails.slice(i, i + batchSize);
|
const batch = emails.slice(i, i + batchSize);
|
||||||
|
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
batch.map(email => this.sendEmail(email))
|
batch.map(email => this.sendEmail(email))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Small delay between batches to avoid rate limiting
|
// Small delay between batches to avoid rate limiting
|
||||||
if (i + batchSize < emails.length) {
|
if (i + batchSize < emails.length) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`✅ Batch email sending complete`);
|
logger.info(`✅ Batch email sending complete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getRequestCreatedEmail(data);
|
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({
|
const result = await emailService.sendEmail({
|
||||||
to: initiatorData.email,
|
to: initiatorData.email,
|
||||||
@ -162,9 +162,9 @@ export class EmailNotificationService {
|
|||||||
// Multi-level approval email
|
// Multi-level approval email
|
||||||
const chainData: ApprovalChainItem[] = approvalChain.map((level: any) => ({
|
const chainData: ApprovalChainItem[] = approvalChain.map((level: any) => ({
|
||||||
name: level.approverName || level.approverEmail,
|
name: level.approverName || level.approverEmail,
|
||||||
status: level.status === 'APPROVED' ? 'approved'
|
status: level.status === 'APPROVED' ? 'approved'
|
||||||
: level.levelNumber === approverData.levelNumber ? 'current'
|
: level.levelNumber === approverData.levelNumber ? 'current'
|
||||||
: level.levelNumber < approverData.levelNumber ? 'pending'
|
: level.levelNumber < approverData.levelNumber ? 'pending'
|
||||||
: 'awaiting',
|
: 'awaiting',
|
||||||
date: level.approvedAt ? this.formatDate(level.approvedAt) : undefined,
|
date: level.approvedAt ? this.formatDate(level.approvedAt) : undefined,
|
||||||
levelNumber: level.levelNumber
|
levelNumber: level.levelNumber
|
||||||
@ -189,7 +189,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getMultiApproverRequestEmail(data);
|
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({
|
const result = await emailService.sendEmail({
|
||||||
to: approverData.email,
|
to: approverData.email,
|
||||||
@ -218,7 +218,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getApprovalRequestEmail(data);
|
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({
|
const result = await emailService.sendEmail({
|
||||||
to: approverData.email,
|
to: approverData.email,
|
||||||
@ -272,7 +272,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getApprovalConfirmationEmail(data);
|
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({
|
const result = await emailService.sendEmail({
|
||||||
to: initiatorData.email,
|
to: initiatorData.email,
|
||||||
@ -323,7 +323,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getRejectionNotificationEmail(data);
|
const html = getRejectionNotificationEmail(data);
|
||||||
const subject = `[${requestData.requestNumber}] Request Rejected`;
|
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Rejected`;
|
||||||
|
|
||||||
const result = await emailService.sendEmail({
|
const result = await emailService.sendEmail({
|
||||||
to: initiatorData.email,
|
to: initiatorData.email,
|
||||||
@ -364,9 +364,9 @@ export class EmailNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine urgency level based on threshold
|
// Determine urgency level based on threshold
|
||||||
const urgencyLevel = tatInfo.thresholdPercentage >= 75 ? 'high'
|
const urgencyLevel = tatInfo.thresholdPercentage >= 75 ? 'high'
|
||||||
: tatInfo.thresholdPercentage >= 50 ? 'medium'
|
: tatInfo.thresholdPercentage >= 50 ? 'medium'
|
||||||
: 'low';
|
: 'low';
|
||||||
|
|
||||||
// Get initiator name - try from requestData first, then fetch if needed
|
// Get initiator name - try from requestData first, then fetch if needed
|
||||||
let initiatorName = requestData.initiatorName || requestData.initiator?.displayName || 'Initiator';
|
let initiatorName = requestData.initiatorName || requestData.initiator?.displayName || 'Initiator';
|
||||||
@ -399,7 +399,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getTATReminderEmail(data);
|
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({
|
const result = await emailService.sendEmail({
|
||||||
to: approverData.email,
|
to: approverData.email,
|
||||||
@ -469,7 +469,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getTATBreachedEmail(data);
|
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({
|
const result = await emailService.sendEmail({
|
||||||
to: approverData.email,
|
to: approverData.email,
|
||||||
@ -516,8 +516,8 @@ export class EmailNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAutoResumed = !resumedByData || resumedByData.userId === 'system';
|
const isAutoResumed = !resumedByData || resumedByData.userId === 'system';
|
||||||
const resumedByText = isAutoResumed
|
const resumedByText = isAutoResumed
|
||||||
? 'automatically'
|
? 'automatically'
|
||||||
: `by ${resumedByData.displayName || resumedByData.email}`;
|
: `by ${resumedByData.displayName || resumedByData.email}`;
|
||||||
|
|
||||||
const data: WorkflowResumedData = {
|
const data: WorkflowResumedData = {
|
||||||
@ -529,7 +529,7 @@ export class EmailNotificationService {
|
|||||||
resumedTime: this.formatTime(new Date()),
|
resumedTime: this.formatTime(new Date()),
|
||||||
pausedDuration: pauseDuration,
|
pausedDuration: pauseDuration,
|
||||||
currentApprover: approverData.displayName || approverData.email,
|
currentApprover: approverData.displayName || approverData.email,
|
||||||
newTATDeadline: requestData.tatDeadline
|
newTATDeadline: requestData.tatDeadline
|
||||||
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
||||||
: 'To be determined',
|
: 'To be determined',
|
||||||
isApprover: true,
|
isApprover: true,
|
||||||
@ -538,7 +538,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getWorkflowResumedEmail(data);
|
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({
|
const result = await emailService.sendEmail({
|
||||||
to: approverData.email,
|
to: approverData.email,
|
||||||
@ -585,8 +585,8 @@ export class EmailNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAutoResumed = !resumedByData || resumedByData.userId === 'system' || !resumedByData.userId;
|
const isAutoResumed = !resumedByData || resumedByData.userId === 'system' || !resumedByData.userId;
|
||||||
const resumedByText = isAutoResumed
|
const resumedByText = isAutoResumed
|
||||||
? 'automatically'
|
? 'automatically'
|
||||||
: `by ${resumedByData.displayName || resumedByData.email || resumedByData.name || 'User'}`;
|
: `by ${resumedByData.displayName || resumedByData.email || resumedByData.name || 'User'}`;
|
||||||
|
|
||||||
const data: WorkflowResumedData = {
|
const data: WorkflowResumedData = {
|
||||||
@ -598,7 +598,7 @@ export class EmailNotificationService {
|
|||||||
resumedTime: this.formatTime(new Date()),
|
resumedTime: this.formatTime(new Date()),
|
||||||
pausedDuration: pauseDuration,
|
pausedDuration: pauseDuration,
|
||||||
currentApprover: approverData?.displayName || approverData?.email || 'Current Approver',
|
currentApprover: approverData?.displayName || approverData?.email || 'Current Approver',
|
||||||
newTATDeadline: requestData.tatDeadline
|
newTATDeadline: requestData.tatDeadline
|
||||||
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
||||||
: 'To be determined',
|
: 'To be determined',
|
||||||
isApprover: false, // This is for initiator
|
isApprover: false, // This is for initiator
|
||||||
@ -607,7 +607,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getWorkflowResumedEmail(data);
|
const html = getWorkflowResumedEmail(data);
|
||||||
const subject = `[${requestData.requestNumber}] Workflow Resumed`;
|
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed`;
|
||||||
|
|
||||||
const result = await emailService.sendEmail({
|
const result = await emailService.sendEmail({
|
||||||
to: initiatorData.email,
|
to: initiatorData.email,
|
||||||
@ -685,7 +685,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getRequestClosedEmail(data);
|
const html = getRequestClosedEmail(data);
|
||||||
const subject = `[${requestData.requestNumber}] Request Closed`;
|
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Closed`;
|
||||||
|
|
||||||
const result = await emailService.sendEmail({
|
const result = await emailService.sendEmail({
|
||||||
to: recipientData.email,
|
to: recipientData.email,
|
||||||
@ -710,7 +710,7 @@ export class EmailNotificationService {
|
|||||||
closureData: any
|
closureData: any
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.info(`📧 Sending Request Closed emails to ${participants.length} participants`);
|
logger.info(`📧 Sending Request Closed emails to ${participants.length} participants`);
|
||||||
|
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
await this.sendRequestClosed(requestData, participant, closureData);
|
await this.sendRequestClosed(requestData, participant, closureData);
|
||||||
// Small delay to avoid rate limiting
|
// Small delay to avoid rate limiting
|
||||||
@ -754,7 +754,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getApproverSkippedEmail(data);
|
const html = getApproverSkippedEmail(data);
|
||||||
const subject = `[${requestData.requestNumber}] Approver Skipped`;
|
const subject = `${requestData.requestNumber} - ${requestData.title} - Approver Skipped`;
|
||||||
|
|
||||||
const result = await emailService.sendEmail({
|
const result = await emailService.sendEmail({
|
||||||
to: skippedApproverData.email,
|
to: skippedApproverData.email,
|
||||||
@ -814,7 +814,7 @@ export class EmailNotificationService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const html = getWorkflowPausedEmail(data);
|
const html = getWorkflowPausedEmail(data);
|
||||||
const subject = `[${requestData.requestNumber}] Workflow Paused`;
|
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Paused`;
|
||||||
|
|
||||||
const result = await emailService.sendEmail({
|
const result = await emailService.sendEmail({
|
||||||
to: recipientData.email,
|
to: recipientData.email,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user