Compare commits

...

7 Commits

32 changed files with 1331 additions and 947 deletions

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -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();

View File

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

View 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'
});
}
};

View 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
}

View File

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

View File

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

View 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;

View File

@ -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,
@ -136,6 +137,7 @@ 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 },
@ -184,8 +186,11 @@ 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
@ -201,10 +206,10 @@ 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
@ -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();
@ -282,6 +292,9 @@ async function autoSetup(): Promise<void> {
console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.'); console.log('📝 Note: 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');

View 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();

View 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();

View File

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

View File

@ -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,7 +18,30 @@ 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)
// This ensures the email service uses production SMTP if credentials are available
try {
await emailService.initialize();
console.log('📧 Email service re-initialized after secrets loaded');
} catch (error) {
console.warn('⚠️ Email service re-initialization warning (will use test account if SMTP not configured):', error);
}
// Re-initialize email service after secrets are loaded (in case SMTP credentials were loaded) // 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

View File

@ -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)
@ -381,7 +384,7 @@ ${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)

View File

@ -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,
@ -163,8 +163,8 @@ export class EmailNotificationService {
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,
@ -365,8 +365,8 @@ 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,
@ -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,
@ -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,
@ -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,

View File

@ -899,8 +899,8 @@ export class WorkflowService {
include: [{ model: User, as: 'approver', attributes: ['userId', 'email', 'displayName'] }], include: [{ model: User, as: 'approver', attributes: ['userId', 'email', 'displayName'] }],
// Include pause-related fields for SLA calculation // Include pause-related fields for SLA calculation
attributes: ['levelId', 'levelNumber', 'levelName', 'approverId', 'approverEmail', 'approverName', attributes: ['levelId', 'levelNumber', 'levelName', 'approverId', 'approverEmail', 'approverName',
'tatHours', 'tatDays', 'status', 'levelStartTime', 'tatStartTime', 'levelEndTime', 'tatHours', 'tatDays', 'status', 'levelStartTime', 'tatStartTime', 'levelEndTime',
'isPaused', 'pausedAt', 'pauseElapsedHours', 'pauseResumeDate', 'elapsedHours'] 'isPaused', 'pausedAt', 'pauseElapsedHours', 'pauseResumeDate', 'elapsedHours']
}); });
// Fetch all approval levels for this request (including pause fields for SLA calculation) // Fetch all approval levels for this request (including pause fields for SLA calculation)
@ -1952,14 +1952,16 @@ export class WorkflowService {
// Include PAUSED status so paused requests where user is the current approver are shown // Include PAUSED status so paused requests where user is the current approver are shown
const pendingLevels = await ApprovalLevel.findAll({ const pendingLevels = await ApprovalLevel.findAll({
where: { where: {
status: { [Op.in]: [ status: {
ApprovalStatus.PENDING as any, [Op.in]: [
(ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS', ApprovalStatus.PENDING as any,
ApprovalStatus.PAUSED as any, (ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS',
'PENDING', ApprovalStatus.PAUSED as any,
'IN_PROGRESS', 'PENDING',
'PAUSED' 'IN_PROGRESS',
] as any }, 'PAUSED'
] as any
},
}, },
order: [['requestId', 'ASC'], ['levelNumber', 'ASC']], order: [['requestId', 'ASC'], ['levelNumber', 'ASC']],
attributes: ['requestId', 'levelNumber', 'approverId'], attributes: ['requestId', 'levelNumber', 'approverId'],
@ -2028,15 +2030,17 @@ export class WorkflowService {
baseConditions.push({ baseConditions.push({
[Op.or]: [ [Op.or]: [
{ {
status: { [Op.in]: [ status: {
WorkflowStatus.PENDING as any, [Op.in]: [
WorkflowStatus.APPROVED as any, WorkflowStatus.PENDING as any,
WorkflowStatus.PAUSED as any, WorkflowStatus.APPROVED as any,
'PENDING', WorkflowStatus.PAUSED as any,
'IN_PROGRESS', // Legacy support - will be migrated to PENDING 'PENDING',
'APPROVED', 'IN_PROGRESS', // Legacy support - will be migrated to PENDING
'PAUSED' 'APPROVED',
] as any } 'PAUSED'
] as any
}
}, },
// Also include requests with isPaused = true (even if status is PENDING) // Also include requests with isPaused = true (even if status is PENDING)
{ {
@ -2063,10 +2067,12 @@ export class WorkflowService {
baseConditions.push({ baseConditions.push({
[Op.and]: [ [Op.and]: [
{ status: statusUpper }, { status: statusUpper },
{ [Op.or]: [ {
{ isPaused: { [Op.is]: null } }, [Op.or]: [
{ isPaused: false } { isPaused: { [Op.is]: null } },
]} { isPaused: false }
]
}
] ]
}); });
} }
@ -2204,12 +2210,14 @@ export class WorkflowService {
const levelRows = await ApprovalLevel.findAll({ const levelRows = await ApprovalLevel.findAll({
where: { where: {
approverId: userId, approverId: userId,
status: { [Op.in]: [ status: {
ApprovalStatus.APPROVED as any, [Op.in]: [
(ApprovalStatus as any).REJECTED ?? 'REJECTED', ApprovalStatus.APPROVED as any,
'APPROVED', (ApprovalStatus as any).REJECTED ?? 'REJECTED',
'REJECTED' 'APPROVED',
] as any }, 'REJECTED'
] as any
},
}, },
attributes: ['requestId'], attributes: ['requestId'],
}); });
@ -2272,37 +2280,37 @@ export class WorkflowService {
} }
} }
// Apply priority filter // Apply priority filter
if (filters?.priority && filters.priority !== 'all') { if (filters?.priority && filters.priority !== 'all') {
approverConditionParts.push({ priority: filters.priority.toUpperCase() }); approverConditionParts.push({ priority: filters.priority.toUpperCase() });
} }
// Apply templateType filter // Apply templateType filter
if (filters?.templateType && filters.templateType !== 'all') { if (filters?.templateType && filters.templateType !== 'all') {
const templateTypeUpper = filters.templateType.toUpperCase(); const templateTypeUpper = filters.templateType.toUpperCase();
// For CUSTOM, also include null values (legacy requests without templateType) // For CUSTOM, also include null values (legacy requests without templateType)
if (templateTypeUpper === 'CUSTOM') { if (templateTypeUpper === 'CUSTOM') {
approverConditionParts.push({
[Op.or]: [
{ templateType: 'CUSTOM' },
{ templateType: null }
]
});
} else {
approverConditionParts.push({ templateType: templateTypeUpper });
}
}
// Apply search filter (title, description, or requestNumber)
if (filters?.search && filters.search.trim()) {
approverConditionParts.push({ approverConditionParts.push({
[Op.or]: [ [Op.or]: [
{ templateType: 'CUSTOM' }, { title: { [Op.iLike]: `%${filters.search.trim()}%` } },
{ templateType: null } { description: { [Op.iLike]: `%${filters.search.trim()}%` } },
{ requestNumber: { [Op.iLike]: `%${filters.search.trim()}%` } }
] ]
}); });
} else {
approverConditionParts.push({ templateType: templateTypeUpper });
} }
}
// Apply search filter (title, description, or requestNumber)
if (filters?.search && filters.search.trim()) {
approverConditionParts.push({
[Op.or]: [
{ title: { [Op.iLike]: `%${filters.search.trim()}%` } },
{ description: { [Op.iLike]: `%${filters.search.trim()}%` } },
{ requestNumber: { [Op.iLike]: `%${filters.search.trim()}%` } }
]
});
}
const approverCondition = approverConditionParts.length > 0 const approverCondition = approverConditionParts.length > 0
? { [Op.and]: approverConditionParts } ? { [Op.and]: approverConditionParts }
@ -2687,7 +2695,7 @@ export class WorkflowService {
// Reload with associations // Reload with associations
const workflow = await WorkflowRequest.findByPk(actualRequestId, { const workflow = await WorkflowRequest.findByPk(actualRequestId, {
include: [ { association: 'initiator' } ] include: [{ association: 'initiator' }]
}); });
if (!workflow) return null; if (!workflow) return null;
@ -2783,7 +2791,7 @@ export class WorkflowService {
// Use the actual UUID requestId for all queries // Use the actual UUID requestId for all queries
const approvals = await ApprovalLevel.findAll({ const approvals = await ApprovalLevel.findAll({
where: { requestId: actualRequestId }, where: { requestId: actualRequestId },
order: [['levelNumber','ASC']] order: [['levelNumber', 'ASC']]
}) as any[]; }) as any[];
const participants = await Participant.findAll({ const participants = await Participant.findAll({
@ -3312,24 +3320,24 @@ export class WorkflowService {
// For direct submissions, createWorkflow already logs "Initial request submitted" // For direct submissions, createWorkflow already logs "Initial request submitted"
if (existingActivities > 1) { if (existingActivities > 1) {
// This is a saved draft being submitted later // This is a saved draft being submitted later
activityService.log({ activityService.log({
requestId: actualRequestId, requestId: actualRequestId,
type: 'submitted', type: 'submitted',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined, user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
action: 'Draft submitted', action: 'Draft submitted',
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}` details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
}); });
} else { } else {
// Direct submission - just update the status, createWorkflow already logged the activity // Direct submission - just update the status, createWorkflow already logged the activity
activityService.log({ activityService.log({
requestId: actualRequestId, requestId: actualRequestId,
type: 'submitted', type: 'submitted',
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined, user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
action: 'Request submitted', action: 'Request submitted',
details: `Request "${workflowTitle}" submitted for approval` details: `Request "${workflowTitle}" submitted for approval`
}); });
} }
const current = await ApprovalLevel.findOne({ const current = await ApprovalLevel.findOne({