Re_Backend/src/controllers/conclusion.controller.ts

405 lines
14 KiB
TypeScript

import { Request, Response } from 'express';
import { WorkflowRequest, ApprovalLevel, WorkNote, Document, Activity, ConclusionRemark } from '@models/index';
import { aiService } from '@services/ai.service';
import { activityService } from '@services/activity.service';
import logger from '@utils/logger';
import { getRequestMetadata } from '@utils/requestUtils';
export class ConclusionController {
/**
* Generate AI conclusion remark for a request
* POST /api/v1/conclusions/:requestId/generate
*/
async generateConclusion(req: Request, res: Response) {
try {
const { requestId } = req.params;
const userId = (req as any).user?.userId;
// Fetch request with all related data
const request = await WorkflowRequest.findOne({
where: { requestId },
include: [
{ association: 'initiator', attributes: ['userId', 'displayName', 'email'] }
]
});
if (!request) {
return res.status(404).json({ error: 'Request not found' });
}
// Check if user is the initiator
if ((request as any).initiatorId !== userId) {
return res.status(403).json({ error: 'Only the initiator can generate conclusion remarks' });
}
// Check if request is approved
if ((request as any).status !== 'APPROVED') {
return res.status(400).json({ error: 'Conclusion can only be generated for approved requests' });
}
// Check if AI features are enabled in admin config
const { getConfigValue } = await import('../services/configReader.service');
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
if (!aiEnabled) {
logger.warn(`[Conclusion] AI features disabled in admin config for request ${requestId}`);
return res.status(400).json({
error: 'AI features disabled',
message: 'AI features are currently disabled by administrator. Please write the conclusion manually.',
canContinueManually: true
});
}
if (!remarkGenerationEnabled) {
logger.warn(`[Conclusion] AI remark generation disabled in admin config for request ${requestId}`);
return res.status(400).json({
error: 'AI remark generation disabled',
message: 'AI-powered conclusion generation is currently disabled by administrator. Please write the conclusion manually.',
canContinueManually: true
});
}
// Check if AI service is available
if (!aiService.isAvailable()) {
logger.warn(`[Conclusion] AI service unavailable for request ${requestId}`);
return res.status(503).json({
error: 'AI service not available',
message: 'AI features are currently unavailable. Please configure an AI provider (Claude, OpenAI, or Gemini) in the admin panel, or write the conclusion manually.',
canContinueManually: true
});
}
// Gather context for AI generation
const approvalLevels = await ApprovalLevel.findAll({
where: { requestId },
order: [['levelNumber', 'ASC']]
});
const workNotes = await WorkNote.findAll({
where: { requestId },
order: [['createdAt', 'ASC']],
limit: 20 // Last 20 work notes
});
const documents = await Document.findAll({
where: { requestId },
order: [['uploadedAt', 'DESC']]
});
const activities = await Activity.findAll({
where: { requestId },
order: [['createdAt', 'ASC']],
limit: 50 // Last 50 activities
});
// Build context object
const context = {
requestTitle: (request as any).title,
requestDescription: (request as any).description,
requestNumber: (request as any).requestNumber,
priority: (request as any).priority,
approvalFlow: approvalLevels.map((level: any) => ({
levelNumber: level.levelNumber,
approverName: level.approverName,
status: level.status,
comments: level.comments,
actionDate: level.actionDate,
tatHours: Number(level.tatHours || 0),
elapsedHours: Number(level.elapsedHours || 0)
})),
workNotes: workNotes.map((note: any) => ({
userName: note.userName,
message: note.message,
createdAt: note.createdAt
})),
documents: documents.map((doc: any) => ({
fileName: doc.originalFileName || doc.fileName,
uploadedBy: doc.uploadedBy,
uploadedAt: doc.uploadedAt
})),
activities: activities.map((activity: any) => ({
type: activity.activityType,
action: activity.activityDescription,
details: activity.activityDescription,
timestamp: activity.createdAt
}))
};
logger.info(`[Conclusion] Generating AI remark for request ${requestId}...`);
// Generate AI conclusion
const aiResult = await aiService.generateConclusionRemark(context);
// Check if conclusion already exists
let conclusionInstance = await ConclusionRemark.findOne({ where: { requestId } });
const conclusionData = {
aiGeneratedRemark: aiResult.remark,
aiModelUsed: aiResult.provider,
aiConfidenceScore: aiResult.confidence,
approvalSummary: {
totalLevels: approvalLevels.length,
approvedLevels: approvalLevels.filter((l: any) => l.status === 'APPROVED').length,
averageTatUsage: approvalLevels.reduce((sum: number, l: any) =>
sum + Number(l.tatPercentageUsed || 0), 0) / (approvalLevels.length || 1)
},
documentSummary: {
totalDocuments: documents.length,
documentNames: documents.map((d: any) => d.originalFileName || d.fileName)
},
keyDiscussionPoints: aiResult.keyPoints,
generatedAt: new Date()
};
if (conclusionInstance) {
// Update existing conclusion (allow regeneration)
await conclusionInstance.update(conclusionData as any);
logger.info(`[Conclusion] ✅ AI conclusion regenerated for request ${requestId}`);
} else {
// Create new conclusion
conclusionInstance = await ConclusionRemark.create({
requestId,
...conclusionData,
finalRemark: null,
editedBy: null,
isEdited: false,
editCount: 0,
finalizedAt: null
} as any);
logger.info(`[Conclusion] ✅ AI conclusion generated for request ${requestId}`);
}
// Log activity
const requestMeta = getRequestMetadata(req);
await activityService.log({
requestId,
type: 'ai_conclusion_generated',
user: { userId, name: (request as any).initiator?.displayName || 'Initiator' },
timestamp: new Date().toISOString(),
action: 'AI Conclusion Generated',
details: 'AI-powered conclusion remark generated for review',
ipAddress: requestMeta.ipAddress,
userAgent: requestMeta.userAgent
});
return res.status(200).json({
message: 'Conclusion generated successfully',
data: {
conclusionId: (conclusionInstance as any).conclusionId,
aiGeneratedRemark: aiResult.remark,
keyDiscussionPoints: aiResult.keyPoints,
confidence: aiResult.confidence,
provider: aiResult.provider,
generatedAt: new Date()
}
});
} catch (error: any) {
logger.error('[Conclusion] Error generating conclusion:', error);
// Provide helpful error messages
const isConfigError = error.message?.includes('not configured') ||
error.message?.includes('not available') ||
error.message?.includes('not initialized');
return res.status(isConfigError ? 503 : 500).json({
error: isConfigError ? 'AI service not configured' : 'Failed to generate conclusion',
message: error.message || 'An unexpected error occurred',
canContinueManually: true // User can still write manual conclusion
});
}
}
/**
* Update conclusion remark (edit by initiator)
* PUT /api/v1/conclusions/:requestId
*/
async updateConclusion(req: Request, res: Response) {
try {
const { requestId } = req.params;
const { finalRemark } = req.body;
const userId = (req as any).user?.userId;
if (!finalRemark || typeof finalRemark !== 'string') {
return res.status(400).json({ error: 'Final remark is required' });
}
// Fetch request
const request = await WorkflowRequest.findOne({ where: { requestId } });
if (!request) {
return res.status(404).json({ error: 'Request not found' });
}
// Check if user is the initiator
if ((request as any).initiatorId !== userId) {
return res.status(403).json({ error: 'Only the initiator can update conclusion remarks' });
}
// Find conclusion
const conclusion = await ConclusionRemark.findOne({ where: { requestId } });
if (!conclusion) {
return res.status(404).json({ error: 'Conclusion not found. Generate it first.' });
}
// Update conclusion
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
await conclusion.update({
finalRemark: finalRemark,
editedBy: userId,
isEdited: wasEdited,
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount
} as any);
logger.info(`[Conclusion] Updated conclusion for request ${requestId} (edited: ${wasEdited})`);
return res.status(200).json({
message: 'Conclusion updated successfully',
data: conclusion
});
} catch (error: any) {
logger.error('[Conclusion] Error updating conclusion:', error);
return res.status(500).json({ error: 'Failed to update conclusion' });
}
}
/**
* Finalize conclusion and close request
* POST /api/v1/conclusions/:requestId/finalize
*/
async finalizeConclusion(req: Request, res: Response) {
try {
const { requestId } = req.params;
const { finalRemark } = req.body;
const userId = (req as any).user?.userId;
if (!finalRemark || typeof finalRemark !== 'string') {
return res.status(400).json({ error: 'Final remark is required' });
}
// Fetch request
const request = await WorkflowRequest.findOne({
where: { requestId },
include: [
{ association: 'initiator', attributes: ['userId', 'displayName', 'email'] }
]
});
if (!request) {
return res.status(404).json({ error: 'Request not found' });
}
// Check if user is the initiator
if ((request as any).initiatorId !== userId) {
return res.status(403).json({ error: 'Only the initiator can finalize conclusion remarks' });
}
// Check if request is approved
if ((request as any).status !== 'APPROVED') {
return res.status(400).json({ error: 'Only approved requests can be closed' });
}
// Find or create conclusion
let conclusion = await ConclusionRemark.findOne({ where: { requestId } });
if (!conclusion) {
// Create if doesn't exist (manual conclusion without AI)
conclusion = await ConclusionRemark.create({
requestId,
aiGeneratedRemark: null,
aiModelUsed: null,
aiConfidenceScore: null,
finalRemark: finalRemark,
editedBy: userId,
isEdited: false,
editCount: 0,
approvalSummary: {},
documentSummary: {},
keyDiscussionPoints: [],
generatedAt: null,
finalizedAt: new Date()
} as any);
} else {
// Update existing conclusion
const wasEdited = (conclusion as any).aiGeneratedRemark !== finalRemark;
await conclusion.update({
finalRemark: finalRemark,
editedBy: userId,
isEdited: wasEdited,
editCount: wasEdited ? (conclusion as any).editCount + 1 : (conclusion as any).editCount,
finalizedAt: new Date()
} as any);
}
// Update request status to CLOSED
await request.update({
status: 'CLOSED',
conclusionRemark: finalRemark,
closureDate: new Date()
} as any);
logger.info(`[Conclusion] ✅ Request ${requestId} finalized and closed`);
// Log activity
const requestMeta = getRequestMetadata(req);
await activityService.log({
requestId,
type: 'closed',
user: { userId, name: (request as any).initiator?.displayName || 'Initiator' },
timestamp: new Date().toISOString(),
action: 'Request Closed',
details: `Request closed with conclusion remark by ${(request as any).initiator?.displayName}`,
ipAddress: requestMeta.ipAddress,
userAgent: requestMeta.userAgent
});
return res.status(200).json({
message: 'Request finalized and closed successfully',
data: {
conclusionId: (conclusion as any).conclusionId,
requestNumber: (request as any).requestNumber,
status: 'CLOSED',
finalRemark: finalRemark,
finalizedAt: (conclusion as any).finalizedAt
}
});
} catch (error: any) {
logger.error('[Conclusion] Error finalizing conclusion:', error);
return res.status(500).json({ error: 'Failed to finalize conclusion' });
}
}
/**
* Get conclusion for a request
* GET /api/v1/conclusions/:requestId
*/
async getConclusion(req: Request, res: Response) {
try {
const { requestId } = req.params;
const conclusion = await ConclusionRemark.findOne({
where: { requestId },
include: [
{ association: 'editor', attributes: ['userId', 'displayName', 'email'] }
]
});
if (!conclusion) {
return res.status(404).json({ error: 'Conclusion not found' });
}
return res.status(200).json({
message: 'Conclusion retrieved successfully',
data: conclusion
});
} catch (error: any) {
logger.error('[Conclusion] Error getting conclusion:', error);
return res.status(500).json({ error: 'Failed to get conclusion' });
}
}
}
export const conclusionController = new ConclusionController();