preview and token issue resolved
This commit is contained in:
parent
595f4db36c
commit
b82cc3a5ff
@ -1,2 +1,2 @@
|
||||
import{a as t}from"./index-YNtuvw0h.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-CFl7S1sk.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-1fSSvDCY.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||
//# sourceMappingURL=conclusionApi-CHSYkcgZ.js.map
|
||||
import{a as t}from"./index-DuNOVwDS.js";import"./radix-vendor-C2EbRL2a.js";import"./charts-vendor-Cji9-Yri.js";import"./utils-vendor-DHm03ykU.js";import"./ui-vendor-biwEPLZp.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-1fSSvDCY.js";async function m(n){return(await t.post(`/conclusions/${n}/generate`)).data.data}async function d(n,o){return(await t.post(`/conclusions/${n}/finalize`,{finalRemark:o})).data.data}async function f(n){return(await t.get(`/conclusions/${n}`)).data.data}export{d as finalizeConclusion,m as generateConclusion,f as getConclusion};
|
||||
//# sourceMappingURL=conclusionApi-COMANvz1.js.map
|
||||
@ -1 +1 @@
|
||||
{"version":3,"file":"conclusionApi-CHSYkcgZ.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":"6RAwBA,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-COMANvz1.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":"6RAwBA,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
1
build/assets/index-CiGKtlR4.css
Normal file
1
build/assets/index-CiGKtlR4.css
Normal file
File diff suppressed because one or more lines are too long
64
build/assets/index-DuNOVwDS.js
Normal file
64
build/assets/index-DuNOVwDS.js
Normal file
File diff suppressed because one or more lines are too long
1
build/assets/index-DuNOVwDS.js.map
Normal file
1
build/assets/index-DuNOVwDS.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
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-biwEPLZp.js.map
Normal file
1
build/assets/ui-vendor-biwEPLZp.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -52,15 +52,15 @@
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-YNtuvw0h.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DuNOVwDS.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Cji9-Yri.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/ui-vendor-CFl7S1sk.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-biwEPLZp.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-vendor-1fSSvDCY.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BZdlHmA5.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CiGKtlR4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
159
docs/NOTIFICATIONS.md
Normal file
159
docs/NOTIFICATIONS.md
Normal file
@ -0,0 +1,159 @@
|
||||
# Notification Triggers Documentation
|
||||
|
||||
This document lists all notification triggers in the Royal Enfield Workflow Management System.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The system sends push notifications to users based on various workflow events. Notifications are sent via the `notificationService.sendToUsers()` method and support web push notifications.
|
||||
|
||||
---
|
||||
|
||||
## 1. Workflow Service Notifications
|
||||
|
||||
**File:** `src/services/workflow.service.ts`
|
||||
|
||||
| # | Trigger Event | Recipient | Title | Body | Priority |
|
||||
|---|---------------|-----------|-------|------|----------|
|
||||
| 1 | New approver added to request | New Approver | "New Request Assignment" | "You have been added as an approver to request {requestNumber}: {title}" | DEFAULT |
|
||||
| 2 | Approver skipped → Next approver notified | Next Approver | "Request Escalated" | "Previous approver was skipped. Request {requestNumber} is now awaiting your approval." | DEFAULT |
|
||||
| 3 | New approver added at specific level | New Approver | "New Request Assignment" | "You have been added as Level {X} approver to request {requestNumber}: {title}" | DEFAULT |
|
||||
| 4 | Spectator added to request | Spectator | "Added to Request" | "You have been added as a spectator to request {requestNumber}: {title}" | DEFAULT |
|
||||
| 5 | Request submitted by initiator | Initiator | "Request Submitted Successfully" | "Your request '{title}' has been submitted and is now with the first approver." | MEDIUM |
|
||||
| 6 | Request submitted → First approver assigned | First Approver | "New Request Assigned" | "{title}" | HIGH |
|
||||
|
||||
---
|
||||
|
||||
## 2. Approval Service Notifications
|
||||
|
||||
**File:** `src/services/approval.service.ts`
|
||||
|
||||
| # | Trigger Event | Recipient | Title | Body | Priority |
|
||||
|---|---------------|-----------|-------|------|----------|
|
||||
| 7 | Final approval complete (closure pending) | Initiator | "Request Approved - Closure Pending" | "Your request '{title}' has been fully approved. Please review and finalize the conclusion remark to close the request." | HIGH |
|
||||
| 8 | Final approval complete (info only) | All Participants | "Request Approved" | "Request '{title}' has been fully approved. The initiator will finalize the conclusion remark to close the request." | MEDIUM |
|
||||
| 9 | Level approved → Next level activated | Next Approver | "Action required: {requestNumber}" | "{title}" | DEFAULT |
|
||||
| 10 | Request fully approved (auto-close) | Initiator | "Approved: {requestNumber}" | "{title}" | DEFAULT |
|
||||
| 11 | Request rejected | Initiator + All Participants | "Rejected: {requestNumber}" | "{title}" | DEFAULT |
|
||||
|
||||
---
|
||||
|
||||
## 3. Pause Service Notifications
|
||||
|
||||
**File:** `src/services/pause.service.ts`
|
||||
|
||||
| # | Trigger Event | Recipient | Title | Body | Priority |
|
||||
|---|---------------|-----------|-------|------|----------|
|
||||
| 12 | Workflow paused by approver | Initiator | "Workflow Paused" | "Your request '{title}' has been paused by {userName}. Reason: {reason}. Will resume on {date}." | HIGH |
|
||||
| 13 | Workflow paused (confirmation) | Approver (self) | "Workflow Paused Successfully" | "You have paused request '{title}'. It will automatically resume on {date}." | MEDIUM |
|
||||
| 14 | Workflow resumed | Initiator | "Workflow Resumed" | "Your request '{title}' has been resumed {by user/automatically}." | HIGH |
|
||||
| 15 | Workflow resumed | Approver | "Workflow Resumed" | "Request '{title}' has been resumed. Please continue with your review." | HIGH |
|
||||
| 16 | Initiator requests pause cancellation | Approver who paused | "Pause Retrigger Request" | "{initiatorName} is requesting you to cancel the pause and resume work on request '{title}'." | HIGH |
|
||||
|
||||
---
|
||||
|
||||
## 4. TAT (Turnaround Time) Notifications
|
||||
|
||||
**File:** `src/queues/tatProcessor.ts`
|
||||
|
||||
| # | Trigger Event | Recipient | Title | Body | Priority |
|
||||
|---|---------------|-----------|-------|------|----------|
|
||||
| 17 | TAT 50% threshold reached | Approver | "TAT Reminder" | "50% TAT Alert: {message}" | MEDIUM |
|
||||
| 18 | TAT 75% threshold reached | Approver | "TAT Reminder" | "75% TAT Alert: {message}" | HIGH |
|
||||
| 19 | TAT 100% breach | Approver | "TAT Breach Alert" | "TAT Breached: {message}" | URGENT |
|
||||
| 20 | TAT breach (initiator notification) | Initiator | "TAT Breach - Request Delayed" | "Your request {requestNumber}: '{title}' has exceeded its TAT. The approver has been notified." | HIGH |
|
||||
|
||||
---
|
||||
|
||||
## 5. Work Note Notifications
|
||||
|
||||
**File:** `src/services/worknote.service.ts`
|
||||
|
||||
| # | Trigger Event | Recipient | Title | Body | Priority |
|
||||
|---|---------------|-----------|-------|------|----------|
|
||||
| 21 | User mentioned in work note | Mentioned Users | "💬 Mentioned in Work Note" | "{userName} mentioned you in {requestNumber}: '{message preview}'" | DEFAULT |
|
||||
|
||||
---
|
||||
|
||||
## Summary by Recipient
|
||||
|
||||
### Initiator Receives:
|
||||
- ✅ Request submitted confirmation
|
||||
- ✅ Approval pending closure (action required)
|
||||
- ✅ Request approved
|
||||
- ✅ Request rejected
|
||||
- ✅ Workflow paused
|
||||
- ✅ Workflow resumed
|
||||
- ✅ TAT breach notification
|
||||
|
||||
### Approver Receives:
|
||||
- ✅ New request assignment
|
||||
- ✅ Request escalation (previous approver skipped)
|
||||
- ✅ Action required (next level activated)
|
||||
- ✅ Workflow paused confirmation
|
||||
- ✅ Workflow resumed
|
||||
- ✅ Pause retrigger request from initiator
|
||||
- ✅ TAT 50% reminder
|
||||
- ✅ TAT 75% reminder
|
||||
- ✅ TAT 100% breach alert
|
||||
|
||||
### Spectator/Participant Receives:
|
||||
- ✅ Added to request
|
||||
- ✅ Request approved (info only)
|
||||
- ✅ Request rejected
|
||||
|
||||
### Mentioned Users Receive:
|
||||
- ✅ Work note mention notification
|
||||
|
||||
---
|
||||
|
||||
## Notification Payload Structure
|
||||
|
||||
```typescript
|
||||
interface NotificationPayload {
|
||||
title: string; // Notification title
|
||||
body: string; // Notification message
|
||||
requestId?: string; // UUID of the request
|
||||
requestNumber?: string; // Human-readable request number (e.g., REQ-2025-11-0001)
|
||||
url?: string; // Deep link URL (e.g., /request/REQ-2025-11-0001)
|
||||
type?: string; // Notification type for categorization
|
||||
priority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
|
||||
actionRequired?: boolean; // Whether user action is required
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Priority Levels
|
||||
|
||||
| Priority | Use Case |
|
||||
|----------|----------|
|
||||
| **URGENT** | TAT breach alerts |
|
||||
| **HIGH** | Action required notifications, assignments, resumptions |
|
||||
| **MEDIUM** | Informational updates, confirmations |
|
||||
| **LOW/DEFAULT** | General updates, spectator notifications |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Notifications can be enabled/disabled via environment variables:
|
||||
|
||||
```env
|
||||
ENABLE_EMAIL_NOTIFICATIONS=true
|
||||
ENABLE_PUSH_NOTIFICATIONS=true
|
||||
```
|
||||
|
||||
Web push requires VAPID keys:
|
||||
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your_public_key
|
||||
VAPID_PRIVATE_KEY=your_private_key
|
||||
VAPID_SUBJECT=mailto:admin@example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: November 2025*
|
||||
|
||||
@ -145,16 +145,18 @@ export async function getPublicConfig() {
|
||||
ui: SYSTEM_CONFIG.UI
|
||||
};
|
||||
|
||||
// Try to get AI service status and configuration (gracefully handle if not available)
|
||||
try {
|
||||
const { aiService } = require('../services/ai.service');
|
||||
// Get configuration from database first (always try to read from DB)
|
||||
const { getConfigValue } = require('../services/configReader.service');
|
||||
|
||||
// Get AI configuration from admin settings
|
||||
// Get AI configuration from admin settings (database)
|
||||
const aiEnabled = (await getConfigValue('AI_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||
const remarkGenerationEnabled = (await getConfigValue('AI_REMARK_GENERATION_ENABLED', 'true'))?.toLowerCase() === 'true';
|
||||
const maxRemarkLength = parseInt(await getConfigValue('AI_MAX_REMARK_LENGTH', '2000') || '2000', 10);
|
||||
|
||||
// Try to get AI service status (gracefully handle if not available)
|
||||
try {
|
||||
const { aiService } = require('../services/ai.service');
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
ai: {
|
||||
@ -168,14 +170,14 @@ export async function getPublicConfig() {
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// AI service not available - return config without AI info
|
||||
// AI service not available - return config with database values but AI disabled
|
||||
return {
|
||||
...baseConfig,
|
||||
ai: {
|
||||
enabled: false,
|
||||
provider: 'None',
|
||||
remarkGenerationEnabled: false,
|
||||
maxRemarkLength: 2000,
|
||||
maxRemarkLength: maxRemarkLength, // Use database value, not hardcoded
|
||||
features: {
|
||||
conclusionGeneration: false
|
||||
}
|
||||
|
||||
@ -154,9 +154,23 @@ export class WorkflowController {
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkflowDetails(req: Request, res: Response): Promise<void> {
|
||||
async getWorkflowDetails(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params as any;
|
||||
const userId = req.user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
ResponseHandler.error(res, 'Authentication required', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user has access to this request
|
||||
const accessCheck = await workflowService.checkUserRequestAccess(userId, id);
|
||||
if (!accessCheck.hasAccess) {
|
||||
ResponseHandler.error(res, accessCheck.reason || 'Access denied', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await workflowService.getWorkflowDetails(id);
|
||||
if (!result) {
|
||||
ResponseHandler.notFound(res, 'Workflow not found');
|
||||
|
||||
@ -384,10 +384,30 @@ class AIService {
|
||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||
|
||||
// Validate and trim if exceeds max length
|
||||
// Validate length - AI should already be within limit, but trim as safety net
|
||||
if (remarkText.length > maxLength) {
|
||||
logger.warn(`[AI Service] Generated remark exceeds max length (${remarkText.length} > ${maxLength}), trimming...`);
|
||||
remarkText = remarkText.substring(0, maxLength - 3) + '...'; // Trim with ellipsis
|
||||
logger.warn(`[AI Service] ⚠️ AI exceeded character limit (${remarkText.length} > ${maxLength}). This should be rare - AI was instructed to prioritize and condense. Applying safety trim...`);
|
||||
|
||||
// Try to find a natural break point (sentence end) near the limit
|
||||
const safeLimit = maxLength - 3;
|
||||
let trimPoint = safeLimit;
|
||||
|
||||
// Look for last sentence end (. ! ?) within the safe limit
|
||||
const lastPeriod = remarkText.lastIndexOf('.', safeLimit);
|
||||
const lastExclaim = remarkText.lastIndexOf('!', safeLimit);
|
||||
const lastQuestion = remarkText.lastIndexOf('?', safeLimit);
|
||||
const bestBreak = Math.max(lastPeriod, lastExclaim, lastQuestion);
|
||||
|
||||
// Use sentence break if it's reasonably close to the limit (within 80% of max)
|
||||
if (bestBreak > maxLength * 0.8) {
|
||||
trimPoint = bestBreak + 1; // Include the punctuation
|
||||
remarkText = remarkText.substring(0, trimPoint).trim();
|
||||
} else {
|
||||
// Fall back to hard trim with ellipsis
|
||||
remarkText = remarkText.substring(0, safeLimit).trim() + '...';
|
||||
}
|
||||
|
||||
logger.info(`[AI Service] Trimmed to ${remarkText.length} characters`);
|
||||
}
|
||||
|
||||
// Extract key points (look for bullet points or numbered items)
|
||||
@ -513,17 +533,28 @@ ${isRejected
|
||||
- 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
|
||||
**CRITICAL CHARACTER LIMIT - STRICT REQUIREMENT:**
|
||||
- Your response MUST be EXACTLY within ${maxLength} characters (not words, CHARACTERS including spaces)
|
||||
- Count your characters carefully before responding
|
||||
- If you have too much content, PRIORITIZE the most important information:
|
||||
1. Final decision (approved/rejected)
|
||||
2. Key approvers and their decisions
|
||||
3. Critical TAT breaches (if any)
|
||||
4. Brief summary of the request
|
||||
- OMIT less important details to fit within the limit rather than exceeding it
|
||||
- Better to be concise than to exceed the limit
|
||||
|
||||
**WRITING GUIDELINES:**
|
||||
- Be concise and direct - every word must add value
|
||||
- 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 ${isRejected ? 'rejected' : 'approved'}, what was decided
|
||||
- Use past tense for completed actions
|
||||
- Use short sentences and avoid filler words
|
||||
|
||||
Write the conclusion now (remember: max ${maxLength} characters):`;
|
||||
Write the conclusion now. STRICT LIMIT: ${maxLength} characters maximum. Prioritize and condense if needed:`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
@ -2258,6 +2258,75 @@ export class WorkflowService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has access to view a specific request.
|
||||
* User has access if they are:
|
||||
* 1. Admin/Management (has management access)
|
||||
* 2. The initiator of the request
|
||||
* 3. An approver at any level of the request
|
||||
* 4. A spectator/participant of the request
|
||||
*
|
||||
* @param userId - The user ID to check access for
|
||||
* @param requestId - The request ID or request number
|
||||
* @returns Object with hasAccess boolean and reason string
|
||||
*/
|
||||
async checkUserRequestAccess(userId: string, requestId: string): Promise<{ hasAccess: boolean; reason?: string }> {
|
||||
try {
|
||||
// First, find the workflow
|
||||
const workflowBase = await this.findWorkflowByIdentifier(requestId);
|
||||
if (!workflowBase) {
|
||||
return { hasAccess: false, reason: 'Request not found' };
|
||||
}
|
||||
|
||||
const actualRequestId = (workflowBase as any).getDataValue
|
||||
? (workflowBase as any).getDataValue('requestId')
|
||||
: (workflowBase as any).requestId;
|
||||
|
||||
// Check 1: Is the user an admin/management?
|
||||
const user = await User.findByPk(userId);
|
||||
if (user && user.hasManagementAccess()) {
|
||||
return { hasAccess: true };
|
||||
}
|
||||
|
||||
// Check 2: Is the user the initiator?
|
||||
const initiatorId = (workflowBase as any).initiatorId || (workflowBase as any).initiator_id;
|
||||
if (initiatorId === userId) {
|
||||
return { hasAccess: true };
|
||||
}
|
||||
|
||||
// Check 3: Is the user an approver at any level?
|
||||
const isApprover = await ApprovalLevel.findOne({
|
||||
where: {
|
||||
requestId: actualRequestId,
|
||||
approverId: userId
|
||||
}
|
||||
});
|
||||
if (isApprover) {
|
||||
return { hasAccess: true };
|
||||
}
|
||||
|
||||
// Check 4: Is the user a spectator/participant?
|
||||
const isParticipant = await Participant.findOne({
|
||||
where: {
|
||||
requestId: actualRequestId,
|
||||
userId: userId
|
||||
}
|
||||
});
|
||||
if (isParticipant) {
|
||||
return { hasAccess: true };
|
||||
}
|
||||
|
||||
// No access
|
||||
return {
|
||||
hasAccess: false,
|
||||
reason: 'You do not have permission to view this request. Access is restricted to the initiator, approvers, and spectators of this request.'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Failed to check user access for request ${requestId}:`, error);
|
||||
throw new Error('Failed to verify access permissions');
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkflowDetails(requestId: string) {
|
||||
try {
|
||||
const workflowBase = await this.findWorkflowByIdentifier(requestId);
|
||||
@ -2761,25 +2830,36 @@ export class WorkflowService {
|
||||
const initiatorName = (initiator as any)?.displayName || (initiator as any)?.email || 'User';
|
||||
const workflowTitle = (updated as any).title || 'Request';
|
||||
|
||||
// Log submitted activity (similar to created activity in createWorkflow)
|
||||
// Check if this was a previously saved draft (has activity history before submission)
|
||||
// or a direct submission (createWorkflow + submitWorkflow in same flow)
|
||||
const { Activity } = require('@models/Activity');
|
||||
const existingActivities = await Activity.count({
|
||||
where: { requestId: (updated as any).requestId }
|
||||
});
|
||||
|
||||
// Only log "Request submitted" if this is a draft being submitted (has prior activities)
|
||||
// For direct submissions, createWorkflow already logs "Initial request submitted"
|
||||
if (existingActivities > 1) {
|
||||
// This is a saved draft being submitted later
|
||||
activityService.log({
|
||||
requestId: (updated as any).requestId,
|
||||
type: 'submitted',
|
||||
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
action: 'Draft submitted',
|
||||
details: `Draft request "${workflowTitle}" submitted for approval by ${initiatorName}`
|
||||
});
|
||||
} else {
|
||||
// Direct submission - just update the status, createWorkflow already logged the activity
|
||||
activityService.log({
|
||||
requestId: (updated as any).requestId,
|
||||
type: 'submitted',
|
||||
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
action: 'Request submitted',
|
||||
details: `Request "${workflowTitle}" submitted by ${initiatorName}`
|
||||
});
|
||||
|
||||
// Log status change activity
|
||||
activityService.log({
|
||||
requestId: (updated as any).requestId,
|
||||
type: 'status_change',
|
||||
user: initiatorId ? { userId: initiatorId, name: initiatorName } : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
action: 'Submitted',
|
||||
details: 'Request moved from DRAFT to PENDING'
|
||||
details: `Request "${workflowTitle}" submitted for approval`
|
||||
});
|
||||
}
|
||||
|
||||
const current = await ApprovalLevel.findOne({
|
||||
where: { requestId: (updated as any).requestId, levelNumber: (updated as any).currentLevel || 1 }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user