formatedd description implemente along with request number format chnged
This commit is contained in:
parent
7fd5d58080
commit
1b389a8704
@ -1,2 +1,2 @@
|
|||||||
import{a as t}from"./index-BJC2x1CB.js";import"./radix-vendor-BP4rDxsU.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-DB5PynB_.js";import"./socket-vendor-TjCxX7sJ.js";import"./router-vendor-1fSSvDCY.js";async function l(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function m(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function d(n){return(await t.get(`/conclusions/${n}`)).data.data}export{m as finalizeConclusion,l as generateConclusion,d as getConclusion};
|
import{a as t}from"./index-BvTcQxNO.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-0pr8l1kE.js";import"./socket-vendor-TjCxX7sJ.js";import"./router-vendor-1fSSvDCY.js";async function l(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function m(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function d(n){return(await t.get(`/conclusions/${n}`)).data.data}export{m as finalizeConclusion,l as generateConclusion,d as getConclusion};
|
||||||
//# sourceMappingURL=conclusionApi-BNkt5Ttj.js.map
|
//# sourceMappingURL=conclusionApi-Ch9SaBrQ.js.map
|
||||||
@ -1 +1 @@
|
|||||||
{"version":3,"file":"conclusionApi-BNkt5Ttj.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":"0PAwBA,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-Ch9SaBrQ.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":"0PAwBA,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
File diff suppressed because one or more lines are too long
56
build/assets/index-BvTcQxNO.js
Normal file
56
build/assets/index-BvTcQxNO.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-BvTcQxNO.js.map
Normal file
1
build/assets/index-BvTcQxNO.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
build/assets/index-zQ0sgzTQ.css
Normal file
1
build/assets/index-zQ0sgzTQ.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
File diff suppressed because one or more lines are too long
1
build/assets/ui-vendor-0pr8l1kE.js.map
Normal file
1
build/assets/ui-vendor-0pr8l1kE.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -52,14 +52,14 @@
|
|||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-BJC2x1CB.js"></script>
|
<script type="module" crossorigin src="/assets/index-BvTcQxNO.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-BP4rDxsU.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">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-DB5PynB_.js">
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-0pr8l1kE.js">
|
||||||
<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/router-vendor-1fSSvDCY.js">
|
<link rel="modulepreload" crossorigin href="/assets/router-vendor-1fSSvDCY.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-lDXiEKLW.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-zQ0sgzTQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
671
docs/AI_CONCLUSION_REMARK_GENERATION.md
Normal file
671
docs/AI_CONCLUSION_REMARK_GENERATION.md
Normal file
@ -0,0 +1,671 @@
|
|||||||
|
# AI Conclusion Remark Generation Documentation
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Architecture](#architecture)
|
||||||
|
3. [Configuration](#configuration)
|
||||||
|
4. [API Usage](#api-usage)
|
||||||
|
5. [Implementation Details](#implementation-details)
|
||||||
|
6. [Prompt Engineering](#prompt-engineering)
|
||||||
|
7. [Error Handling](#error-handling)
|
||||||
|
8. [Best Practices](#best-practices)
|
||||||
|
9. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The AI Conclusion Remark Generation feature automatically generates professional, context-aware conclusion remarks for workflow requests that have been approved or rejected. This feature uses AI providers (Claude, OpenAI, or Gemini) to analyze the entire request lifecycle and create a comprehensive summary suitable for permanent archiving.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
- **Multi-Provider Support**: Supports Claude (Anthropic), OpenAI (GPT-4), and Google Gemini
|
||||||
|
- **Context-Aware**: Analyzes approval flow, work notes, documents, and activities
|
||||||
|
- **Configurable**: Admin-configurable max length, provider selection, and enable/disable
|
||||||
|
- **Automatic Generation**: Can be triggered automatically when a request is approved/rejected
|
||||||
|
- **Manual Generation**: Users can regenerate conclusions on demand
|
||||||
|
- **Editable**: Generated remarks can be edited before finalization
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
1. **Automatic Generation**: When the final approver approves/rejects a request, an AI conclusion is generated in the background
|
||||||
|
2. **Manual Generation**: Initiator can click "Generate AI Conclusion" button to create or regenerate a conclusion
|
||||||
|
3. **Finalization**: Initiator reviews, edits (if needed), and finalizes the conclusion to close the request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Component Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Frontend (React) │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ useConclusionRemark Hook │ │
|
||||||
|
│ │ - handleGenerateConclusion() │ │
|
||||||
|
│ │ - handleFinalizeConclusion() │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ conclusionApi Service │ │
|
||||||
|
│ │ - generateConclusion(requestId) │ │
|
||||||
|
│ │ - finalizeConclusion(requestId, remark) │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
│ HTTP API
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Backend (Node.js/Express) │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ConclusionController │ │
|
||||||
|
│ │ - generateConclusion() │ │
|
||||||
|
│ │ - finalizeConclusion() │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ AIService │ │
|
||||||
|
│ │ - generateConclusionRemark(context) │ │
|
||||||
|
│ │ - buildConclusionPrompt(context) │ │
|
||||||
|
│ │ - extractKeyPoints(remark) │ │
|
||||||
|
│ │ - calculateConfidence(remark, context) │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ AI Providers (Claude/OpenAI/Gemini) │ │
|
||||||
|
│ │ - ClaudeProvider │ │
|
||||||
|
│ │ - OpenAIProvider │ │
|
||||||
|
│ │ - GeminiProvider │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Database (PostgreSQL) │ │
|
||||||
|
│ │ - conclusion_remarks table │ │
|
||||||
|
│ │ - workflow_requests table │ │
|
||||||
|
│ └──────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. **Request Approval/Rejection** → `ApprovalService.approveLevel()`
|
||||||
|
- Automatically triggers AI generation in background
|
||||||
|
- Saves to `conclusion_remarks` table
|
||||||
|
|
||||||
|
2. **Manual Generation** → `ConclusionController.generateConclusion()`
|
||||||
|
- User clicks "Generate AI Conclusion"
|
||||||
|
- Fetches request context
|
||||||
|
- Calls `AIService.generateConclusionRemark()`
|
||||||
|
- Returns generated remark
|
||||||
|
|
||||||
|
3. **Finalization** → `ConclusionController.finalizeConclusion()`
|
||||||
|
- User reviews and edits (optional)
|
||||||
|
- Submits final remark
|
||||||
|
- Updates request status to `CLOSED`
|
||||||
|
- Saves `finalRemark` to database
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# AI Provider Selection (claude, openai, gemini)
|
||||||
|
AI_PROVIDER=claude
|
||||||
|
|
||||||
|
# Claude Configuration
|
||||||
|
CLAUDE_API_KEY=your_claude_api_key
|
||||||
|
CLAUDE_MODEL=claude-sonnet-4-20250514
|
||||||
|
|
||||||
|
# OpenAI Configuration
|
||||||
|
OPENAI_API_KEY=your_openai_api_key
|
||||||
|
OPENAI_MODEL=gpt-4o
|
||||||
|
|
||||||
|
# Gemini Configuration
|
||||||
|
GEMINI_API_KEY=your_gemini_api_key
|
||||||
|
GEMINI_MODEL=gemini-2.0-flash-lite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin Configuration (Database)
|
||||||
|
|
||||||
|
The system reads configuration from the `system_config` table. Key settings:
|
||||||
|
|
||||||
|
| Config Key | Default | Description |
|
||||||
|
|------------|---------|-------------|
|
||||||
|
| `AI_ENABLED` | `true` | Enable/disable all AI features |
|
||||||
|
| `AI_REMARK_GENERATION_ENABLED` | `true` | Enable/disable conclusion generation |
|
||||||
|
| `AI_PROVIDER` | `claude` | Preferred AI provider (claude, openai, gemini) |
|
||||||
|
| `AI_MAX_REMARK_LENGTH` | `2000` | Maximum characters for generated remarks |
|
||||||
|
| `CLAUDE_API_KEY` | - | Claude API key (if using Claude) |
|
||||||
|
| `CLAUDE_MODEL` | `claude-sonnet-4-20250514` | Claude model name |
|
||||||
|
| `OPENAI_API_KEY` | - | OpenAI API key (if using OpenAI) |
|
||||||
|
| `OPENAI_MODEL` | `gpt-4o` | OpenAI model name |
|
||||||
|
| `GEMINI_API_KEY` | - | Gemini API key (if using Gemini) |
|
||||||
|
| `GEMINI_MODEL` | `gemini-2.0-flash-lite` | Gemini model name |
|
||||||
|
|
||||||
|
### Provider Priority
|
||||||
|
|
||||||
|
1. **Preferred Provider**: Set via `AI_PROVIDER` config
|
||||||
|
2. **Fallback Chain**: If preferred fails, tries:
|
||||||
|
- Claude → OpenAI → Gemini
|
||||||
|
3. **Environment Fallback**: If database config fails, uses environment variables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Usage
|
||||||
|
|
||||||
|
### Generate AI Conclusion
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/v1/conclusions/:requestId/generate`
|
||||||
|
|
||||||
|
**Authentication**: Required (JWT token)
|
||||||
|
|
||||||
|
**Authorization**: Only the request initiator can generate conclusions
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
```http
|
||||||
|
POST /api/v1/conclusions/REQ-2025-00123/generate
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response** (Success - 200):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"conclusionId": "concl-123",
|
||||||
|
"aiGeneratedRemark": "This request for [title] was approved through [levels]...",
|
||||||
|
"keyDiscussionPoints": [
|
||||||
|
"Approved by John Doe at Level 1",
|
||||||
|
"TAT compliance: 85%",
|
||||||
|
"3 documents attached"
|
||||||
|
],
|
||||||
|
"confidence": 0.85,
|
||||||
|
"generatedAt": "2025-01-15T10:30:00Z",
|
||||||
|
"provider": "Claude (Anthropic)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response** (Error - 403):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Only the initiator can generate conclusion remarks"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response** (Error - 400):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Conclusion can only be generated for approved or rejected requests"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finalize Conclusion
|
||||||
|
|
||||||
|
**Endpoint**: `POST /api/v1/conclusions/:requestId/finalize`
|
||||||
|
|
||||||
|
**Authentication**: Required (JWT token)
|
||||||
|
|
||||||
|
**Authorization**: Only the request initiator can finalize
|
||||||
|
|
||||||
|
**Request**:
|
||||||
|
```http
|
||||||
|
POST /api/v1/conclusions/REQ-2025-00123/finalize
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"finalRemark": "This request was approved through all levels. The implementation will begin next week."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response** (Success - 200):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"conclusionId": "concl-123",
|
||||||
|
"finalRemark": "This request was approved through all levels...",
|
||||||
|
"finalizedAt": "2025-01-15T10:35:00Z",
|
||||||
|
"requestStatus": "CLOSED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Existing Conclusion
|
||||||
|
|
||||||
|
**Endpoint**: `GET /api/v1/conclusions/:requestId`
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"conclusionId": "concl-123",
|
||||||
|
"requestId": "REQ-2025-00123",
|
||||||
|
"aiGeneratedRemark": "Generated text...",
|
||||||
|
"finalRemark": "Finalized text...",
|
||||||
|
"isEdited": true,
|
||||||
|
"editCount": 2,
|
||||||
|
"aiModelUsed": "Claude (Anthropic)",
|
||||||
|
"aiConfidenceScore": 0.85,
|
||||||
|
"keyDiscussionPoints": ["Point 1", "Point 2"],
|
||||||
|
"generatedAt": "2025-01-15T10:30:00Z",
|
||||||
|
"finalizedAt": "2025-01-15T10:35:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Context Data Structure
|
||||||
|
|
||||||
|
The `generateConclusionRemark()` method accepts a context object with the following structure:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ConclusionContext {
|
||||||
|
requestTitle: string;
|
||||||
|
requestDescription: string;
|
||||||
|
requestNumber: string;
|
||||||
|
priority: string;
|
||||||
|
approvalFlow: Array<{
|
||||||
|
levelNumber: number;
|
||||||
|
approverName: string;
|
||||||
|
status: 'APPROVED' | 'REJECTED' | 'PENDING' | 'IN_PROGRESS';
|
||||||
|
comments?: string;
|
||||||
|
actionDate?: string;
|
||||||
|
tatHours?: number;
|
||||||
|
elapsedHours?: number;
|
||||||
|
tatPercentageUsed?: number;
|
||||||
|
}>;
|
||||||
|
workNotes: Array<{
|
||||||
|
userName: string;
|
||||||
|
message: string;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
documents: Array<{
|
||||||
|
fileName: string;
|
||||||
|
uploadedBy: string;
|
||||||
|
uploadedAt: string;
|
||||||
|
}>;
|
||||||
|
activities: Array<{
|
||||||
|
type: string;
|
||||||
|
action: string;
|
||||||
|
details: string;
|
||||||
|
timestamp: string;
|
||||||
|
}>;
|
||||||
|
rejectionReason?: string;
|
||||||
|
rejectedBy?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generation Process
|
||||||
|
|
||||||
|
1. **Context Collection**:
|
||||||
|
- Fetches request details from `workflow_requests`
|
||||||
|
- Fetches approval levels from `approval_levels`
|
||||||
|
- Fetches work notes from `work_notes`
|
||||||
|
- Fetches documents from `documents`
|
||||||
|
- Fetches activities from `activities`
|
||||||
|
|
||||||
|
2. **Prompt Building**:
|
||||||
|
- Constructs a detailed prompt with all context
|
||||||
|
- Includes TAT risk information (ON_TRACK, AT_RISK, CRITICAL, BREACHED)
|
||||||
|
- Includes rejection context if applicable
|
||||||
|
- Sets target word count based on `AI_MAX_REMARK_LENGTH`
|
||||||
|
|
||||||
|
3. **AI Generation**:
|
||||||
|
- Sends prompt to selected AI provider
|
||||||
|
- Receives generated text
|
||||||
|
- Validates length (trims if exceeds max)
|
||||||
|
- Extracts key points
|
||||||
|
- Calculates confidence score
|
||||||
|
|
||||||
|
4. **Storage**:
|
||||||
|
- Saves to `conclusion_remarks` table
|
||||||
|
- Links to `workflow_requests` via `requestId`
|
||||||
|
- Stores metadata (provider, confidence, key points)
|
||||||
|
|
||||||
|
### Automatic Generation
|
||||||
|
|
||||||
|
When a request is approved/rejected, `ApprovalService.approveLevel()` automatically generates a conclusion in the background:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In ApprovalService.approveLevel()
|
||||||
|
if (isFinalApproval) {
|
||||||
|
// Background task - doesn't block the approval response
|
||||||
|
(async () => {
|
||||||
|
const context = { /* ... */ };
|
||||||
|
const aiResult = await aiService.generateConclusionRemark(context);
|
||||||
|
await ConclusionRemark.create({ /* ... */ });
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt Engineering
|
||||||
|
|
||||||
|
### Prompt Structure
|
||||||
|
|
||||||
|
The prompt is designed to generate professional, archival-quality conclusions:
|
||||||
|
|
||||||
|
```
|
||||||
|
You are writing a closure summary for a workflow request at Royal Enfield.
|
||||||
|
Write a practical, realistic conclusion that an employee would write when closing a request.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
[Request Number] - [Title]
|
||||||
|
Description: [Description]
|
||||||
|
Priority: [Priority]
|
||||||
|
|
||||||
|
**What Happened:**
|
||||||
|
[Approval Summary with TAT info]
|
||||||
|
[Rejection Context if applicable]
|
||||||
|
|
||||||
|
**Discussions (if any):**
|
||||||
|
[Work Notes Summary]
|
||||||
|
|
||||||
|
**Documents:**
|
||||||
|
[Document List]
|
||||||
|
|
||||||
|
**YOUR TASK:**
|
||||||
|
Write a brief, professional conclusion (approximately X words, max Y characters) that:
|
||||||
|
- Summarizes what was requested and the final decision
|
||||||
|
- Mentions who approved it and any key comments
|
||||||
|
- Mentions if any approval levels were AT_RISK, CRITICAL, or BREACHED
|
||||||
|
- Notes the outcome and next steps (if applicable)
|
||||||
|
- Uses clear, factual language without time-specific references
|
||||||
|
- Is suitable for permanent archiving and future reference
|
||||||
|
- Sounds natural and human-written (not AI-generated)
|
||||||
|
|
||||||
|
**IMPORTANT:**
|
||||||
|
- Be concise and direct
|
||||||
|
- MUST stay within [maxLength] characters limit
|
||||||
|
- No time-specific words like "today", "now", "currently", "recently"
|
||||||
|
- No corporate jargon or buzzwords
|
||||||
|
- No emojis or excessive formatting
|
||||||
|
- Write like a professional documenting a completed process
|
||||||
|
- Focus on facts: what was requested, who approved, what was decided
|
||||||
|
- Use past tense for completed actions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Prompt Features
|
||||||
|
|
||||||
|
1. **TAT Risk Integration**: Includes TAT percentage usage and risk status for each approval level
|
||||||
|
2. **Rejection Handling**: Different instructions for rejected vs approved requests
|
||||||
|
3. **Length Control**: Dynamically sets target word count based on config
|
||||||
|
4. **Tone Guidelines**: Emphasizes natural, professional, archival-quality writing
|
||||||
|
5. **Context Awareness**: Includes all relevant data (approvals, notes, documents, activities)
|
||||||
|
|
||||||
|
### Provider-Specific Settings
|
||||||
|
|
||||||
|
| Provider | Model | Max Tokens | Temperature | Notes |
|
||||||
|
|----------|-------|------------|-------------|-------|
|
||||||
|
| Claude | claude-sonnet-4-20250514 | 2048 | 0.3 | Best for longer, detailed conclusions |
|
||||||
|
| OpenAI | gpt-4o | 1024 | 0.3 | Balanced performance |
|
||||||
|
| Gemini | gemini-2.0-flash-lite | - | 0.3 | Fast and cost-effective |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
1. **No AI Provider Available**
|
||||||
|
```
|
||||||
|
Error: AI features are currently unavailable. Please configure an AI provider...
|
||||||
|
```
|
||||||
|
**Solution**: Configure API keys in admin panel or environment variables
|
||||||
|
|
||||||
|
2. **Provider API Error**
|
||||||
|
```
|
||||||
|
Error: AI generation failed (Claude): API rate limit exceeded
|
||||||
|
```
|
||||||
|
**Solution**: Check API key validity, rate limits, and provider status
|
||||||
|
|
||||||
|
3. **Request Not Found**
|
||||||
|
```
|
||||||
|
Error: Request not found
|
||||||
|
```
|
||||||
|
**Solution**: Verify requestId is correct and request exists
|
||||||
|
|
||||||
|
4. **Unauthorized Access**
|
||||||
|
```
|
||||||
|
Error: Only the initiator can generate conclusion remarks
|
||||||
|
```
|
||||||
|
**Solution**: Ensure user is the request initiator
|
||||||
|
|
||||||
|
5. **Invalid Request Status**
|
||||||
|
```
|
||||||
|
Error: Conclusion can only be generated for approved or rejected requests
|
||||||
|
```
|
||||||
|
**Solution**: Request must be in APPROVED or REJECTED status
|
||||||
|
|
||||||
|
### Error Recovery
|
||||||
|
|
||||||
|
- **Automatic Fallback**: If preferred provider fails, system tries fallback providers
|
||||||
|
- **Graceful Degradation**: If AI generation fails, user can write conclusion manually
|
||||||
|
- **Retry Logic**: Manual regeneration is always available
|
||||||
|
- **Logging**: All errors are logged with context for debugging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
1. **Error Handling**: Always wrap AI calls in try-catch blocks
|
||||||
|
2. **Async Operations**: Use background tasks for automatic generation (don't block approval)
|
||||||
|
3. **Validation**: Validate context data before sending to AI
|
||||||
|
4. **Logging**: Log all AI operations for debugging and monitoring
|
||||||
|
5. **Configuration**: Use database config for flexibility (not hardcoded values)
|
||||||
|
|
||||||
|
### For Administrators
|
||||||
|
|
||||||
|
1. **API Key Management**: Store API keys securely in database or environment variables
|
||||||
|
2. **Provider Selection**: Choose provider based on:
|
||||||
|
- **Claude**: Best quality, higher cost
|
||||||
|
- **OpenAI**: Balanced quality/cost
|
||||||
|
- **Gemini**: Fast, cost-effective
|
||||||
|
3. **Length Configuration**: Set `AI_MAX_REMARK_LENGTH` based on your archival needs
|
||||||
|
4. **Monitoring**: Monitor AI usage and costs through provider dashboards
|
||||||
|
5. **Testing**: Test with sample requests before enabling in production
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
|
||||||
|
1. **Review Before Finalizing**: Always review AI-generated conclusions
|
||||||
|
2. **Edit if Needed**: Don't hesitate to edit the generated text
|
||||||
|
3. **Regenerate**: If not satisfied, regenerate with updated context
|
||||||
|
4. **Finalize Promptly**: Finalize conclusions soon after generation for accuracy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: AI Generation Not Working
|
||||||
|
|
||||||
|
**Symptoms**: Error message "AI features are currently unavailable"
|
||||||
|
|
||||||
|
**Diagnosis**:
|
||||||
|
1. Check `AI_ENABLED` config value
|
||||||
|
2. Check `AI_REMARK_GENERATION_ENABLED` config value
|
||||||
|
3. Verify API keys are configured
|
||||||
|
4. Check provider initialization logs
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
tail -f logs/app.log | grep "AI Service"
|
||||||
|
|
||||||
|
# Verify config
|
||||||
|
SELECT * FROM system_config WHERE config_key LIKE 'AI_%';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Generated Text Too Long/Short
|
||||||
|
|
||||||
|
**Symptoms**: Generated remarks exceed or are much shorter than expected
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Adjust `AI_MAX_REMARK_LENGTH` in admin config
|
||||||
|
2. Check prompt target word count calculation
|
||||||
|
3. Verify provider max_tokens setting
|
||||||
|
|
||||||
|
### Issue: Poor Quality Conclusions
|
||||||
|
|
||||||
|
**Symptoms**: Generated text is generic or inaccurate
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Verify context data is complete (approvals, notes, documents)
|
||||||
|
2. Check prompt includes all relevant information
|
||||||
|
3. Try different provider (Claude generally produces better quality)
|
||||||
|
4. Adjust temperature if needed (lower = more focused)
|
||||||
|
|
||||||
|
### Issue: Slow Generation
|
||||||
|
|
||||||
|
**Symptoms**: AI generation takes too long
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
1. Check provider API status
|
||||||
|
2. Verify network connectivity
|
||||||
|
3. Consider using faster provider (Gemini)
|
||||||
|
4. Check for rate limiting
|
||||||
|
|
||||||
|
### Issue: Provider Not Initializing
|
||||||
|
|
||||||
|
**Symptoms**: Provider shows as "None" in logs
|
||||||
|
|
||||||
|
**Diagnosis**:
|
||||||
|
1. Check API key is valid
|
||||||
|
2. Verify SDK package is installed
|
||||||
|
3. Check environment variables
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Install missing SDK
|
||||||
|
npm install @anthropic-ai/sdk # For Claude
|
||||||
|
npm install openai # For OpenAI
|
||||||
|
npm install @google/generative-ai # For Gemini
|
||||||
|
|
||||||
|
# Verify API key
|
||||||
|
echo $CLAUDE_API_KEY # Should show key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### conclusion_remarks Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE conclusion_remarks (
|
||||||
|
conclusion_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
request_id VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
ai_generated_remark TEXT,
|
||||||
|
ai_model_used VARCHAR(100),
|
||||||
|
ai_confidence_score DECIMAL(3,2),
|
||||||
|
final_remark TEXT,
|
||||||
|
edited_by UUID,
|
||||||
|
is_edited BOOLEAN DEFAULT false,
|
||||||
|
edit_count INTEGER DEFAULT 0,
|
||||||
|
approval_summary JSONB,
|
||||||
|
document_summary JSONB,
|
||||||
|
key_discussion_points TEXT[],
|
||||||
|
generated_at TIMESTAMP,
|
||||||
|
finalized_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
FOREIGN KEY (request_id) REFERENCES workflow_requests(request_id),
|
||||||
|
FOREIGN KEY (edited_by) REFERENCES users(user_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Fields
|
||||||
|
|
||||||
|
- `ai_generated_remark`: Original AI-generated text
|
||||||
|
- `final_remark`: User-edited/finalized text
|
||||||
|
- `ai_confidence_score`: Quality score (0.0 - 1.0)
|
||||||
|
- `key_discussion_points`: Extracted key points array
|
||||||
|
- `approval_summary`: JSON with approval statistics
|
||||||
|
- `document_summary`: JSON with document information
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Approved Request Conclusion
|
||||||
|
|
||||||
|
**Context**:
|
||||||
|
- Request: "Purchase 50 laptops for IT department"
|
||||||
|
- Priority: STANDARD
|
||||||
|
- 3 approval levels, all approved
|
||||||
|
- TAT: 100%, 85%, 90% usage
|
||||||
|
- 2 documents attached
|
||||||
|
|
||||||
|
**Generated Conclusion**:
|
||||||
|
```
|
||||||
|
This request for the purchase of 50 laptops for the IT department was approved
|
||||||
|
through all three approval levels. The request was reviewed and approved by
|
||||||
|
John Doe at Level 1, Jane Smith at Level 2, and Bob Johnson at Level 3. All
|
||||||
|
approval levels completed within their respective TAT windows, with Level 1
|
||||||
|
using 100% of allocated time. The purchase order has been generated and
|
||||||
|
forwarded to the procurement team for processing. Implementation is expected
|
||||||
|
to begin within the next two weeks.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Rejected Request Conclusion
|
||||||
|
|
||||||
|
**Context**:
|
||||||
|
- Request: "Implement new HR policy"
|
||||||
|
- Priority: EXPRESS
|
||||||
|
- Rejected at Level 2 by Jane Smith
|
||||||
|
- Reason: "Budget constraints"
|
||||||
|
|
||||||
|
**Generated Conclusion**:
|
||||||
|
```
|
||||||
|
This request for implementing a new HR policy was reviewed through two approval
|
||||||
|
levels but was ultimately rejected. The request was approved by John Doe at
|
||||||
|
Level 1, but rejected by Jane Smith at Level 2 due to budget constraints.
|
||||||
|
The rejection was communicated to the initiator, and alternative approaches
|
||||||
|
are being considered. The request documentation has been archived for future
|
||||||
|
reference.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.0.0** (2025-01-15): Initial implementation
|
||||||
|
- Multi-provider support (Claude, OpenAI, Gemini)
|
||||||
|
- Automatic and manual generation
|
||||||
|
- TAT risk integration
|
||||||
|
- Key points extraction
|
||||||
|
- Confidence scoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check logs: `logs/app.log`
|
||||||
|
2. Review admin configuration panel
|
||||||
|
3. Contact development team
|
||||||
|
4. Refer to provider documentation:
|
||||||
|
- [Claude API Docs](https://docs.anthropic.com)
|
||||||
|
- [OpenAI API Docs](https://platform.openai.com/docs)
|
||||||
|
- [Gemini API Docs](https://ai.google.dev/docs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: January 2025
|
||||||
|
**Maintained By**: Royal Enfield Development Team
|
||||||
|
|
||||||
@ -18,8 +18,9 @@ export class DashboardController {
|
|||||||
const dateRange = req.query.dateRange as string | undefined;
|
const dateRange = req.query.dateRange as string | undefined;
|
||||||
const startDate = req.query.startDate as string | undefined;
|
const startDate = req.query.startDate as string | undefined;
|
||||||
const endDate = req.query.endDate as string | undefined;
|
const endDate = req.query.endDate as string | undefined;
|
||||||
|
const viewAsUser = req.query.viewAsUser === 'true'; // For admin to view as normal user
|
||||||
|
|
||||||
const kpis = await this.dashboardService.getKPIs(userId, dateRange, startDate, endDate);
|
const kpis = await this.dashboardService.getKPIs(userId, dateRange, startDate, endDate, viewAsUser);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -246,8 +247,9 @@ export class DashboardController {
|
|||||||
const userId = (req as any).user?.userId;
|
const userId = (req as any).user?.userId;
|
||||||
const page = Number(req.query.page || 1);
|
const page = Number(req.query.page || 1);
|
||||||
const limit = Number(req.query.limit || 10);
|
const limit = Number(req.query.limit || 10);
|
||||||
|
const viewAsUser = req.query.viewAsUser === 'true'; // For admin to view as normal user
|
||||||
|
|
||||||
const result = await this.dashboardService.getRecentActivity(userId, page, limit);
|
const result = await this.dashboardService.getRecentActivity(userId, page, limit, viewAsUser);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -276,8 +278,9 @@ export class DashboardController {
|
|||||||
const userId = (req as any).user?.userId;
|
const userId = (req as any).user?.userId;
|
||||||
const page = Number(req.query.page || 1);
|
const page = Number(req.query.page || 1);
|
||||||
const limit = Number(req.query.limit || 10);
|
const limit = Number(req.query.limit || 10);
|
||||||
|
const viewAsUser = req.query.viewAsUser === 'true'; // For admin to view as normal user
|
||||||
|
|
||||||
const result = await this.dashboardService.getCriticalRequests(userId, page, limit);
|
const result = await this.dashboardService.getCriticalRequests(userId, page, limit, viewAsUser);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@ -306,8 +309,9 @@ export class DashboardController {
|
|||||||
const userId = (req as any).user?.userId;
|
const userId = (req as any).user?.userId;
|
||||||
const page = Number(req.query.page || 1);
|
const page = Number(req.query.page || 1);
|
||||||
const limit = Number(req.query.limit || 10);
|
const limit = Number(req.query.limit || 10);
|
||||||
|
const viewAsUser = req.query.viewAsUser === 'true'; // For admin to view as normal user
|
||||||
|
|
||||||
const result = await this.dashboardService.getUpcomingDeadlines(userId, page, limit);
|
const result = await this.dashboardService.getUpcomingDeadlines(userId, page, limit, viewAsUser);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
34
src/migrations/20250123-update-request-number-format.ts
Normal file
34
src/migrations/20250123-update-request-number-format.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { QueryInterface } from 'sequelize';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration: Update Request Number Format
|
||||||
|
*
|
||||||
|
* This migration documents the change in request number format from:
|
||||||
|
* - Old: REQ-YYYY-NNNNN (e.g., REQ-2025-12345)
|
||||||
|
* - New: REQ-YYYY-MM-XXXX (e.g., REQ-2025-11-0001)
|
||||||
|
*
|
||||||
|
* The counter now resets every month automatically.
|
||||||
|
*
|
||||||
|
* No schema changes are required as the request_number column (VARCHAR(20))
|
||||||
|
* is already sufficient for the new format (16 characters).
|
||||||
|
*
|
||||||
|
* Existing request numbers will remain unchanged.
|
||||||
|
* New requests will use the new format starting from this migration.
|
||||||
|
*/
|
||||||
|
export async function up(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
// No schema changes needed - this is a code-level change only
|
||||||
|
// The generateRequestNumber() function in helpers.ts has been updated
|
||||||
|
// to generate the new format: REQ-YYYY-MM-XXXX
|
||||||
|
|
||||||
|
// Log the change for reference
|
||||||
|
console.log('[Migration] Request number format updated to REQ-YYYY-MM-XXXX');
|
||||||
|
console.log('[Migration] Counter will reset automatically each month');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(queryInterface: QueryInterface): Promise<void> {
|
||||||
|
// No rollback needed - this is a code-level change
|
||||||
|
// To revert, simply update the generateRequestNumber() function
|
||||||
|
// in helpers.ts back to the old format
|
||||||
|
console.log('[Migration] Request number format can be reverted by updating generateRequestNumber() function');
|
||||||
|
}
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get all KPIs for dashboard
|
* Get all KPIs for dashboard
|
||||||
*/
|
*/
|
||||||
async getKPIs(userId: string, dateRange?: string, startDate?: string, endDate?: string) {
|
async getKPIs(userId: string, dateRange?: string, startDate?: string, endDate?: string, viewAsUser?: boolean) {
|
||||||
const range = this.parseDateRange(dateRange, startDate, endDate);
|
const range = this.parseDateRange(dateRange, startDate, endDate);
|
||||||
|
|
||||||
// Run all KPI queries in parallel for performance
|
// Run all KPI queries in parallel for performance
|
||||||
@ -95,11 +95,11 @@ export class DashboardService {
|
|||||||
engagement,
|
engagement,
|
||||||
aiInsights
|
aiInsights
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getRequestStats(userId, dateRange, startDate, endDate),
|
this.getRequestStats(userId, dateRange, startDate, endDate, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, viewAsUser),
|
||||||
this.getTATEfficiency(userId, dateRange, startDate, endDate),
|
this.getTATEfficiency(userId, dateRange, startDate, endDate, viewAsUser),
|
||||||
this.getApproverLoad(userId, dateRange, startDate, endDate),
|
this.getApproverLoad(userId, dateRange, startDate, endDate, viewAsUser),
|
||||||
this.getEngagementStats(userId, dateRange, startDate, endDate),
|
this.getEngagementStats(userId, dateRange, startDate, endDate, viewAsUser),
|
||||||
this.getAIInsights(userId, dateRange, startDate, endDate)
|
this.getAIInsights(userId, dateRange, startDate, endDate, viewAsUser)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -131,15 +131,17 @@ export class DashboardService {
|
|||||||
approver?: string,
|
approver?: string,
|
||||||
approverType?: 'current' | 'any',
|
approverType?: 'current' | 'any',
|
||||||
search?: string,
|
search?: string,
|
||||||
slaCompliance?: string
|
slaCompliance?: string,
|
||||||
|
viewAsUser?: boolean
|
||||||
) {
|
) {
|
||||||
// Check if date range should be applied
|
// Check if date range should be applied
|
||||||
const applyDateRange = dateRange !== undefined && dateRange !== null;
|
const applyDateRange = dateRange !== undefined && dateRange !== null;
|
||||||
const range = applyDateRange ? this.parseDateRange(dateRange, startDate, endDate) : null;
|
const range = applyDateRange ? this.parseDateRange(dateRange, startDate, endDate) : null;
|
||||||
|
|
||||||
// Check if user is admin or management (has broader access)
|
// Check if user is admin or management (has broader access)
|
||||||
|
// If viewAsUser is true, treat as normal user even if admin
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
const isAdmin = user?.hasManagementAccess() || false;
|
const isAdmin = viewAsUser ? false : (user?.hasManagementAccess() || false);
|
||||||
|
|
||||||
// Build filter conditions
|
// Build filter conditions
|
||||||
let filterConditions = '';
|
let filterConditions = '';
|
||||||
@ -346,12 +348,13 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get TAT efficiency metrics
|
* Get TAT efficiency metrics
|
||||||
*/
|
*/
|
||||||
async getTATEfficiency(userId: string, dateRange?: string, startDate?: string, endDate?: string) {
|
async getTATEfficiency(userId: string, dateRange?: string, startDate?: string, endDate?: string, viewAsUser?: boolean) {
|
||||||
const range = this.parseDateRange(dateRange, startDate, endDate);
|
const range = this.parseDateRange(dateRange, startDate, endDate);
|
||||||
|
|
||||||
// Check if user is admin or management (has broader access)
|
// Check if user is admin or management (has broader access)
|
||||||
|
// If viewAsUser is true, treat as normal user even if admin
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
const isAdmin = user?.hasManagementAccess() || false;
|
const isAdmin = viewAsUser ? false : (user?.hasManagementAccess() || false);
|
||||||
|
|
||||||
// For regular users: only their initiated requests
|
// For regular users: only their initiated requests
|
||||||
// For admin: all requests
|
// For admin: all requests
|
||||||
@ -601,11 +604,12 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get approver load statistics
|
* Get approver load statistics
|
||||||
*/
|
*/
|
||||||
async getApproverLoad(userId: string, dateRange?: string, startDate?: string, endDate?: string) {
|
async getApproverLoad(userId: string, dateRange?: string, startDate?: string, endDate?: string, viewAsUser?: boolean) {
|
||||||
const range = this.parseDateRange(dateRange, startDate, endDate);
|
const range = this.parseDateRange(dateRange, startDate, endDate);
|
||||||
|
|
||||||
// Get pending actions where user is the CURRENT active approver
|
// Get pending actions where user is the CURRENT active approver
|
||||||
// This means: the request is at this user's level AND it's the current level
|
// This means: the request is at this user's level AND it's the current level
|
||||||
|
// Note: getApproverLoad is always user-specific (shows user's own pending/completed), so viewAsUser doesn't change behavior
|
||||||
const pendingResult = await sequelize.query(`
|
const pendingResult = await sequelize.query(`
|
||||||
SELECT COUNT(DISTINCT al.level_id)::int AS pending_count
|
SELECT COUNT(DISTINCT al.level_id)::int AS pending_count
|
||||||
FROM approval_levels al
|
FROM approval_levels al
|
||||||
@ -673,12 +677,13 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get engagement and quality metrics
|
* Get engagement and quality metrics
|
||||||
*/
|
*/
|
||||||
async getEngagementStats(userId: string, dateRange?: string, startDate?: string, endDate?: string) {
|
async getEngagementStats(userId: string, dateRange?: string, startDate?: string, endDate?: string, viewAsUser?: boolean) {
|
||||||
const range = this.parseDateRange(dateRange, startDate, endDate);
|
const range = this.parseDateRange(dateRange, startDate, endDate);
|
||||||
|
|
||||||
// Check if user is admin or management (has broader access)
|
// Check if user is admin or management (has broader access)
|
||||||
|
// If viewAsUser is true, treat as normal user even if admin
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
const isAdmin = user?.hasManagementAccess() || false;
|
const isAdmin = viewAsUser ? false : (user?.hasManagementAccess() || false);
|
||||||
|
|
||||||
// Get work notes count - uses created_at
|
// Get work notes count - uses created_at
|
||||||
// For regular users: only from requests they initiated
|
// For regular users: only from requests they initiated
|
||||||
@ -738,12 +743,13 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get AI insights and closure metrics
|
* Get AI insights and closure metrics
|
||||||
*/
|
*/
|
||||||
async getAIInsights(userId: string, dateRange?: string, startDate?: string, endDate?: string) {
|
async getAIInsights(userId: string, dateRange?: string, startDate?: string, endDate?: string, viewAsUser?: boolean) {
|
||||||
const range = this.parseDateRange(dateRange, startDate, endDate);
|
const range = this.parseDateRange(dateRange, startDate, endDate);
|
||||||
|
|
||||||
// Check if user is admin or management (has broader access)
|
// Check if user is admin or management (has broader access)
|
||||||
|
// If viewAsUser is true, treat as normal user even if admin
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
const isAdmin = user?.hasManagementAccess() || false;
|
const isAdmin = viewAsUser ? false : (user?.hasManagementAccess() || false);
|
||||||
|
|
||||||
// For regular users: only their initiated requests
|
// For regular users: only their initiated requests
|
||||||
// Use submission_date instead of created_at to filter by actual submission date
|
// Use submission_date instead of created_at to filter by actual submission date
|
||||||
@ -1113,10 +1119,11 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get recent activity feed with pagination
|
* Get recent activity feed with pagination
|
||||||
*/
|
*/
|
||||||
async getRecentActivity(userId: string, page: number = 1, limit: number = 10) {
|
async getRecentActivity(userId: string, page: number = 1, limit: number = 10, viewAsUser?: boolean) {
|
||||||
// Check if user is admin or management (has broader access)
|
// Check if user is admin or management (has broader access)
|
||||||
|
// If viewAsUser is true, treat as normal user even if admin
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
const isAdmin = user?.hasManagementAccess() || false;
|
const isAdmin = viewAsUser ? false : (user?.hasManagementAccess() || false);
|
||||||
|
|
||||||
// For regular users: only activities from their initiated requests OR where they're a participant
|
// For regular users: only activities from their initiated requests OR where they're a participant
|
||||||
let whereClause = isAdmin ? '' : `
|
let whereClause = isAdmin ? '' : `
|
||||||
@ -1197,10 +1204,11 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get critical requests (breached TAT or approaching deadline) with pagination
|
* Get critical requests (breached TAT or approaching deadline) with pagination
|
||||||
*/
|
*/
|
||||||
async getCriticalRequests(userId: string, page: number = 1, limit: number = 10) {
|
async getCriticalRequests(userId: string, page: number = 1, limit: number = 10, viewAsUser?: boolean) {
|
||||||
// Check if user is admin or management (has broader access)
|
// Check if user is admin or management (has broader access)
|
||||||
|
// If viewAsUser is true, treat as normal user even if admin
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
const isAdmin = user?.hasManagementAccess() || false;
|
const isAdmin = viewAsUser ? false : (user?.hasManagementAccess() || false);
|
||||||
|
|
||||||
// For regular users: show only requests where they are current approver (awaiting their approval)
|
// For regular users: show only requests where they are current approver (awaiting their approval)
|
||||||
// For admins: show all critical requests organization-wide
|
// For admins: show all critical requests organization-wide
|
||||||
@ -1476,10 +1484,11 @@ export class DashboardService {
|
|||||||
/**
|
/**
|
||||||
* Get upcoming deadlines with pagination
|
* Get upcoming deadlines with pagination
|
||||||
*/
|
*/
|
||||||
async getUpcomingDeadlines(userId: string, page: number = 1, limit: number = 10) {
|
async getUpcomingDeadlines(userId: string, page: number = 1, limit: number = 10, viewAsUser?: boolean) {
|
||||||
// Check if user is admin or management (has broader access)
|
// Check if user is admin or management (has broader access)
|
||||||
|
// If viewAsUser is true, treat as normal user even if admin
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
const isAdmin = user?.hasManagementAccess() || false;
|
const isAdmin = viewAsUser ? false : (user?.hasManagementAccess() || false);
|
||||||
|
|
||||||
// For regular users: only show CURRENT LEVEL where they are the approver
|
// For regular users: only show CURRENT LEVEL where they are the approver
|
||||||
// For admins: show all current active levels
|
// For admins: show all current active levels
|
||||||
|
|||||||
@ -1973,7 +1973,7 @@ export class WorkflowService {
|
|||||||
}
|
}
|
||||||
async createWorkflow(initiatorId: string, workflowData: CreateWorkflowRequest, requestMetadata?: { ipAddress?: string | null; userAgent?: string | null }): Promise<WorkflowRequest> {
|
async createWorkflow(initiatorId: string, workflowData: CreateWorkflowRequest, requestMetadata?: { ipAddress?: string | null; userAgent?: string | null }): Promise<WorkflowRequest> {
|
||||||
try {
|
try {
|
||||||
const requestNumber = generateRequestNumber();
|
const requestNumber = await generateRequestNumber();
|
||||||
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
|
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
|
||||||
|
|
||||||
const workflow = await WorkflowRequest.create({
|
const workflow = await WorkflowRequest.create({
|
||||||
|
|||||||
@ -1,7 +1,55 @@
|
|||||||
export const generateRequestNumber = (): string => {
|
import { WorkflowRequest } from '@models/WorkflowRequest';
|
||||||
const year = new Date().getFullYear();
|
import { Op } from 'sequelize';
|
||||||
const randomNumber = Math.floor(Math.random() * 100000).toString().padStart(5, '0');
|
import logger from './logger';
|
||||||
return `REQ-${year}-${randomNumber}`;
|
|
||||||
|
/**
|
||||||
|
* Generate request number in format: REQ-YYYY-MM-XXXX
|
||||||
|
* Counter resets every month
|
||||||
|
* Example: REQ-2025-11-0001
|
||||||
|
*/
|
||||||
|
export const generateRequestNumber = async (): Promise<string> => {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = (now.getMonth() + 1).toString().padStart(2, '0'); // Month is 0-indexed, so add 1
|
||||||
|
|
||||||
|
// Build the prefix pattern for current year-month
|
||||||
|
const prefix = `REQ-${year}-${month}-`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the highest counter for the current year-month
|
||||||
|
const existingRequests = await WorkflowRequest.findAll({
|
||||||
|
where: {
|
||||||
|
requestNumber: {
|
||||||
|
[Op.like]: `${prefix}%`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes: ['requestNumber'],
|
||||||
|
order: [['requestNumber', 'DESC']],
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
if (existingRequests.length > 0) {
|
||||||
|
// Extract the counter from the last request number
|
||||||
|
const lastRequestNumber = (existingRequests[0] as any).requestNumber;
|
||||||
|
const lastCounter = parseInt(lastRequestNumber.replace(prefix, ''), 10);
|
||||||
|
|
||||||
|
if (!isNaN(lastCounter)) {
|
||||||
|
counter = lastCounter + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format counter as 4-digit number (0001, 0002, etc.)
|
||||||
|
const counterStr = counter.toString().padStart(4, '0');
|
||||||
|
|
||||||
|
return `${prefix}${counterStr}`;
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to timestamp-based counter if database query fails
|
||||||
|
logger.error('Error generating request number:', error);
|
||||||
|
const fallbackCounter = Date.now().toString().slice(-4);
|
||||||
|
return `${prefix}${fallbackCounter}`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const calculateTATDays = (tatHours: number): number => {
|
export const calculateTATDays = (tatHours: number): number => {
|
||||||
|
|||||||
@ -6,7 +6,8 @@ export const approvalActionSchema = z.object({
|
|||||||
rejectionReason: z.string().optional(),
|
rejectionReason: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper to validate UUID or requestNumber format (REQ-YYYY-NNNNN)
|
// Helper to validate UUID or requestNumber format
|
||||||
|
// Supports both old format (REQ-YYYY-NNNNN) and new format (REQ-YYYY-MM-XXXX)
|
||||||
const workflowIdValidator = z.string().refine(
|
const workflowIdValidator = z.string().refine(
|
||||||
(val) => {
|
(val) => {
|
||||||
// Check if it's a UUID
|
// Check if it's a UUID
|
||||||
@ -14,15 +15,18 @@ const workflowIdValidator = z.string().refine(
|
|||||||
if (uuidRegex.test(val)) {
|
if (uuidRegex.test(val)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check if it's a requestNumber format (REQ-YYYY-NNNNN)
|
// Check if it's a valid requestNumber format
|
||||||
const requestNumberRegex = /^REQ-\d{4}-\d+$/i;
|
// Old format: REQ-YYYY-NNNNN (e.g., REQ-2025-12057) - 5+ digits after year
|
||||||
if (requestNumberRegex.test(val)) {
|
// New format: REQ-YYYY-MM-XXXX (e.g., REQ-2025-11-0001) - 2-digit month, 4-digit counter
|
||||||
|
const oldFormatRegex = /^REQ-\d{4}-\d{5,}$/i; // Old: REQ-2025-12057
|
||||||
|
const newFormatRegex = /^REQ-\d{4}-\d{2}-\d{4}$/i; // New: REQ-2025-11-0001
|
||||||
|
if (oldFormatRegex.test(val) || newFormatRegex.test(val)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'Invalid workflow ID - must be a UUID or requestNumber format (REQ-YYYY-NNNNN)'
|
message: 'Invalid workflow ID - must be a UUID or requestNumber format (e.g., REQ-2025-11-0001 or REQ-2025-12057)'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export const updateWorkflowSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper to validate UUID or requestNumber format
|
// Helper to validate UUID or requestNumber format
|
||||||
|
// Supports both old format (REQ-YYYY-NNNNN) and new format (REQ-YYYY-MM-XXXX)
|
||||||
const workflowIdValidator = z.string().refine(
|
const workflowIdValidator = z.string().refine(
|
||||||
(val) => {
|
(val) => {
|
||||||
// Check if it's a valid UUID
|
// Check if it's a valid UUID
|
||||||
@ -63,15 +64,18 @@ const workflowIdValidator = z.string().refine(
|
|||||||
if (uuidRegex.test(val)) {
|
if (uuidRegex.test(val)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check if it's a valid requestNumber format (e.g., REQ-2025-12057)
|
// Check if it's a valid requestNumber format
|
||||||
const requestNumberRegex = /^REQ-\d{4}-\d{5,}$/i;
|
// Old format: REQ-YYYY-NNNNN (e.g., REQ-2025-12057) - 5+ digits after year
|
||||||
if (requestNumberRegex.test(val)) {
|
// New format: REQ-YYYY-MM-XXXX (e.g., REQ-2025-11-0001) - 2-digit month, 4-digit counter
|
||||||
|
const oldFormatRegex = /^REQ-\d{4}-\d{5,}$/i; // Old: REQ-2025-12057
|
||||||
|
const newFormatRegex = /^REQ-\d{4}-\d{2}-\d{4}$/i; // New: REQ-2025-11-0001
|
||||||
|
if (oldFormatRegex.test(val) || newFormatRegex.test(val)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'Invalid workflow ID - must be a valid UUID or requestNumber (e.g., REQ-2025-12057)',
|
message: 'Invalid workflow ID - must be a valid UUID or requestNumber (e.g., REQ-2025-11-0001 or REQ-2025-12057)',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user