Compare commits
No commits in common. "be220bbb0ca7b8c09cb10548aaec119d7b5fce91" and "4cf72888579f6d9970c01f8cdc25eede43e5283f" have entirely different histories.
be220bbb0c
...
4cf7288857
@ -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
|
||||||
@ -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
1
build/assets/index-9cOIFSn9.js.map
Normal file
1
build/assets/index-9cOIFSn9.js.map
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-BmOYs32D.css
Normal file
1
build/assets/index-BmOYs32D.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -52,7 +52,7 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-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>
|
||||||
|
|||||||
@ -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'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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;');
|
|
||||||
}
|
|
||||||
@ -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'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
@ -2523,7 +2549,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;
|
||||||
|
|
||||||
@ -2619,7 +2645,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({
|
||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user