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;
|
||||||
@ -120,6 +120,7 @@ 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 },
|
||||||
@ -152,6 +153,7 @@ 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();
|
||||||
|
|||||||
@ -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,
|
||||||
@ -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,
|
||||||
@ -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,
|
||||||
@ -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,
|
||||||
@ -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,
|
||||||
@ -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