Compare commits

..

No commits in common. "be220bbb0ca7b8c09cb10548aaec119d7b5fce91" and "4cf72888579f6d9970c01f8cdc25eede43e5283f" have entirely different histories.

20 changed files with 571 additions and 861 deletions

View File

@ -1,2 +1,2 @@
import{a as t}from"./index-D5U31xpx.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion}; import{a as t}from"./index-9cOIFSn9.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-BmvKDhMD.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-CRr9x_Jp.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
//# sourceMappingURL=conclusionApi-xBwvOJP0.js.map //# sourceMappingURL=conclusionApi-uNxtglEr.js.map

View File

@ -1 +1 @@
{"version":3,"file":"conclusionApi-xBwvOJP0.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"} {"version":3,"file":"conclusionApi-uNxtglEr.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAKA,eAAsBC,EAAcJ,EAA8C,CAEhF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB"}

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,7 +52,7 @@
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
</style> </style>
<script type="module" crossorigin src="/assets/index-D5U31xpx.js"></script> <script type="module" crossorigin src="/assets/index-9cOIFSn9.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js"> <link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js"> <link rel="modulepreload" crossorigin href="/assets/radix-vendor-C2EbRL2a.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js"> <link rel="modulepreload" crossorigin href="/assets/utils-vendor-DHm03ykU.js">
@ -60,7 +60,7 @@
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js"> <link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js"> <link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-CRr9x_Jp.js"> <link rel="modulepreload" crossorigin href="/assets/router-vendor-CRr9x_Jp.js">
<link rel="stylesheet" crossorigin href="/assets/index-DwXE9Ynd.css"> <link rel="stylesheet" crossorigin href="/assets/index-BmOYs32D.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,56 +0,0 @@
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({
name,
description,
category,
priority,
estimatedTime,
approvers,
suggestedSLA,
createdBy: userId,
isActive: true
});
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'
});
}
};

View File

@ -1,84 +0,0 @@
import { QueryInterface, DataTypes } from 'sequelize';
export async function up(queryInterface: QueryInterface): Promise<void> {
// Drop table if exists to ensure schema is correct (CASCADE to remove FKs)
await queryInterface.dropTable('workflow_templates', { cascade: true });
// Create Enum for Template Priority
await queryInterface.sequelize.query(`DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'enum_template_priority') THEN
CREATE TYPE enum_template_priority AS ENUM ('low', 'medium', 'high');
END IF;
END$$;`);
await queryInterface.createTable('workflow_templates', {
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4
},
name: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
category: {
type: DataTypes.STRING,
defaultValue: 'General'
},
priority: {
type: 'enum_template_priority',
defaultValue: 'medium'
},
estimated_time: {
type: DataTypes.STRING,
defaultValue: 'Variable'
},
approvers: {
type: DataTypes.JSONB,
defaultValue: []
},
suggested_sla: {
type: DataTypes.INTEGER,
defaultValue: 24
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
created_by: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'user_id'
}
},
fields: {
type: DataTypes.JSONB,
defaultValue: {}
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
});
// Add index on created_by
await queryInterface.sequelize.query('CREATE INDEX IF NOT EXISTS "workflow_templates_created_by" ON "workflow_templates" ("created_by");');
}
export async function down(queryInterface: QueryInterface): Promise<void> {
await queryInterface.dropTable('workflow_templates');
await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_template_priority;');
}

View File

@ -1,99 +0,0 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '@config/database';
import { User } from './User';
export class WorkflowTemplate extends Model {
public id!: string;
public name!: string;
public description!: string;
public category!: string;
public priority!: 'low' | 'medium' | 'high';
public estimatedTime!: string;
public approvers!: any[];
public suggestedSLA!: number;
public isActive!: boolean;
public createdBy!: string;
public fields!: any;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
}
WorkflowTemplate.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
category: {
type: DataTypes.STRING,
defaultValue: 'General'
},
priority: {
type: DataTypes.ENUM('low', 'medium', 'high'),
defaultValue: 'medium'
},
estimatedTime: {
type: DataTypes.STRING,
defaultValue: 'Variable',
field: 'estimated_time'
},
approvers: {
type: DataTypes.JSONB,
defaultValue: []
},
suggestedSLA: {
type: DataTypes.INTEGER,
defaultValue: 24,
comment: 'In hours',
field: 'suggested_sla'
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
field: 'is_active'
},
createdBy: {
type: DataTypes.UUID,
allowNull: true,
field: 'created_by',
references: {
model: 'users',
key: 'user_id'
}
},
fields: {
type: DataTypes.JSONB,
defaultValue: {}
},
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,
tableName: 'workflow_templates',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at'
}
);

