From be220bbb0ca7b8c09cb10548aaec119d7b5fce91 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Thu, 22 Jan 2026 19:21:26 +0530 Subject: [PATCH] add the title in email subject and statd implementing admin templates --- .../workflowTemplate.controller.ts | 56 +++++++++++ .../20260122-create-workflow-templates.ts | 84 ++++++++++++++++ src/models/WorkflowTemplate.ts | 99 +++++++++++++++++++ src/models/index.ts | 4 +- src/routes/index.ts | 2 + src/routes/workflowTemplate.routes.ts | 14 +++ src/scripts/auto-setup.ts | 42 ++++---- src/services/emailNotification.service.ts | 50 +++++----- 8 files changed, 305 insertions(+), 46 deletions(-) create mode 100644 src/controllers/workflowTemplate.controller.ts create mode 100644 src/migrations/20260122-create-workflow-templates.ts create mode 100644 src/models/WorkflowTemplate.ts create mode 100644 src/routes/workflowTemplate.routes.ts diff --git a/src/controllers/workflowTemplate.controller.ts b/src/controllers/workflowTemplate.controller.ts new file mode 100644 index 0000000..8fb7d05 --- /dev/null +++ b/src/controllers/workflowTemplate.controller.ts @@ -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' + }); + } +}; diff --git a/src/migrations/20260122-create-workflow-templates.ts b/src/migrations/20260122-create-workflow-templates.ts new file mode 100644 index 0000000..77e623d --- /dev/null +++ b/src/migrations/20260122-create-workflow-templates.ts @@ -0,0 +1,84 @@ +import { QueryInterface, DataTypes } from 'sequelize'; + +export async function up(queryInterface: QueryInterface): Promise { + // 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 { + await queryInterface.dropTable('workflow_templates'); + await queryInterface.sequelize.query('DROP TYPE IF EXISTS enum_template_priority;'); +} diff --git a/src/models/WorkflowTemplate.ts b/src/models/WorkflowTemplate.ts new file mode 100644 index 0000000..24773ee --- /dev/null +++ b/src/models/WorkflowTemplate.ts @@ -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' + } +); diff --git a/src/models/index.ts b/src/models/index.ts index 9e70840..c405808 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -16,6 +16,7 @@ import { Notification } from './Notification'; import ConclusionRemark from './ConclusionRemark'; import RequestSummary from './RequestSummary'; import SharedSummary from './SharedSummary'; +import { WorkflowTemplate } from './WorkflowTemplate'; // Define associations const defineAssociations = () => { @@ -138,7 +139,8 @@ export { Notification, ConclusionRemark, RequestSummary, - SharedSummary + SharedSummary, + WorkflowTemplate }; // Export default sequelize instance diff --git a/src/routes/index.ts b/src/routes/index.ts index aeca3e9..a90652f 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -13,6 +13,7 @@ import dashboardRoutes from './dashboard.routes'; import notificationRoutes from './notification.routes'; import conclusionRoutes from './conclusion.routes'; import aiRoutes from './ai.routes'; +import workflowTemplateRoutes from './workflowTemplate.routes'; const router = Router(); @@ -40,6 +41,7 @@ router.use('/notifications', notificationRoutes); router.use('/conclusions', conclusionRoutes); router.use('/ai', aiRoutes); router.use('/summaries', summaryRoutes); +router.use('/templates', workflowTemplateRoutes); // TODO: Add other route modules as they are implemented // router.use('/approvals', approvalRoutes); diff --git a/src/routes/workflowTemplate.routes.ts b/src/routes/workflowTemplate.routes.ts new file mode 100644 index 0000000..3ba2875 --- /dev/null +++ b/src/routes/workflowTemplate.routes.ts @@ -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; diff --git a/src/scripts/auto-setup.ts b/src/scripts/auto-setup.ts index 010253a..7a64778 100644 --- a/src/scripts/auto-setup.ts +++ b/src/scripts/auto-setup.ts @@ -49,13 +49,13 @@ async function checkAndCreateDatabase(): Promise { if (result.rows.length === 0) { console.log(`📦 Database '${DB_NAME}' not found. Creating...`); - + // Create database await client.query(`CREATE DATABASE "${DB_NAME}"`); console.log(`✅ Database '${DB_NAME}' created successfully!`); - + await client.end(); - + // Connect to new database and install extensions const newDbClient = new Client({ host: DB_HOST, @@ -64,13 +64,13 @@ async function checkAndCreateDatabase(): Promise { password: DB_PASSWORD, database: DB_NAME, }); - + await newDbClient.connect(); console.log('📦 Installing uuid-ossp extension...'); await newDbClient.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); console.log('✅ Extension installed!'); await newDbClient.end(); - + return true; // Database was created } else { console.log(`✅ Database '${DB_NAME}' already exists.`); @@ -87,7 +87,7 @@ async function checkAndCreateDatabase(): Promise { async function runMigrations(): Promise { try { console.log('🔄 Checking and running pending migrations...'); - + // Import all migrations using require for CommonJS compatibility // Some migrations use module.exports, others use export const m0 = require('../migrations/2025103000-create-users'); @@ -120,7 +120,8 @@ async function runMigrations(): Promise { const m27 = require('../migrations/20250127-migrate-in-progress-to-pending'); const m28 = require('../migrations/20250130-migrate-to-vertex-ai'); const m29 = require('../migrations/20251203-add-user-notification-preferences'); - + const m30 = require('../migrations/20260122-create-workflow-templates'); + const migrations = [ { name: '2025103000-create-users', module: m0 }, { name: '2025103001-create-workflow-requests', module: m1 }, @@ -152,10 +153,11 @@ async function runMigrations(): Promise { { name: '20250127-migrate-in-progress-to-pending', module: m27 }, { name: '20250130-migrate-to-vertex-ai', module: m28 }, { name: '20251203-add-user-notification-preferences', module: m29 }, + { name: '20260122-create-workflow-templates', module: m30 }, ]; - + const queryInterface = sequelize.getQueryInterface(); - + // Ensure migrations tracking table exists const tables = await queryInterface.showAllTables(); if (!tables.includes('migrations')) { @@ -167,34 +169,34 @@ async function runMigrations(): Promise { ) `); } - + // Get already executed migrations const executedResults = await sequelize.query<{ name: string }>( 'SELECT name FROM migrations ORDER BY id', { type: QueryTypes.SELECT } ); const executedMigrations = executedResults.map(r => r.name); - + // Find pending migrations const pendingMigrations = migrations.filter( m => !executedMigrations.includes(m.name) ); - + if (pendingMigrations.length === 0) { console.log('✅ Migrations up-to-date'); return; } - + console.log(`🔄 Running ${pendingMigrations.length} pending migration(s)...`); - + // Run each pending migration for (const migration of pendingMigrations) { try { console.log(` → ${migration.name}`); - + // Call the up function - works for both module.exports and export styles await migration.module.up(queryInterface); - + // Mark as executed await sequelize.query( 'INSERT INTO migrations (name) VALUES (:name) ON CONFLICT (name) DO NOTHING', @@ -209,7 +211,7 @@ async function runMigrations(): Promise { throw error; } } - + console.log(`✅ Applied ${pendingMigrations.length} migration(s)`); } catch (error: any) { console.error('❌ Migration failed:', error.message); @@ -246,9 +248,9 @@ async function autoSetup(): Promise { console.log('\n========================================'); console.log('✅ Setup completed successfully!'); console.log('========================================\n'); - + console.log('📝 Note: Admin configurations will be auto-seeded on server start if table is empty.\n'); - + if (wasCreated) { console.log('💡 Next steps:'); console.log(' 1. Server will start automatically'); @@ -256,7 +258,7 @@ async function autoSetup(): Promise { console.log(' 3. Run this SQL to make yourself admin:'); console.log(` UPDATE users SET role = 'ADMIN' WHERE email = 'your-email@royalenfield.com';\n`); } - + } catch (error: any) { console.error('\n========================================'); console.error('❌ Setup failed!'); diff --git a/src/services/emailNotification.service.ts b/src/services/emailNotification.service.ts index 3b5045b..f8b7e8e 100644 --- a/src/services/emailNotification.service.ts +++ b/src/services/emailNotification.service.ts @@ -101,7 +101,7 @@ export class EmailNotificationService { }; 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({ to: initiatorData.email, @@ -144,9 +144,9 @@ export class EmailNotificationService { // Multi-level approval email const chainData: ApprovalChainItem[] = approvalChain.map((level: any) => ({ name: level.approverName || level.approverEmail, - status: level.status === 'APPROVED' ? 'approved' - : level.levelNumber === approverData.levelNumber ? 'current' - : level.levelNumber < approverData.levelNumber ? 'pending' + status: level.status === 'APPROVED' ? 'approved' + : level.levelNumber === approverData.levelNumber ? 'current' + : level.levelNumber < approverData.levelNumber ? 'pending' : 'awaiting', date: level.approvedAt ? this.formatDate(level.approvedAt) : undefined, levelNumber: level.levelNumber @@ -170,7 +170,7 @@ export class EmailNotificationService { }; 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({ to: approverData.email, @@ -198,7 +198,7 @@ export class EmailNotificationService { }; 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({ to: approverData.email, @@ -252,7 +252,7 @@ export class EmailNotificationService { }; 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({ to: initiatorData.email, @@ -303,7 +303,7 @@ export class EmailNotificationService { }; const html = getRejectionNotificationEmail(data); - const subject = `[${requestData.requestNumber}] Request Rejected`; + const subject = `${requestData.requestNumber} - ${requestData.title} - Request Rejected`; const result = await emailService.sendEmail({ to: initiatorData.email, @@ -344,9 +344,9 @@ export class EmailNotificationService { } // Determine urgency level based on threshold - const urgencyLevel = tatInfo.thresholdPercentage >= 75 ? 'high' - : tatInfo.thresholdPercentage >= 50 ? 'medium' - : 'low'; + const urgencyLevel = tatInfo.thresholdPercentage >= 75 ? 'high' + : tatInfo.thresholdPercentage >= 50 ? 'medium' + : 'low'; // Get initiator name - try from requestData first, then fetch if needed let initiatorName = requestData.initiatorName || requestData.initiator?.displayName || 'Initiator'; @@ -379,7 +379,7 @@ export class EmailNotificationService { }; 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({ to: approverData.email, @@ -449,7 +449,7 @@ export class EmailNotificationService { }; 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({ to: approverData.email, @@ -496,8 +496,8 @@ export class EmailNotificationService { } const isAutoResumed = !resumedByData || resumedByData.userId === 'system'; - const resumedByText = isAutoResumed - ? 'automatically' + const resumedByText = isAutoResumed + ? 'automatically' : `by ${resumedByData.displayName || resumedByData.email}`; const data: WorkflowResumedData = { @@ -509,7 +509,7 @@ export class EmailNotificationService { resumedTime: this.formatTime(new Date()), pausedDuration: pauseDuration, currentApprover: approverData.displayName || approverData.email, - newTATDeadline: requestData.tatDeadline + newTATDeadline: requestData.tatDeadline ? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline) : 'To be determined', isApprover: true, @@ -518,7 +518,7 @@ export class EmailNotificationService { }; 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({ to: approverData.email, @@ -565,8 +565,8 @@ export class EmailNotificationService { } const isAutoResumed = !resumedByData || resumedByData.userId === 'system' || !resumedByData.userId; - const resumedByText = isAutoResumed - ? 'automatically' + const resumedByText = isAutoResumed + ? 'automatically' : `by ${resumedByData.displayName || resumedByData.email || resumedByData.name || 'User'}`; const data: WorkflowResumedData = { @@ -578,7 +578,7 @@ export class EmailNotificationService { resumedTime: this.formatTime(new Date()), pausedDuration: pauseDuration, currentApprover: approverData?.displayName || approverData?.email || 'Current Approver', - newTATDeadline: requestData.tatDeadline + newTATDeadline: requestData.tatDeadline ? this.formatDate(requestData.tatDeadline) + ' ' + this.formatTime(requestData.tatDeadline) : 'To be determined', isApprover: false, // This is for initiator @@ -587,7 +587,7 @@ export class EmailNotificationService { }; const html = getWorkflowResumedEmail(data); - const subject = `[${requestData.requestNumber}] Workflow Resumed`; + const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Resumed`; const result = await emailService.sendEmail({ to: initiatorData.email, @@ -665,7 +665,7 @@ export class EmailNotificationService { }; const html = getRequestClosedEmail(data); - const subject = `[${requestData.requestNumber}] Request Closed`; + const subject = `${requestData.requestNumber} - ${requestData.title} - Request Closed`; const result = await emailService.sendEmail({ to: recipientData.email, @@ -690,7 +690,7 @@ export class EmailNotificationService { closureData: any ): Promise { logger.info(`📧 Sending Request Closed emails to ${participants.length} participants`); - + for (const participant of participants) { await this.sendRequestClosed(requestData, participant, closureData); // Small delay to avoid rate limiting @@ -734,7 +734,7 @@ export class EmailNotificationService { }; const html = getApproverSkippedEmail(data); - const subject = `[${requestData.requestNumber}] Approver Skipped`; + const subject = `${requestData.requestNumber} - ${requestData.title} - Approver Skipped`; const result = await emailService.sendEmail({ to: skippedApproverData.email, @@ -794,7 +794,7 @@ export class EmailNotificationService { }; const html = getWorkflowPausedEmail(data); - const subject = `[${requestData.requestNumber}] Workflow Paused`; + const subject = `${requestData.requestNumber} - ${requestData.title} - Workflow Paused`; const result = await emailService.sendEmail({ to: recipientData.email,