add the title in email subject and statd implementing admin templates
This commit is contained in:
parent
d1ae0ffaec
commit
be220bbb0c
56
src/controllers/workflowTemplate.controller.ts
Normal file
56
src/controllers/workflowTemplate.controller.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
84
src/migrations/20260122-create-workflow-templates.ts
Normal file
84
src/migrations/20260122-create-workflow-templates.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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;');
|
||||||
|
}
|
||||||
99
src/models/WorkflowTemplate.ts
Normal file
99
src/models/WorkflowTemplate.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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,6 +16,7 @@ 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 = () => {
|
||||||
@ -138,7 +139,8 @@ export {
|
|||||||
Notification,
|
Notification,
|
||||||
ConclusionRemark,
|
ConclusionRemark,
|
||||||
RequestSummary,
|
RequestSummary,
|
||||||
SharedSummary
|
SharedSummary,
|
||||||
|
WorkflowTemplate
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export default sequelize instance
|
// Export default sequelize instance
|
||||||
|
|||||||
@ -13,6 +13,7 @@ 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();
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ 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);
|
||||||
|
|||||||
14
src/routes/workflowTemplate.routes.ts
Normal file
14
src/routes/workflowTemplate.routes.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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;
|
||||||
@ -49,13 +49,13 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
|
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
console.log(`📦 Database '${DB_NAME}' not found. Creating...`);
|
console.log(`📦 Database '${DB_NAME}' not found. Creating...`);
|
||||||
|
|
||||||
// Create database
|
// Create database
|
||||||
await client.query(`CREATE DATABASE "${DB_NAME}"`);
|
await client.query(`CREATE DATABASE "${DB_NAME}"`);
|
||||||
console.log(`✅ Database '${DB_NAME}' created successfully!`);
|
console.log(`✅ Database '${DB_NAME}' created successfully!`);
|
||||||
|
|
||||||
await client.end();
|
await client.end();
|
||||||
|
|
||||||
// Connect to new database and install extensions
|
// Connect to new database and install extensions
|
||||||
const newDbClient = new Client({
|
const newDbClient = new Client({
|
||||||
host: DB_HOST,
|
host: DB_HOST,
|
||||||
@ -64,13 +64,13 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
password: DB_PASSWORD,
|
password: DB_PASSWORD,
|
||||||
database: DB_NAME,
|
database: DB_NAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
await newDbClient.connect();
|
await newDbClient.connect();
|
||||||
console.log('📦 Installing uuid-ossp extension...');
|
console.log('📦 Installing uuid-ossp extension...');
|
||||||
await newDbClient.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
await newDbClient.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
||||||
console.log('✅ Extension installed!');
|
console.log('✅ Extension installed!');
|
||||||
await newDbClient.end();
|
await newDbClient.end();
|
||||||
|
|
||||||
return true; // Database was created
|
return true; // Database was created
|
||||||
} else {
|
} else {
|
||||||
console.log(`✅ Database '${DB_NAME}' already exists.`);
|
console.log(`✅ Database '${DB_NAME}' already exists.`);
|
||||||
@ -87,7 +87,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
|
|||||||
async function runMigrations(): Promise<void> {
|
async function runMigrations(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Checking and running pending migrations...');
|
console.log('🔄 Checking and running pending migrations...');
|
||||||
|
|
||||||
// Import all migrations using require for CommonJS compatibility
|
// Import all migrations using require for CommonJS compatibility
|
||||||
// Some migrations use module.exports, others use export
|
// Some migrations use module.exports, others use export
|
||||||
const m0 = require('../migrations/2025103000-create-users');
|
const m0 = require('../migrations/2025103000-create-users');
|
||||||
@ -120,7 +120,8 @@ 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 },
|
||||||
{ name: '2025103001-create-workflow-requests', module: m1 },
|
{ name: '2025103001-create-workflow-requests', module: m1 },
|
||||||
@ -152,10 +153,11 @@ 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();
|
||||||
|
|
||||||
// Ensure migrations tracking table exists
|
// Ensure migrations tracking table exists
|
||||||
const tables = await queryInterface.showAllTables();
|
const tables = await queryInterface.showAllTables();
|
||||||
if (!tables.includes('migrations')) {
|
if (!tables.includes('migrations')) {
|
||||||
@ -167,34 +169,34 @@ async function runMigrations(): Promise<void> {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get already executed migrations
|
// Get already executed migrations
|
||||||
const executedResults = await sequelize.query<{ name: string }>(
|
const executedResults = await sequelize.query<{ name: string }>(
|
||||||
'SELECT name FROM migrations ORDER BY id',
|
'SELECT name FROM migrations ORDER BY id',
|
||||||
{ type: QueryTypes.SELECT }
|
{ type: QueryTypes.SELECT }
|
||||||
);
|
);
|
||||||
const executedMigrations = executedResults.map(r => r.name);
|
const executedMigrations = executedResults.map(r => r.name);
|
||||||
|
|
||||||
// Find pending migrations
|
// Find pending migrations
|
||||||
const pendingMigrations = migrations.filter(
|
const pendingMigrations = migrations.filter(
|
||||||
m => !executedMigrations.includes(m.name)
|
m => !executedMigrations.includes(m.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pendingMigrations.length === 0) {
|
if (pendingMigrations.length === 0) {
|
||||||
console.log('✅ Migrations up-to-date');
|
console.log('✅ Migrations up-to-date');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔄 Running ${pendingMigrations.length} pending migration(s)...`);
|
console.log(`🔄 Running ${pendingMigrations.length} pending migration(s)...`);
|
||||||
|
|
||||||
// Run each pending migration
|
// Run each pending migration
|
||||||
for (const migration of pendingMigrations) {
|
for (const migration of pendingMigrations) {
|
||||||
try {
|
try {
|
||||||
console.log(` → ${migration.name}`);
|
console.log(` → ${migration.name}`);
|
||||||
|
|
||||||
// Call the up function - works for both module.exports and export styles
|
// Call the up function - works for both module.exports and export styles
|
||||||
await migration.module.up(queryInterface);
|
await migration.module.up(queryInterface);
|
||||||
|
|
||||||
// Mark as executed
|
// Mark as executed
|
||||||
await sequelize.query(
|
await sequelize.query(
|
||||||
'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING',
|
'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING',
|
||||||
@ -209,7 +211,7 @@ async function runMigrations(): Promise<void> {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ Applied ${pendingMigrations.length} migration(s)`);
|
console.log(`✅ Applied ${pendingMigrations.length} migration(s)`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Migration failed:', error.message);
|
console.error('❌ Migration failed:', error.message);
|
||||||
@ -246,9 +248,9 @@ async function autoSetup(): Promise<void> {
|
|||||||
console.log('\n========================================');
|
console.log('\n========================================');
|
||||||
console.log('✅ Setup completed successfully!');
|
console.log('✅ Setup completed successfully!');
|
||||||
console.log('========================================\n');
|
console.log('========================================\n');
|
||||||
|
|
||||||
console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.\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');
|
||||||
@ -256,7 +258,7 @@ async function autoSetup(): Promise<void> {
|
|||||||
console.log(' 3. Run this SQL to make yourself admin:');
|
console.log(' 3. Run this SQL to make yourself admin:');
|
||||||
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`);
|
console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('\n========================================');
|
console.error('\n========================================');
|
||||||
console.error('❌ Setup failed!');
|
console.error('❌ Setup failed!');
|
||||||
|
|||||||
@ -101,7 +101,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,
|
||||||
@ -144,9 +144,9 @@ export class EmailNotificationService {
|
|||||||
// Multi-level approval email
|
// Multi-level approval email
|
||||||
const chainData: ApprovalChainItem[] = approvalChain.map((level: any) => ({
|
const chainData: ApprovalChainItem[] = approvalChain.map((level: any) => ({
|
||||||
name: level.approverName || level.approverEmail,
|
name: level.approverName || level.approverEmail,
|
||||||
status: level.status === 'APPROVED' ? 'approved'
|
status: level.status === 'APPROVED' ? 'approved'
|
||||||
: level.levelNumber === approverData.levelNumber ? 'current'
|
: level.levelNumber === approverData.levelNumber ? 'current'
|
||||||
: level.levelNumber < approverData.levelNumber ? 'pending'
|
: level.levelNumber < approverData.levelNumber ? 'pending'
|
||||||
: 'awaiting',
|
: 'awaiting',
|
||||||
date: level.approvedAt ? this.formatDate(level.approvedAt) : undefined,
|
date: level.approvedAt ? this.formatDate(level.approvedAt) : undefined,
|
||||||
levelNumber: level.levelNumber
|
levelNumber: level.levelNumber
|
||||||
@ -170,7 +170,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,
|
||||||
@ -198,7 +198,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,
|
||||||
@ -252,7 +252,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,
|
||||||
@ -303,7 +303,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,
|
||||||
@ -344,9 +344,9 @@ export class EmailNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine urgency level based on threshold
|
// Determine urgency level based on threshold
|
||||||
const urgencyLevel = tatInfo.thresholdPercentage >= 75 ? 'high'
|
const urgencyLevel = tatInfo.thresholdPercentage >= 75 ? 'high'
|
||||||
: tatInfo.thresholdPercentage >= 50 ? 'medium'
|
: tatInfo.thresholdPercentage >= 50 ? 'medium'
|
||||||
: 'low';
|
: 'low';
|
||||||
|
|
||||||
// Get initiator name - try from requestData first, then fetch if needed
|
// Get initiator name - try from requestData first, then fetch if needed
|
||||||
let initiatorName = requestData.initiatorName || requestData.initiator?.displayName || 'Initiator';
|
let initiatorName = requestData.initiatorName || requestData.initiator?.displayName || 'Initiator';
|
||||||
@ -379,7 +379,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,
|
||||||
@ -449,7 +449,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,
|
||||||
@ -496,8 +496,8 @@ export class EmailNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAutoResumed = !resumedByData || resumedByData.userId === 'system';
|
const isAutoResumed = !resumedByData || resumedByData.userId === 'system';
|
||||||
const resumedByText = isAutoResumed
|
const resumedByText = isAutoResumed
|
||||||
? 'automatically'
|
? 'automatically'
|
||||||
: `by ${resumedByData.displayName || resumedByData.email}`;
|
: `by ${resumedByData.displayName || resumedByData.email}`;
|
||||||
|
|
||||||
const data: WorkflowResumedData = {
|
const data: WorkflowResumedData = {
|
||||||
@ -509,7 +509,7 @@ export class EmailNotificationService {
|
|||||||
resumedTime: this.formatTime(new Date()),
|
resumedTime: this.formatTime(new Date()),
|
||||||
pausedDuration: pauseDuration,
|
pausedDuration: pauseDuration,
|
||||||
currentApprover: approverData.displayName || approverData.email,
|
currentApprover: approverData.displayName || approverData.email,
|
||||||
newTATDeadline: requestData.tatDeadline
|
newTATDeadline: requestData.tatDeadline
|
||||||
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
||||||
: 'To be determined',
|
: 'To be determined',
|
||||||
isApprover: true,
|
isApprover: true,
|
||||||
@ -518,7 +518,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,
|
||||||
@ -565,8 +565,8 @@ export class EmailNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAutoResumed = !resumedByData || resumedByData.userId === 'system' || !resumedByData.userId;
|
const isAutoResumed = !resumedByData || resumedByData.userId === 'system' || !resumedByData.userId;
|
||||||
const resumedByText = isAutoResumed
|
const resumedByText = isAutoResumed
|
||||||
? 'automatically'
|
? 'automatically'
|
||||||
: `by ${resumedByData.displayName || resumedByData.email || resumedByData.name || 'User'}`;
|
: `by ${resumedByData.displayName || resumedByData.email || resumedByData.name || 'User'}`;
|
||||||
|
|
||||||
const data: WorkflowResumedData = {
|
const data: WorkflowResumedData = {
|
||||||
@ -578,7 +578,7 @@ export class EmailNotificationService {
|
|||||||
resumedTime: this.formatTime(new Date()),
|
resumedTime: this.formatTime(new Date()),
|
||||||
pausedDuration: pauseDuration,
|
pausedDuration: pauseDuration,
|
||||||
currentApprover: approverData?.displayName || approverData?.email || 'Current Approver',
|
currentApprover: approverData?.displayName || approverData?.email || 'Current Approver',
|
||||||
newTATDeadline: requestData.tatDeadline
|
newTATDeadline: requestData.tatDeadline
|
||||||
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline)
|
||||||
: 'To be determined',
|
: 'To be determined',
|
||||||
isApprover: false, // This is for initiator
|
isApprover: false, // This is for initiator
|
||||||
@ -587,7 +587,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,
|
||||||
@ -665,7 +665,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,
|
||||||
@ -690,7 +690,7 @@ export class EmailNotificationService {
|
|||||||
closureData: any
|
closureData: any
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.info(`📧 Sending Request Closed emails to ${participants.length} participants`);
|
logger.info(`📧 Sending Request Closed emails to ${participants.length} participants`);
|
||||||
|
|
||||||
for (const participant of participants) {
|
for (const participant of participants) {
|
||||||
await this.sendRequestClosed(requestData, participant, closureData);
|
await this.sendRequestClosed(requestData, participant, closureData);
|
||||||
// Small delay to avoid rate limiting
|
// Small delay to avoid rate limiting
|
||||||
@ -734,7 +734,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,
|
||||||
@ -794,7 +794,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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user