View File

@ -16,7 +16,6 @@ import { Notification } from './Notification';
import ConclusionRemark from './ConclusionRemark'; import ConclusionRemark from './ConclusionRemark';
import RequestSummary from './RequestSummary'; import RequestSummary from './RequestSummary';
import SharedSummary from './SharedSummary'; import SharedSummary from './SharedSummary';
import { WorkflowTemplate } from './WorkflowTemplate';
// Define associations // Define associations
const defineAssociations = () => { const defineAssociations = () => {
@ -139,8 +138,7 @@ export {
Notification, Notification,
ConclusionRemark, ConclusionRemark,
RequestSummary, RequestSummary,
SharedSummary, SharedSummary
WorkflowTemplate
}; };
// Export default sequelize instance // Export default sequelize instance

View File

@ -13,7 +13,6 @@ import dashboardRoutes from './dashboard.routes';
import notificationRoutes from './notification.routes'; import notificationRoutes from './notification.routes';
import conclusionRoutes from './conclusion.routes'; import conclusionRoutes from './conclusion.routes';
import aiRoutes from './ai.routes'; import aiRoutes from './ai.routes';
import workflowTemplateRoutes from './workflowTemplate.routes';
const router = Router(); const router = Router();
@ -41,7 +40,6 @@ router.use('/notifications', notificationRoutes);
router.use('/conclusions', conclusionRoutes); router.use('/conclusions', conclusionRoutes);
router.use('/ai', aiRoutes); router.use('/ai', aiRoutes);
router.use('/summaries', summaryRoutes); router.use('/summaries', summaryRoutes);
router.use('/templates', workflowTemplateRoutes);
// TODO: Add other route modules as they are implemented // TODO: Add other route modules as they are implemented
// router.use('/approvals', approvalRoutes); // router.use('/approvals', approvalRoutes);

View File

@ -1,14 +0,0 @@
import { Router } from 'express';
import { createTemplate, getTemplates } 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);
export default router;

View File

@ -120,7 +120,6 @@ async function runMigrations(): Promise<void> {
const m27 = require('../migrations/20250127-migrate-in-progress-to-pending'); const m27 = require('../migrations/20250127-migrate-in-progress-to-pending');
const m28 = require('../migrations/20250130-migrate-to-vertex-ai'); const m28 = require('../migrations/20250130-migrate-to-vertex-ai');
const m29 = require('../migrations/20251203-add-user-notification-preferences'); const m29 = require('../migrations/20251203-add-user-notification-preferences');
const m30 = require('../migrations/20260122-create-workflow-templates');
const migrations = [ const migrations = [
{ name: '2025103000-create-users', module: m0 }, { name: '2025103000-create-users', module: m0 },
@ -153,7 +152,6 @@ async function runMigrations(): Promise<void> {
{ name: '20250127-migrate-in-progress-to-pending', module: m27 }, { name: '20250127-migrate-in-progress-to-pending', module: m27 },
{ name: '20250130-migrate-to-vertex-ai', module: m28 }, { name: '20250130-migrate-to-vertex-ai', module: m28 },
{ name: '20251203-add-user-notification-preferences', module: m29 }, { name: '20251203-add-user-notification-preferences', module: m29 },
{ name: '20260122-create-workflow-templates', module: m30 },
]; ];
const queryInterface = sequelize.getQueryInterface(); const queryInterface = sequelize.getQueryInterface();

View File

@ -10,7 +10,6 @@ import { seedDefaultConfigurations } from './services/configSeed.service';
import { startPauseResumeJob } from './jobs/pauseResumeJob'; import { startPauseResumeJob } from './jobs/pauseResumeJob';
import './queues/pauseResumeWorker'; // Initialize pause resume worker import './queues/pauseResumeWorker'; // Initialize pause resume worker
import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics'; import { initializeQueueMetrics, stopQueueMetrics } from './utils/queueMetrics';
import { emailService } from './services/email.service';
const PORT: number = parseInt(process.env.PORT || '5000', 10); const PORT: number = parseInt(process.env.PORT || '5000', 10);
@ -21,15 +20,6 @@ const startServer = async (): Promise<void> => {
// This will merge secrets from GCS into process.env if enabled // This will merge secrets from GCS into process.env if enabled
await initializeSecrets(); await initializeSecrets();
// 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);

View File

@ -99,11 +99,10 @@ class AIService {
try { try {
// Get the generative model // Get the generative model
// Increase maxOutputTokens to handle longer conclusions (up to ~4000 tokens ≈ 3000 words)
const generativeModel = this.vertexAI.getGenerativeModel({ const generativeModel = this.vertexAI.getGenerativeModel({
model: this.model, model: this.model,
generationConfig: { generationConfig: {
maxOutputTokens: 4096, // Increased from 2048 to handle longer conclusions maxOutputTokens: 2048,
temperature: 0.3, temperature: 0.3,
}, },
}); });
@ -155,19 +154,6 @@ class AIService {
// 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
// We trust the AI's response - no truncation on our side
if (candidate.finishReason === 'MAX_TOKENS' && text) {
// Accept the response as-is - AI was instructed to stay within limits
// If it hit the limit, we still use what we got (no truncation on our side)
logger.info('[AI Service] Vertex AI response hit token limit, but content received is preserved as-is:', {
textLength: text.length,
finishReason: candidate.finishReason
});
// Return the response without any truncation - trust what AI generated
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:', {
@ -183,7 +169,7 @@ class AIService {
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.');
} else if (candidate.finishReason === 'MAX_TOKENS') { } else if (candidate.finishReason === 'MAX_TOKENS') {
throw new Error('Vertex AI response was truncated due to token limit. The prompt may be too long or the response limit was exceeded.'); throw new Error('Vertex AI response was truncated due to token limit.');
} else if (candidate.finishReason === 'RECITATION') { } else if (candidate.finishReason === 'RECITATION') {
throw new Error('Vertex AI blocked the response due to recitation concerns.'); throw new Error('Vertex AI blocked the response due to recitation concerns.');
} else { } else {
@ -268,10 +254,9 @@ 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 // Log length (no trimming - preserve complete AI-generated content)
// 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.warn(`[AI Service] ⚠️ AI exceeded suggested limit (${remarkText.length} > ${maxLength}). Content preserved to avoid incomplete information.`);
} }
// Extract key points (look for bullet points or numbered items) // Extract key points (look for bullet points or numbered items)
@ -351,9 +336,8 @@ class AIService {
.map((wn: any) => `- ${wn.userName}: "${wn.message.substring(0, 150)}${wn.message.length > 150 ? '...' : ''}"`) .map((wn: any) => `- ${wn.userName}: "${wn.message.substring(0, 150)}${wn.message.length > 150 ? '...' : ''}"`)
.join('\n'); .join('\n');
// Summarize documents (limit to reduce token usage) // Summarize documents
const documentSummary = documents const documentSummary = documents
.slice(0, 10) // Limit to first 10 documents
.map((d: any) => `- ${d.fileName} (by ${d.uploadedBy})`) .map((d: any) => `- ${d.fileName} (by ${d.uploadedBy})`)
.join('\n'); .join('\n');
@ -398,17 +382,15 @@ ${isRejected
- Sounds natural and human-written (not AI-generated)`} - Sounds natural and human-written (not AI-generated)`}
**CRITICAL CHARACTER LIMIT - STRICT REQUIREMENT:** **CRITICAL CHARACTER LIMIT - STRICT REQUIREMENT:**
- Your response MUST stay within ${maxLength} characters (not words, CHARACTERS including spaces including HTML tags) - Your response MUST be EXACTLY within ${maxLength} characters (not words, CHARACTERS including spaces)
- This is a HARD LIMIT - you must count your characters and ensure your complete response fits within ${maxLength} characters - Count your characters carefully before responding
- Count your characters carefully before responding - include all HTML tags in your count
- If you have too much content, PRIORITIZE the most important information: - If you have too much content, PRIORITIZE the most important information:
1. Final decision (approved/rejected) 1. Final decision (approved/rejected)
2. Key approvers and their decisions 2. Key approvers and their decisions
3. Critical TAT breaches (if any) 3. Critical TAT breaches (if any)
4. Brief summary of the request 4. Brief summary of the request
- OMIT less important details to fit within the limit rather than exceeding it - OMIT less important details to fit within the limit rather than exceeding it
- Better to be concise and complete within the limit than to exceed it - Better to be concise than to exceed the limit
- IMPORTANT: Generate your complete response within this limit - do not generate partial content that exceeds the limit
**WRITING GUIDELINES:** **WRITING GUIDELINES:**
- Be concise and direct - every word must add value - Be concise and direct - every word must add value

View File

@ -100,18 +100,6 @@ export class EmailService {
await this.initialize(); await this.initialize();
} }
// If using test account, check if SMTP credentials are now available and re-initialize
if (this.useTestAccount) {
const smtpHost = process.env.SMTP_HOST;
const smtpUser = process.env.SMTP_USER;
const smtpPassword = process.env.SMTP_PASSWORD;
if (smtpHost && smtpUser && smtpPassword) {
logger.info('📧 SMTP credentials detected - re-initializing email service with production SMTP');
await this.initialize();
}
}
const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to; const recipients = Array.isArray(options.to) ? options.to.join(', ') : options.to;
const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>'; const fromAddress = process.env.EMAIL_FROM || 'RE Flow <noreply@royalenfield.com>';
@ -245,8 +233,6 @@ export class EmailService {
export const emailService = new EmailService(); export const emailService = new EmailService();
// Initialize on import (will use test account if SMTP not configured) // Initialize on import (will use test account if SMTP not configured)
// Note: If secrets are loaded later, the service will re-initialize automatically
// when sendEmail is called (if SMTP credentials become available)
emailService.initialize().catch(error => { emailService.initialize().catch(error => {
logger.error('Failed to initialize email service:', error); logger.error('Failed to initialize email service:', error);
}); });

View File

@ -101,7 +101,7 @@ export class EmailNotificationService {
}; };
const html = getRequestCreatedEmail(data); const html = getRequestCreatedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Created Successfully`; const subject = `[${requestData.requestNumber}] Request Created Successfully`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: initiatorData.email, to: initiatorData.email,
@ -170,7 +170,7 @@ export class EmailNotificationService {
}; };
const html = getMultiApproverRequestEmail(data); const html = getMultiApproverRequestEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Multi-Level Approval Request - Your Turn`; const subject = `[${requestData.requestNumber}] Multi-Level Approval Request - Your Turn`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: approverData.email, to: approverData.email,
@ -198,7 +198,7 @@ export class EmailNotificationService {
}; };
const html = getApprovalRequestEmail(data); const html = getApprovalRequestEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Approval Request - Action Required`; const subject = `[${requestData.requestNumber}] Approval Request - Action Required`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: approverData.email, to: approverData.email,
@ -252,7 +252,7 @@ export class EmailNotificationService {
}; };
const html = getApprovalConfirmationEmail(data); const html = getApprovalConfirmationEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Approved${isFinalApproval ? ' - All Approvals Complete' : ''}`; const subject = `[${requestData.requestNumber}] Request Approved${isFinalApproval ? ' - All Approvals Complete' : ''}`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: initiatorData.email, to: initiatorData.email,
@ -303,7 +303,7 @@ export class EmailNotificationService {
}; };
const html = getRejectionNotificationEmail(data); const html = getRejectionNotificationEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Rejected`; const subject = `[${requestData.requestNumber}] Request Rejected`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: initiatorData.email, to: initiatorData.email,
@ -379,7 +379,7 @@ export class EmailNotificationService {
}; };
const html = getTATReminderEmail(data); const html = getTATReminderEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - TAT Reminder - ${tatInfo.thresholdPercentage}% Elapsed`; const subject = `[${requestData.requestNumber}] TAT Reminder - ${tatInfo.thresholdPercentage}% Elapsed`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: approverData.email, to: approverData.email,
@ -449,7 +449,7 @@ export class EmailNotificationService {
}; };
const html = getTATBreachedEmail(data); const html = getTATBreachedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - TAT BREACHED - Immediate Action Required`; const subject = `[${requestData.requestNumber}] TAT BREACHED - Immediate Action Required`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: approverData.email, to: approverData.email,
@ -518,7 +518,7 @@ export class EmailNotificationService {
}; };
const html = getWorkflowResumedEmail(data); const html = getWorkflowResumedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed - Action Required`; const subject = `[${requestData.requestNumber}] Workflow Resumed - Action Required`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: approverData.email, to: approverData.email,
@ -587,7 +587,7 @@ export class EmailNotificationService {
}; };
const html = getWorkflowResumedEmail(data); const html = getWorkflowResumedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed`; const subject = `[${requestData.requestNumber}] Workflow Resumed`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: initiatorData.email, to: initiatorData.email,
@ -665,7 +665,7 @@ export class EmailNotificationService {
}; };
const html = getRequestClosedEmail(data); const html = getRequestClosedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Closed`; const subject = `[${requestData.requestNumber}] Request Closed`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: recipientData.email, to: recipientData.email,
@ -734,7 +734,7 @@ export class EmailNotificationService {
}; };
const html = getApproverSkippedEmail(data); const html = getApproverSkippedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Approver Skipped`; const subject = `[${requestData.requestNumber}] Approver Skipped`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: skippedApproverData.email, to: skippedApproverData.email,
@ -794,7 +794,7 @@ export class EmailNotificationService {
}; };
const html = getWorkflowPausedEmail(data); const html = getWorkflowPausedEmail(data);
const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Paused`; const subject = `[${requestData.requestNumber}] Workflow Paused`;
const result = await emailService.sendEmail({ const result = await emailService.sendEmail({
to: recipientData.email, to: recipientData.email,

View File

@ -1831,16 +1831,14 @@ 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: { status: { [Op.in]: [
[Op.in]: [
ApprovalStatus.PENDING as any, ApprovalStatus.PENDING as any,
(ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS', (ApprovalStatus as any).IN_PROGRESS ?? 'IN_PROGRESS',
ApprovalStatus.PAUSED as any, ApprovalStatus.PAUSED as any,
'PENDING', 'PENDING',
'IN_PROGRESS', 'IN_PROGRESS',
'PAUSED' 'PAUSED'
] as any ] as any },
},
}, },
order: [['requestId', 'ASC'], ['levelNumber', 'ASC']], order: [['requestId', 'ASC'], ['levelNumber', 'ASC']],
attributes: ['requestId', 'levelNumber', 'approverId'], attributes: ['requestId', 'levelNumber', 'approverId'],
@ -1909,8 +1907,7 @@ export class WorkflowService {
baseConditions.push({ baseConditions.push({
[Op.or]: [ [Op.or]: [
{ {
status: { status: { [Op.in]: [
[Op.in]: [
WorkflowStatus.PENDING as any, WorkflowStatus.PENDING as any,
WorkflowStatus.APPROVED as any, WorkflowStatus.APPROVED as any,
WorkflowStatus.PAUSED as any, WorkflowStatus.PAUSED as any,
@ -1918,8 +1915,7 @@ export class WorkflowService {
'IN_PROGRESS', // Legacy support - will be migrated to PENDING 'IN_PROGRESS', // Legacy support - will be migrated to PENDING
'APPROVED', 'APPROVED',
'PAUSED' 'PAUSED'
] as any ] as any }
}
}, },
// Also include requests with isPaused = true (even if status is PENDING) // Also include requests with isPaused = true (even if status is PENDING)
{ {
@ -1946,12 +1942,10 @@ export class WorkflowService {
baseConditions.push({ baseConditions.push({
[Op.and]: [ [Op.and]: [
{ status: statusUpper }, { status: statusUpper },
{ { [Op.or]: [
[Op.or]: [
{ isPaused: { [Op.is]: null } }, { isPaused: { [Op.is]: null } },
{ isPaused: false } { isPaused: false }
] ]}
}
] ]
}); });
} }
@ -2073,14 +2067,12 @@ export class WorkflowService {
const levelRows = await ApprovalLevel.findAll({ const levelRows = await ApprovalLevel.findAll({
where: { where: {
approverId: userId, approverId: userId,
status: { status: { [Op.in]: [
[Op.in]: [
ApprovalStatus.APPROVED as any, ApprovalStatus.APPROVED as any,
(ApprovalStatus as any).REJECTED ?? 'REJECTED', (ApprovalStatus as any).REJECTED ?? 'REJECTED',
'APPROVED', 'APPROVED',
'REJECTED' 'REJECTED'
] as any ] as any },
},
}, },
attributes: ['requestId'], attributes: ['requestId'],
}); });
@ -2386,6 +2378,40 @@ export class WorkflowService {
userAgent: requestMetadata?.userAgent || undefined userAgent: requestMetadata?.userAgent || undefined
}); });
// Send notification to INITIATOR confirming submission
await notificationService.sendToUsers([initiatorId], {
title: 'Request Submitted Successfully',
body: `Your request "${workflowData.title}" has been submitted and is now with the first approver.`,
requestNumber: requestNumber,
requestId: (workflow as any).requestId,
url: `/request/${requestNumber}`,
type: 'request_submitted',
priority: 'MEDIUM'
});
// Send notification to FIRST APPROVER for assignment
const firstLevel = await ApprovalLevel.findOne({ where: { requestId: (workflow as any).requestId, levelNumber: 1 } });
if (firstLevel) {
await notificationService.sendToUsers([(firstLevel as any).approverId], {
title: 'New Request Assigned',
body: `${workflowData.title}`,
requestNumber: requestNumber,
requestId: (workflow as any).requestId,
url: `/request/${requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
});
activityService.log({
requestId: (workflow as any).requestId,
type: 'assignment',
user: { userId: initiatorId, name: initiatorName },
timestamp: new Date().toISOString(),
action: 'Assigned to approver',
details: `Request assigned to ${(firstLevel as any).approverName || (firstLevel as any).approverEmail || 'approver'} for review`
});
}
return workflow; return workflow;
} catch (error) { } catch (error) {
@ -3166,28 +3192,13 @@ export class WorkflowService {
// Don't fail the submission if TAT scheduling fails // Don't fail the submission if TAT scheduling fails
} }
// Send "Request Submitted" notification to INITIATOR // NOTE: Notifications are already sent in createWorkflow() when the workflow is created
await notificationService.sendToUsers([initiatorId], { // We should NOT send "Request submitted" to the approver here - that's incorrect
title: 'Request Submitted Successfully', // The approver should only receive "New Request Assigned" notification (sent in createWorkflow)
body: `Your request "${workflowTitle}" has been submitted and is now with the first approver.`, // The initiator receives "Request Submitted Successfully" notification (sent in createWorkflow)
requestNumber: (updated as any).requestNumber, //
requestId: (updated as any).requestId, // If this is a draft being submitted, notifications were already sent during creation,
url: `/request/${(updated as any).requestNumber}`, // so we don't need to send them again here to avoid duplicates
type: 'request_submitted',
priority: 'MEDIUM'
});
// Send "New Request Assigned" notification to FIRST APPROVER
await notificationService.sendToUsers([(current as any).approverId], {
title: 'New Request Assigned',
body: `${workflowTitle}`,
requestNumber: (updated as any).requestNumber,
requestId: (updated as any).requestId,
url: `/request/${(updated as any).requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
});
} }
return updated; return updated;
} catch (error) { } catch (error) {