new attribute added for request status as worflow state , and filter and summary related isus fixed which came after migartion
This commit is contained in:
parent
c9a0305d44
commit
47fabeb15e
43
debug-finalize.ts
Normal file
43
debug-finalize.ts
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
import dns from 'dns';
|
||||
import { WorkflowRequestModel } from './src/models/mongoose/WorkflowRequest.schema';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
const mongoUri = process.env.MONGO_URI || process.env.MONGODB_URL;
|
||||
if (!mongoUri) {
|
||||
console.error('MONGO_URI not found in .env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (mongoUri.startsWith('mongodb+srv://')) {
|
||||
dns.setServers(['8.8.8.8', '8.8.4.4', '1.1.1.1', '1.0.0.1']);
|
||||
}
|
||||
|
||||
await mongoose.connect(mongoUri);
|
||||
console.log('✅ Connected to MongoDB');
|
||||
|
||||
const requests = await WorkflowRequestModel.find({
|
||||
$or: [
|
||||
{ conclusionRemark: { $exists: true, $ne: null } },
|
||||
{ workflowState: 'CLOSED' }
|
||||
]
|
||||
}).sort({ updatedAt: -1 }).limit(10);
|
||||
|
||||
console.log('Results (Last 10 finalized/closed):');
|
||||
requests.forEach(r => {
|
||||
console.log(`- REQ: ${r.requestNumber}, Status: ${r.status}, State: ${r.workflowState}, HasRemark: ${!!r.conclusionRemark}`);
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Check failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
check();
|
||||
61
docs/PLAN_STATUS_REFINEMENT.md
Normal file
61
docs/PLAN_STATUS_REFINEMENT.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Implementation Plan: Status Ambiguity Refinement
|
||||
|
||||
This document outlines the specific code changes required to implement the **Dual-Key Status Architecture**.
|
||||
|
||||
## 1. Goal
|
||||
Decouple the business outcome (Approved/Rejected) from the lifecycle state (Open/Closed/Draft) to ensure transparency in finalized requests.
|
||||
|
||||
## 2. Schema Changes
|
||||
|
||||
### `WorkflowRequest.schema.ts`
|
||||
- **Update `status` Enum**: Remove `CLOSED` and `CANCELLED`.
|
||||
- **Add `workflowState`**:
|
||||
- Type: `String`
|
||||
- Enum: `['DRAFT', 'OPEN', 'CLOSED']`
|
||||
- Default: `'DRAFT'`
|
||||
- Index: `true`
|
||||
|
||||
## 3. Logic Updates
|
||||
|
||||
### A. Workflow Creation (`WorkflowService.createWorkflow`)
|
||||
- Initialize `status: 'DRAFT'`.
|
||||
- Initialize `workflowState: 'DRAFT'`.
|
||||
- Set `isDraft: true`.
|
||||
|
||||
### B. Workflow Submission (`WorkflowService.submitRequest`)
|
||||
- Update `status: 'PENDING'`.
|
||||
- Update `workflowState: 'OPEN'`.
|
||||
- Set `isDraft: false`.
|
||||
|
||||
### C. Approval/Rejection (`WorkflowService`)
|
||||
- When approved at a level: Keep `status` as `IN_PROGRESS` or set to `APPROVED` if final.
|
||||
- When rejected: Set `status` to `REJECTED`.
|
||||
- **Crucial**: The `workflowState` remains `OPEN` during these actions.
|
||||
|
||||
### D. Finalization (`ConclusionController.finalizeConclusion`)
|
||||
- **Current Behavior**: Sets `status = 'CLOSED'`.
|
||||
- **New Behavior**:
|
||||
- Sets `workflowState = 'CLOSED'`.
|
||||
- **Does NOT** change `status`. The `status` will remain `APPROVED` or `REJECTED`.
|
||||
- Sets `closureDate = new Date()`.
|
||||
|
||||
### E. Pause Logic (`PauseMongoService`)
|
||||
- Set `status = 'PAUSED'`.
|
||||
- Set `isPaused = true`.
|
||||
- Keep `workflowState = 'OPEN'`.
|
||||
|
||||
## 4. Dashboard & KPI Updates (`DashboardMongoService`)
|
||||
|
||||
### `getRequestStats`
|
||||
- Update the aggregation pipeline to group by `workflowState`.
|
||||
- `OPEN` category will now include all requests where `workflowState == 'OPEN'`.
|
||||
- `CLOSED` category will now include all requests where `workflowState == 'CLOSED'`.
|
||||
- This ensures that a "Closed" count on the dashboard includes both Approved and Rejected requests that have been finalized.
|
||||
|
||||
### `getTATEfficiency`
|
||||
- Update match criteria to `workflowState: 'CLOSED'` instead of `status: 'CLOSED'`.
|
||||
|
||||
## 5. Filter Alignment (`listWorkflowsInternal`)
|
||||
- Update the status filter to handle the new field mapping.
|
||||
- If user filters by `status: 'CLOSED'`, the query will target `workflowState: 'CLOSED'`.
|
||||
- If user filters by `status: 'APPROVED'`, the query will target `status: 'APPROVED'`.
|
||||
55
docs/STATUS_ARCHITECTURE.md
Normal file
55
docs/STATUS_ARCHITECTURE.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Dual-Key Status Architecture
|
||||
|
||||
This document defines the status management system for the Royal Enfield Workflow application. It uses a "Dual-Key" approach to resolve ambiguity between request lifecycles and business outcomes.
|
||||
|
||||
## 1. Core Concepts
|
||||
|
||||
| Key | Purpose | Possible Values |
|
||||
| :--- | :--- | :--- |
|
||||
| **`status`** | **Business Outcome**. Tells you *what* happened or the current granular action. | `DRAFT`, `PENDING`, `IN_PROGRESS`, `APPROVED`, `REJECTED`, `PAUSED` |
|
||||
| **`workflowState`** | **Lifecycle State**. Tells you *where* the request is in its journey. | `DRAFT`, `OPEN`, `CLOSED` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Status Mapping Table
|
||||
|
||||
The `workflowState` is automatically derived from the `status` and the finalization event (Conclusion Remark).
|
||||
|
||||
| Primary Status | Finalized? | workflowState | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `DRAFT` | No | `DRAFT` | Request is being prepared by the initiator. |
|
||||
| `PENDING` | No | `OPEN` | Waiting for first level activation or system processing. |
|
||||
| `IN_PROGRESS` | No | `OPEN` | Actively moving through approval levels. |
|
||||
| `PAUSED` | No | `OPEN` | Temporarily frozen; `isPaused` flag is `true`. |
|
||||
| `APPROVED` | No | `OPEN` | All levels approved, but initiator hasn't written the final conclusion. |
|
||||
| `REJECTED` | No | `OPEN` | Rejected by an approver, but initiator hasn't acknowledged/finalized. |
|
||||
| **`APPROVED`** | **Yes** | **`CLOSED`** | **Final state: Approved and Archived.** |
|
||||
| **`REJECTED`** | **Yes** | **`CLOSED`** | **Final state: Rejected and Archived.** |
|
||||
|
||||
---
|
||||
|
||||
## 3. Ambiguity Resolution (The "Why")
|
||||
|
||||
Previously, the system changed `status` to `CLOSED` after finalization, which destroyed the information about whether the request was Approved or Rejected.
|
||||
|
||||
**Corrected Behavior:**
|
||||
- **Outcome remains visible**: A finalized request will now keep its `status` as `APPROVED` or `REJECTED`.
|
||||
- **Filtering made easy**: Dashboard charts use `workflowState: 'CLOSED'` to count all finished work, while list filters use `status: 'APPROVED'` to find specific results.
|
||||
|
||||
---
|
||||
|
||||
## 4. Technical Implementation Notes
|
||||
|
||||
### Schema Changes
|
||||
- **`WorkflowRequest`**: Added `workflowState` (String, Indexed).
|
||||
- **`status` Enum**: Removed `CLOSED` (deprecated) and `CANCELLED`.
|
||||
|
||||
### Transition Logic
|
||||
1. **Approval/Rejection**: Updates `status` to `APPROVED` or `REJECTED`. `workflowState` remains `OPEN`.
|
||||
2. **Finalization (Conclusion)**: Triggered by initiator. Updates `workflowState` to `CLOSED`. **Does NOT change `status`.**
|
||||
3. **Pause**: Set `status` to `PAUSED` and `isPaused: true`. `workflowState` stays `OPEN`.
|
||||
|
||||
### Impacted Services
|
||||
- `DashboardMongoService`: Uses `workflowState` for Facet/KPI counts.
|
||||
- `WorkflowService`: Filter logic updated to respect both keys.
|
||||
- `ConclusionController`: `finalizeConclusion` logic updated to toggle `workflowState`.
|
||||
@ -25,7 +25,7 @@ export class ConclusionController {
|
||||
}
|
||||
|
||||
// Check if user is the initiator (compare userId strings)
|
||||
if ((request as any).initiatorId !== userId) {
|
||||
if ((request as any).initiator.userId !== userId) {
|
||||
return res.status(403).json({ error: 'Only the initiator can generate conclusion remarks' });
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ export class ConclusionController {
|
||||
: (level.elapsedHours && level.tatHours ? (Number(level.elapsedHours) / Number(level.tatHours)) * 100 : 0);
|
||||
return {
|
||||
levelNumber: level.levelNumber,
|
||||
approverName: level.approverName,
|
||||
approverName: level.approver?.name || level.approverName || 'Unknown',
|
||||
status: level.status,
|
||||
comments: level.comments,
|
||||
actionDate: level.actionDate,
|
||||
@ -232,7 +232,7 @@ export class ConclusionController {
|
||||
}
|
||||
|
||||
// Check if user is the initiator
|
||||
if ((request as any).initiatorId !== userId) {
|
||||
if ((request as any).initiator.userId !== userId) {
|
||||
return res.status(403).json({ error: 'Only the initiator can update conclusion remarks' });
|
||||
}
|
||||
|
||||
@ -287,10 +287,10 @@ export class ConclusionController {
|
||||
}
|
||||
|
||||
// Fetch initiator manually
|
||||
const initiator = await User.findOne({ userId: (request as any).initiatorId });
|
||||
const initiator = await User.findOne({ userId: (request as any).initiator.userId });
|
||||
|
||||
// Check if user is the initiator
|
||||
if ((request as any).initiatorId !== userId) {
|
||||
if ((request as any).initiator.userId !== userId) {
|
||||
return res.status(403).json({ error: 'Only the initiator can finalize conclusion remarks' });
|
||||
}
|
||||
|
||||
@ -333,8 +333,8 @@ export class ConclusionController {
|
||||
await conclusion.save();
|
||||
}
|
||||
|
||||
// Update request status to CLOSED
|
||||
request.status = 'CLOSED';
|
||||
// Update request workflowState to CLOSED (keep granular status as APPROVED/REJECTED)
|
||||
request.workflowState = 'CLOSED';
|
||||
(request as any).conclusionRemark = finalRemark;
|
||||
(request as any).closureDate = new Date();
|
||||
await request.save();
|
||||
|
||||
@ -53,6 +53,7 @@ export class DashboardController {
|
||||
const approverType = req.query.approverType as 'current' | 'any' | undefined;
|
||||
const search = req.query.search as string | undefined;
|
||||
const slaCompliance = req.query.slaCompliance as string | undefined;
|
||||
const lifecycle = req.query.lifecycle as string | undefined;
|
||||
const viewAsUser = req.query.viewAsUser === 'true'; // When true, treat admin as normal user
|
||||
|
||||
const stats = await this.dashboardService.getRequestStats(
|
||||
@ -69,7 +70,8 @@ export class DashboardController {
|
||||
approverType,
|
||||
search,
|
||||
slaCompliance,
|
||||
viewAsUser
|
||||
viewAsUser,
|
||||
lifecycle
|
||||
);
|
||||
|
||||
res.json({
|
||||
|
||||
@ -485,6 +485,7 @@ export class WorkflowController {
|
||||
dateRange: req.query.dateRange as string | undefined,
|
||||
startDate: req.query.startDate as string | undefined,
|
||||
endDate: req.query.endDate as string | undefined,
|
||||
lifecycle: req.query.lifecycle as string | undefined,
|
||||
};
|
||||
|
||||
// USE MONGODB SERVICE FOR LISTING
|
||||
@ -514,8 +515,9 @@ export class WorkflowController {
|
||||
const dateRange = req.query.dateRange as string | undefined;
|
||||
const startDate = req.query.startDate as string | undefined;
|
||||
const endDate = req.query.endDate as string | undefined;
|
||||
const lifecycle = req.query.lifecycle as string | undefined;
|
||||
|
||||
const filters = { search, status, priority, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate };
|
||||
const filters = { search, status, priority, department, initiator, approverName: approver, approverType, slaCompliance, dateRange, startDate, endDate, lifecycle };
|
||||
|
||||
const result = await workflowServiceMongo.listMyRequests(userId, page, limit, filters);
|
||||
ResponseHandler.success(res, result, 'My requests fetched');
|
||||
@ -548,8 +550,9 @@ export class WorkflowController {
|
||||
const dateRange = req.query.dateRange as string | undefined;
|
||||
const startDate = req.query.startDate as string | undefined;
|
||||
const endDate = req.query.endDate as string | undefined;
|
||||
const lifecycle = req.query.lifecycle as string | undefined;
|
||||
|
||||
const filters = { search, status, priority, templateType, department, initiator, approver, approverType, slaCompliance, dateRange, startDate, endDate };
|
||||
const filters = { search, status, priority, templateType, department, initiator, approverName: approver, approverType, slaCompliance, dateRange, startDate, endDate, lifecycle };
|
||||
|
||||
const result = await workflowServiceMongo.listParticipantRequests(userId, page, limit, filters);
|
||||
ResponseHandler.success(res, result, 'Participant requests fetched');
|
||||
@ -578,8 +581,9 @@ export class WorkflowController {
|
||||
const dateRange = req.query.dateRange as string | undefined;
|
||||
const startDate = req.query.startDate as string | undefined;
|
||||
const endDate = req.query.endDate as string | undefined;
|
||||
const lifecycle = req.query.lifecycle as string | undefined;
|
||||
|
||||
const filters = { search, status, priority, templateType, department, slaCompliance, dateRange, startDate, endDate };
|
||||
const filters = { search, status, priority, templateType, department, slaCompliance, dateRange, startDate, endDate, lifecycle };
|
||||
|
||||
const result = await workflowServiceMongo.listMyInitiatedRequests(userId, page, limit, filters);
|
||||
ResponseHandler.success(res, result, 'My initiated requests fetched');
|
||||
|
||||
@ -17,7 +17,8 @@ export interface IWorkflowRequest extends Document {
|
||||
title: string;
|
||||
description: string;
|
||||
priority: 'STANDARD' | 'EXPRESS';
|
||||
status: 'DRAFT' | 'PENDING' | 'IN_PROGRESS' | 'APPROVED' | 'REJECTED' | 'CLOSED' | 'PAUSED' | 'CANCELLED';
|
||||
status: 'DRAFT' | 'PENDING' | 'IN_PROGRESS' | 'APPROVED' | 'REJECTED' | 'PAUSED';
|
||||
workflowState: 'DRAFT' | 'OPEN' | 'CLOSED';
|
||||
|
||||
// Flattened/Cached Fields for KPIs
|
||||
currentLevel: number; // Display purposes - can become stale when levels shift
|
||||
@ -68,7 +69,13 @@ const WorkflowRequestSchema = new Schema<IWorkflowRequest>({
|
||||
priority: { type: String, enum: ['STANDARD', 'EXPRESS'], default: 'STANDARD' },
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['DRAFT', 'PENDING', 'IN_PROGRESS', 'APPROVED', 'REJECTED', 'CLOSED', 'PAUSED', 'CANCELLED'],
|
||||
enum: ['DRAFT', 'PENDING', 'IN_PROGRESS', 'APPROVED', 'REJECTED', 'PAUSED'],
|
||||
default: 'DRAFT',
|
||||
index: true
|
||||
},
|
||||
workflowState: {
|
||||
type: String,
|
||||
enum: ['DRAFT', 'OPEN', 'CLOSED'],
|
||||
default: 'DRAFT',
|
||||
index: true
|
||||
},
|
||||
|
||||
@ -264,6 +264,14 @@ class AIService {
|
||||
// Use Vertex AI to generate text
|
||||
let remarkText = await this.generateText(prompt);
|
||||
|
||||
// STRIP MARKDOWN CODE BLOCKS (Clean up AI response)
|
||||
// Sometimes AI wraps HTML in ```html ... ``` even when asked not to
|
||||
remarkText = remarkText
|
||||
.replace(/^```html\s*/i, '') // Remove opening ```html
|
||||
.replace(/^```\s*/i, '') // Remove opening ```
|
||||
.replace(/\s*```$/, '') // Remove closing ```
|
||||
.trim(); // Trim whitespace
|
||||
|
||||
// Get max length from config for logging
|
||||
const maxLengthStr = await getConfigValue('AI_MAX_REMARK_LENGTH', '2000');
|
||||
const maxLength = parseInt(maxLengthStr || '2000', 10);
|
||||
@ -442,6 +450,7 @@ ${isRejected
|
||||
<p><strong>Outcome:</strong> [Final outcome]</p>
|
||||
- Keep HTML clean and minimal - no inline styles, no divs, no classes
|
||||
- The HTML should render nicely in a rich text editor
|
||||
- IMPORTANT: DO NOT wrap the output in markdown code blocks (e.g., \`\`\`html ... \`\`\`). Return ONLY the raw HTML string.
|
||||
|
||||
Write the conclusion now in HTML format. STRICT LIMIT: ${maxLength} characters maximum (including HTML tags). Prioritize and condense if needed:`;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -609,7 +609,8 @@ export class DealerClaimMongoService {
|
||||
|
||||
if (action === 'CANCEL') {
|
||||
// Update workflow status
|
||||
workflow.status = WorkflowStatus.CANCELLED; // Make sure WorkflowStatus.CANCELLED exists or use 'CANCELLED'
|
||||
workflow.status = 'REJECTED';
|
||||
workflow.workflowState = 'CLOSED';
|
||||
workflow.isDeleted = true; // Soft delete or just mark cancelled? Usually cancelled.
|
||||
// Let's stick to status update.
|
||||
await workflow.save();
|
||||
|
||||
@ -118,6 +118,7 @@ export class PauseMongoService {
|
||||
workflow.pauseReason = reason;
|
||||
workflow.pauseResumeDate = resumeDate;
|
||||
workflow.status = 'PAUSED';
|
||||
workflow.workflowState = 'OPEN';
|
||||
await workflow.save();
|
||||
|
||||
// Cancel jobs
|
||||
@ -265,6 +266,7 @@ export class PauseMongoService {
|
||||
workflow.pauseReason = undefined;
|
||||
workflow.pauseResumeDate = undefined;
|
||||
workflow.status = 'IN_PROGRESS'; // Assuming previous status was IN_PROGRESS or PENDING
|
||||
workflow.workflowState = 'OPEN';
|
||||
await workflow.save();
|
||||
|
||||
// Cancel Resume Job
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
import logger from '../utils/logger';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import mongoose from 'mongoose';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
/**
|
||||
* Summary Service
|
||||
@ -36,7 +37,7 @@ export class SummaryService {
|
||||
throw new Error('Request must be closed (APPROVED, REJECTED, or CLOSED) before creating summary');
|
||||
}
|
||||
|
||||
const initiatorId = (workflow as any).initiatorId;
|
||||
const initiatorId = (workflow as any).initiator?.userId || (workflow as any).initiatorId;
|
||||
const isInitiator = initiatorId === userId;
|
||||
const isAdmin = userRole && ['admin', 'super_admin', 'management'].includes(userRole.toLowerCase());
|
||||
|
||||
@ -248,6 +249,8 @@ export class SummaryService {
|
||||
const summary = await RequestSummary.findOne({ summaryId });
|
||||
if (!summary) throw new Error('Summary not found');
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
if (summary.initiatorId !== sharedBy) {
|
||||
throw new Error('Only the initiator can share this summary');
|
||||
}
|
||||
@ -265,6 +268,14 @@ export class SummaryService {
|
||||
// Try email
|
||||
const userByEmail = await User.findOne({ email: uid });
|
||||
if (userByEmail) internalUserIds.push(userByEmail.userId);
|
||||
else {
|
||||
// Try fetching from Okta and creating (Auto-onboarding)
|
||||
// uid might be Okta ID
|
||||
const neUser = await userService.ensureOktaUserExists(uid);
|
||||
if (neUser) {
|
||||
internalUserIds.push(neUser.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -468,4 +468,24 @@ export class UserService {
|
||||
|
||||
return user;
|
||||
}
|
||||
/**
|
||||
* Ensure user exists by fetching from Okta ID (Auto-onboarding)
|
||||
*/
|
||||
async ensureOktaUserExists(oktaId: string): Promise<IUser | null> {
|
||||
try {
|
||||
// 1. Fetch from Okta
|
||||
const oktaUser = await this.fetchUserFromOktaById(oktaId);
|
||||
if (!oktaUser) return null;
|
||||
|
||||
// 2. Extract Data
|
||||
const ssoData = extractOktaUserData(oktaUser);
|
||||
if (!ssoData) return null;
|
||||
|
||||
// 3. Create or Update in DB
|
||||
return await this.createOrUpdateUser(ssoData);
|
||||
} catch (error) {
|
||||
logger.error(`[UserService] Failed to ensure Okta user exists for ID ${oktaId}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import logger from '../utils/logger';
|
||||
import { notificationMongoService } from './notification.service';
|
||||
import { activityMongoService } from './activity.service';
|
||||
import { tatSchedulerMongoService } from './tatScheduler.service';
|
||||
import { addWorkingHours, addWorkingHoursExpress, calculateSLAStatus } from '../utils/tatTimeUtils';
|
||||
import { addWorkingHours, addWorkingHoursExpress, calculateSLAStatus, formatTime } from '../utils/tatTimeUtils';
|
||||
|
||||
const tatScheduler = tatSchedulerMongoService;
|
||||
|
||||
@ -132,6 +132,7 @@ export class WorkflowServiceMongo {
|
||||
description: workflowData.description,
|
||||
priority: workflowData.priority,
|
||||
status: 'DRAFT',
|
||||
workflowState: 'DRAFT',
|
||||
currentLevel: 1,
|
||||
totalLevels: workflowData.approvalLevels.length,
|
||||
totalTatHours,
|
||||
@ -157,7 +158,7 @@ export class WorkflowServiceMongo {
|
||||
},
|
||||
tat: {
|
||||
assignedHours: level.tatHours,
|
||||
assignedDays: Math.ceil(level.tatHours / 24),
|
||||
assignedDays: Math.ceil(level.tatHours / 8),
|
||||
elapsedHours: 0,
|
||||
remainingHours: level.tatHours,
|
||||
percentageUsed: 0,
|
||||
@ -922,6 +923,47 @@ export class WorkflowServiceMongo {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Parse date range string to Date objects
|
||||
*/
|
||||
private parseDateRange(dateRange?: string, startDate?: string, endDate?: string): { start: Date; end: Date } | null {
|
||||
if (dateRange === 'custom' && startDate && endDate) {
|
||||
return {
|
||||
start: dayjs(startDate).startOf('day').toDate(),
|
||||
end: dayjs(endDate).endOf('day').toDate()
|
||||
};
|
||||
}
|
||||
|
||||
if (!dateRange || dateRange === 'all') return null;
|
||||
|
||||
const now = dayjs();
|
||||
switch (dateRange) {
|
||||
case 'today':
|
||||
return { start: now.startOf('day').toDate(), end: now.endOf('day').toDate() };
|
||||
case 'week':
|
||||
return { start: now.startOf('week').toDate(), end: now.endOf('week').toDate() };
|
||||
case 'month':
|
||||
return { start: now.startOf('month').toDate(), end: now.endOf('month').toDate() };
|
||||
case 'quarter':
|
||||
const quarterStartMonth = Math.floor(now.month() / 3) * 3;
|
||||
return {
|
||||
start: now.month(quarterStartMonth).startOf('month').toDate(),
|
||||
end: now.month(quarterStartMonth + 2).endOf('month').toDate()
|
||||
};
|
||||
case 'year':
|
||||
return { start: now.startOf('year').toDate(), end: now.endOf('year').toDate() };
|
||||
default:
|
||||
// If it's not a known keyword, try parsing it as a number of days
|
||||
const days = parseInt(dateRange, 10);
|
||||
if (!isNaN(days)) {
|
||||
return {
|
||||
start: now.subtract(days, 'day').startOf('day').toDate(),
|
||||
end: now.endOf('day').toDate()
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async listWorkflows(page: number, limit: number, filters: any) {
|
||||
return this.listWorkflowsInternal(page, limit, filters, undefined, 'all');
|
||||
@ -952,21 +994,68 @@ export class WorkflowServiceMongo {
|
||||
const now = new Date();
|
||||
|
||||
// 1. Build Base Match Stage
|
||||
const matchStage: any = { isDraft: false };
|
||||
const matchStage: any = { isDeleted: false };
|
||||
|
||||
// Handle Draft Visibility:
|
||||
// - Allow drafts if specifically requested via status='DRAFT'
|
||||
// - Allow drafts if viewing user's own 'initiated' requests
|
||||
// - Otherwise, exclude drafts by default
|
||||
if (filters.status && filters.status.toUpperCase() === 'DRAFT') {
|
||||
matchStage.isDraft = true;
|
||||
} else if (listType === 'initiated') {
|
||||
// Initiated view shows both drafts and active requests
|
||||
// Unless a specific status filter is already applied
|
||||
if (!filters.status || filters.status === 'all') {
|
||||
matchStage.isDraft = { $in: [true, false] };
|
||||
} else {
|
||||
matchStage.isDraft = false;
|
||||
}
|
||||
} else {
|
||||
matchStage.isDraft = false;
|
||||
}
|
||||
|
||||
if (filters.search) matchStage.$text = { $search: filters.search };
|
||||
|
||||
// 1.1 Handle Lifecycle Filter (Open vs Closed)
|
||||
if (filters.lifecycle && filters.lifecycle !== 'all') {
|
||||
const lifecycle = filters.lifecycle.toLowerCase();
|
||||
if (lifecycle === 'open') {
|
||||
matchStage.workflowState = 'OPEN';
|
||||
} else if (lifecycle === 'closed') {
|
||||
matchStage.workflowState = 'CLOSED';
|
||||
}
|
||||
}
|
||||
|
||||
// 1.2 Handle Outcome Status Filter
|
||||
if (filters.status && filters.status !== 'all') {
|
||||
const status = filters.status.toUpperCase();
|
||||
if (status === 'PENDING') {
|
||||
// Pending outcome usually means in-progress in the OPEN state
|
||||
matchStage.status = { $in: ['PENDING', 'IN_PROGRESS'] };
|
||||
} else if (status === 'DRAFT') {
|
||||
matchStage.isDraft = true;
|
||||
} else if (status === 'CLOSED') {
|
||||
// "CLOSED" as a status is now deprecated in favor of Lifecycle Filter
|
||||
// But if legacy code still sends it, we map it to CLOSED state
|
||||
matchStage.workflowState = 'CLOSED';
|
||||
} else {
|
||||
matchStage.status = status;
|
||||
}
|
||||
}
|
||||
if (filters.priority && filters.priority !== 'all') matchStage.priority = filters.priority.toUpperCase();
|
||||
if (filters.templateType && filters.templateType !== 'all') matchStage.templateType = filters.templateType.toUpperCase();
|
||||
if (filters.initiator) matchStage['initiator.userId'] = filters.initiator;
|
||||
if (filters.department && filters.department !== 'all') matchStage['initiator.department'] = filters.department;
|
||||
if (filters.startDate && filters.endDate) {
|
||||
matchStage['dates.created'] = {
|
||||
|
||||
// Date Range Logic
|
||||
const range = this.parseDateRange(filters.dateRange, filters.startDate, filters.endDate);
|
||||
if (range) {
|
||||
matchStage.createdAt = {
|
||||
$gte: range.start,
|
||||
$lte: range.end
|
||||
};
|
||||
} else if (filters.startDate && filters.endDate) {
|
||||
matchStage.createdAt = {
|
||||
$gte: new Date(filters.startDate),
|
||||
$lte: new Date(filters.endDate)
|
||||
};
|
||||
@ -1044,11 +1133,12 @@ export class WorkflowServiceMongo {
|
||||
}
|
||||
});
|
||||
matchStage.$or = [
|
||||
{ 'active_step.0.approver.userId': userId }, // Check first element of array
|
||||
{ 'active_step.0.approver.userId': userId },
|
||||
{ $and: [{ 'initiator.userId': userId }, { status: 'APPROVED' }] },
|
||||
{ $and: [{ 'membership.userId': userId }, { 'membership.participantType': 'SPECTATOR' }] }
|
||||
];
|
||||
// Only show non-closed/non-rejected for "open for me" (except approved for initiator)
|
||||
// Only show non-closed for "open for me"
|
||||
matchStage.workflowState = { $ne: 'CLOSED' };
|
||||
matchStage.status = { $in: ['PENDING', 'IN_PROGRESS', 'PAUSED', 'APPROVED'] };
|
||||
} else if (listType === 'closed_by_me' && userId) {
|
||||
// Past approver or spectator AND status is CLOSED or REJECTED
|
||||
@ -1061,7 +1151,10 @@ export class WorkflowServiceMongo {
|
||||
}
|
||||
});
|
||||
matchStage['membership.userId'] = userId;
|
||||
matchStage.status = { $in: ['CLOSED', 'REJECTED'] };
|
||||
matchStage.$or = [
|
||||
{ workflowState: 'CLOSED' },
|
||||
{ workflowState: { $exists: false }, status: { $in: ['CLOSED', 'REJECTED'] } }
|
||||
];
|
||||
}
|
||||
|
||||
// CRITICAL: Add match stage AFTER lookups so active_step and membership arrays exist
|
||||
@ -1069,6 +1162,47 @@ export class WorkflowServiceMongo {
|
||||
|
||||
// 3. Deep Filters (Approver Name, Level Status)
|
||||
if (filters.approverName) {
|
||||
const approverRegex = { $regex: filters.approverName, $options: 'i' };
|
||||
const approverMatch = {
|
||||
$or: [
|
||||
{ 'approver.name': approverRegex },
|
||||
{ 'approver.userId': filters.approverName }
|
||||
]
|
||||
};
|
||||
|
||||
if (filters.approverType === 'current') {
|
||||
// Filter by CURRENT level approver name or ID
|
||||
pipeline.push(
|
||||
{
|
||||
$lookup: {
|
||||
from: 'approval_levels',
|
||||
let: { reqId: "$requestId", currLvl: "$currentLevel" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ["$requestId", "$$reqId"] },
|
||||
{ $eq: ["$levelNumber", "$$currLvl"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'current_step_filter'
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ 'current_step_filter.approver.name': approverRegex },
|
||||
{ 'current_step_filter.approver.userId': filters.approverName }
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Search in ANY level (approverType === 'any' or undefined)
|
||||
pipeline.push(
|
||||
{
|
||||
$lookup: {
|
||||
@ -1078,9 +1212,17 @@ export class WorkflowServiceMongo {
|
||||
as: 'matches_approvers'
|
||||
}
|
||||
},
|
||||
{ $match: { 'matches_approvers.approver.name': { $regex: filters.approverName, $options: 'i' } } }
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ 'matches_approvers.approver.name': approverRegex },
|
||||
{ 'matches_approvers.approver.userId': filters.approverName }
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.levelStatus && filters.levelNumber) {
|
||||
pipeline.push(
|
||||
@ -1096,8 +1238,46 @@ export class WorkflowServiceMongo {
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.slaCompliance && filters.slaCompliance !== 'all') {
|
||||
pipeline.push({
|
||||
$lookup: {
|
||||
from: 'approval_levels',
|
||||
let: { reqId: "$requestId", currLevelId: "$currentLevelId", currLvl: "$currentLevel" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: {
|
||||
$and: [
|
||||
{ $eq: ["$requestId", "$$reqId"] },
|
||||
{
|
||||
$or: [
|
||||
{ $eq: ["$levelId", "$$currLevelId"] },
|
||||
{
|
||||
$and: [
|
||||
{ $eq: [{ $type: "$$currLevelId" }, "missing"] },
|
||||
{ $eq: ["$levelNumber", "$$currLvl"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
as: 'active_sla_step'
|
||||
}
|
||||
});
|
||||
|
||||
if (filters.slaCompliance === 'breached') {
|
||||
pipeline.push({ $match: { 'active_sla_step.tat.isBreached': true } });
|
||||
} else if (filters.slaCompliance === 'on_track') {
|
||||
pipeline.push({ $match: { 'active_sla_step.tat.isBreached': false } });
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Sort & Pagination
|
||||
const sortField = sortBy || 'dates.created';
|
||||
const sortField = sortBy || 'createdAt';
|
||||
const sortDir = sortOrder?.toLowerCase() === 'asc' ? 1 : -1;
|
||||
|
||||
pipeline.push(
|
||||
@ -1127,6 +1307,7 @@ export class WorkflowServiceMongo {
|
||||
title: 1,
|
||||
description: 1,
|
||||
status: 1,
|
||||
workflowState: 1,
|
||||
priority: 1,
|
||||
workflowType: 1,
|
||||
templateType: 1,
|
||||
@ -1217,10 +1398,8 @@ export class WorkflowServiceMongo {
|
||||
deadline: deadline || null,
|
||||
isPaused: !!pauseInfo,
|
||||
status: currentStep.tat?.isBreached ? 'breached' : 'on_track',
|
||||
remainingText: `${Math.floor(Math.max(0, assignedHours - elapsedHours))}h ${Math.round((Math.max(0, assignedHours - elapsedHours) % 1) * 60)}m`,
|
||||
elapsedText: elapsedHours >= 24
|
||||
? `${Math.floor(elapsedHours / 24)}d ${Math.floor(elapsedHours % 24)}h ${Math.round((elapsedHours % 1) * 60)}m`
|
||||
: `${Math.floor(elapsedHours)}h ${Math.round((elapsedHours % 1) * 60)}m`
|
||||
remainingText: formatTime(Math.max(0, assignedHours - elapsedHours)),
|
||||
elapsedText: formatTime(elapsedHours)
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[listWorkflows] TAT calculation error:', error);
|
||||
@ -1259,10 +1438,8 @@ export class WorkflowServiceMongo {
|
||||
deadline: workflowDeadline,
|
||||
isPaused: result.isPaused || false,
|
||||
status: requestPercentageUsed >= 100 ? 'breached' : 'on_track',
|
||||
remainingText: `${Math.floor(requestRemainingHours)}h ${Math.round((requestRemainingHours % 1) * 60)}m`,
|
||||
elapsedText: requestElapsedHours >= 24
|
||||
? `${Math.floor(requestElapsedHours / 24)}d ${Math.floor(requestElapsedHours % 24)}h ${Math.round((requestElapsedHours % 1) * 60)}m`
|
||||
: `${Math.floor(requestElapsedHours)}h ${Math.round((requestElapsedHours % 1) * 60)}m`
|
||||
remainingText: formatTime(requestRemainingHours),
|
||||
elapsedText: formatTime(requestElapsedHours)
|
||||
};
|
||||
|
||||
// Add currentApprover info (from currentStep if available)
|
||||
@ -1296,7 +1473,7 @@ export class WorkflowServiceMongo {
|
||||
|
||||
// 7. Total Count (Optimized)
|
||||
let total = 0;
|
||||
const needsAggCount = !!(filters.approverName || (filters.levelStatus) || listType === 'my_requests' || listType === 'participant' || listType === 'open_for_me' || listType === 'closed_by_me');
|
||||
const needsAggCount = !!(filters.approverName || filters.levelStatus || filters.slaCompliance || listType === 'my_requests' || listType === 'participant' || listType === 'open_for_me' || listType === 'closed_by_me');
|
||||
|
||||
if (needsAggCount) {
|
||||
const countPipeline = [...pipeline].filter(s => !s.$sort && !s.$skip && !s.$limit && !s.$project && !s.$lookup || (s.$lookup && (s.$lookup.from === 'participants' || s.$lookup.from === 'approval_levels')));
|
||||
@ -1446,6 +1623,7 @@ export class WorkflowServiceMongo {
|
||||
description: requestObj.description,
|
||||
priority: requestObj.priority,
|
||||
status: requestObj.status,
|
||||
workflowState: requestObj.workflowState || 'OPEN',
|
||||
currentLevel: requestObj.currentLevel,
|
||||
totalLevels: requestObj.totalLevels,
|
||||
totalTatHours: requestObj.totalTatHours?.toString() || '0.00',
|
||||
@ -1575,10 +1753,8 @@ export class WorkflowServiceMongo {
|
||||
deadline: levelObj.tat?.endTime || null,
|
||||
isPaused: levelObj.paused?.isPaused || false,
|
||||
status: levelObj.tat?.isBreached ? 'breached' : 'on_track',
|
||||
remainingText: `${Math.floor(remainingHours)}h ${Math.round((remainingHours % 1) * 60)}m`,
|
||||
elapsedText: elapsedHours >= 24
|
||||
? `${Math.floor(elapsedHours / 24)}d ${Math.floor(elapsedHours % 24)}h ${Math.round((elapsedHours % 1) * 60)}m`
|
||||
: `${Math.floor(elapsedHours)}h ${Math.round((elapsedHours % 1) * 60)}m`
|
||||
remainingText: formatTime(remainingHours),
|
||||
elapsedText: formatTime(elapsedHours)
|
||||
} : null
|
||||
};
|
||||
}));
|
||||
@ -1618,10 +1794,8 @@ export class WorkflowServiceMongo {
|
||||
status: requestPercentageUsed >= 100 ? 'breached' : 'on_track',
|
||||
isPaused: requestObj.isPaused || false, // Use requestObj.isPaused for workflow level
|
||||
deadline: workflowDeadline,
|
||||
elapsedText: requestElapsedHours >= 24
|
||||
? `${Math.floor(requestElapsedHours / 24)}d ${Math.floor(requestElapsedHours % 24)}h ${Math.round((requestElapsedHours % 1) * 60)}m`
|
||||
: `${Math.floor(requestElapsedHours)}h ${Math.round((requestElapsedHours % 1) * 60)}m`,
|
||||
remainingText: `${Math.floor(requestRemainingHours)}h ${Math.round((requestRemainingHours % 1) * 60)}m`
|
||||
elapsedText: formatTime(requestElapsedHours),
|
||||
remainingText: formatTime(requestRemainingHours)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getWorkflowDetails] Request-level TAT calculation error:', error);
|
||||
@ -1649,8 +1823,8 @@ export class WorkflowServiceMongo {
|
||||
status: currentApprovalData.tatBreached ? 'breached' : 'on_track', // Corrected to 'on_track'
|
||||
isPaused: currentApprovalData.isPaused,
|
||||
deadline: currentApprovalData.levelEndTime || null,
|
||||
elapsedText: `${Math.floor(currentApprovalData.elapsedHours)}h ${Math.round((currentApprovalData.elapsedHours % 1) * 60)}m`,
|
||||
remainingText: `${Math.floor(currentApprovalData.remainingHours)}h ${Math.round((currentApprovalData.remainingHours % 1) * 60)}m`
|
||||
elapsedText: formatTime(currentApprovalData.elapsedHours),
|
||||
remainingText: formatTime(currentApprovalData.remainingHours)
|
||||
} : null)
|
||||
};
|
||||
|
||||
@ -1712,6 +1886,7 @@ export class WorkflowServiceMongo {
|
||||
|
||||
workflow.isDraft = false;
|
||||
workflow.status = 'PENDING';
|
||||
workflow.workflowState = 'OPEN';
|
||||
workflow.submissionDate = new Date();
|
||||
await workflow.save();
|
||||
|
||||
|
||||
@ -9,8 +9,7 @@ export enum WorkflowStatus {
|
||||
APPROVED = 'APPROVED',
|
||||
REJECTED = 'REJECTED',
|
||||
CLOSED = 'CLOSED',
|
||||
PAUSED = 'PAUSED',
|
||||
CANCELLED = 'CANCELLED'
|
||||
PAUSED = 'PAUSED'
|
||||
}
|
||||
|
||||
export enum ApprovalStatus {
|
||||
|
||||
@ -540,21 +540,6 @@ export async function calculateSLAStatus(
|
||||
status = 'approaching';
|
||||
}
|
||||
|
||||
// Format remaining time
|
||||
const formatTime = (hours: number) => {
|
||||
if (hours <= 0) return '0h';
|
||||
const days = Math.floor(hours / 8); // 8 working hours per day
|
||||
const remainingHrs = Math.floor(hours % 8);
|
||||
const minutes = Math.round((hours % 1) * 60);
|
||||
|
||||
if (days > 0) {
|
||||
return minutes > 0
|
||||
? `${days}d ${remainingHrs}h ${minutes}m`
|
||||
: `${days}d ${remainingHrs}h`;
|
||||
}
|
||||
return minutes > 0 ? `${remainingHrs}h ${minutes}m` : `${remainingHrs}h`;
|
||||
};
|
||||
|
||||
return {
|
||||
elapsedHours: Math.round(elapsedHours * 100) / 100,
|
||||
remainingHours: Math.round(remainingHours * 100) / 100,
|
||||
@ -567,6 +552,27 @@ export async function calculateSLAStatus(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format hours into "X day(s) Y h Z min" string
|
||||
* @param hours - Number of hours
|
||||
* @returns Formatted string
|
||||
*/
|
||||
export const formatTime = (hours: number): string => {
|
||||
if (hours <= 0) return '0h';
|
||||
const days = Math.floor(hours / 8); // 8 working hours per day
|
||||
const remainingHrs = Math.floor(hours % 8);
|
||||
const minutes = Math.round((hours % 1) * 60);
|
||||
|
||||
const dayLabel = days === 1 ? 'day' : 'days';
|
||||
|
||||
if (days > 0) {
|
||||
return minutes > 0
|
||||
? `${days} ${dayLabel} ${remainingHrs}h ${minutes}min`
|
||||
: `${days} ${dayLabel} ${remainingHrs}h`;
|
||||
}
|
||||
return minutes > 0 ? `${remainingHrs}h ${minutes}min` : `${remainingHrs}h`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate elapsed working hours between two dates
|
||||
* Uses minute-by-minute precision to accurately count only working time
|
||||
|
||||
85
verify-dashboard-filtering.ts
Normal file
85
verify-dashboard-filtering.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import mongoose from 'mongoose';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { dashboardMongoService } from './src/services/dashboard.service';
|
||||
import { UserModel } from './src/models/mongoose/User.schema';
|
||||
import { WorkflowRequestModel } from './src/models/mongoose/WorkflowRequest.schema';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function verify() {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGO_URI!);
|
||||
console.log('Connected to MongoDB');
|
||||
|
||||
// 1. Find an Admin and a Regular User
|
||||
const adminUser = await UserModel.findOne({ role: 'ADMIN', isActive: true });
|
||||
const regularUser = await UserModel.findOne({ role: 'USER', isActive: true });
|
||||
|
||||
if (!adminUser || !regularUser) {
|
||||
console.error('Could not find both Admin and Regular User for testing');
|
||||
// List some users to help debug
|
||||
const users = await UserModel.find({ isActive: true }).limit(5);
|
||||
console.log('Available users:', users.map(u => ({ email: u.email, role: u.role })));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nTesting with:`);
|
||||
console.log(`Admin User: ${adminUser.email} (${adminUser.userId})`);
|
||||
console.log(`Regular User: ${regularUser.email} (${regularUser.userId})`);
|
||||
|
||||
// 2. Test getUpcomingDeadlines
|
||||
console.log('\n--- Testing Upcoming Deadlines ---');
|
||||
const adminDeadlines = await dashboardMongoService.getUpcomingDeadlines(adminUser.userId, 1, 10, false);
|
||||
const userDeadlines = await dashboardMongoService.getUpcomingDeadlines(regularUser.userId, 1, 10, true);
|
||||
|
||||
console.log(`Admin count: ${adminDeadlines.totalRecords}`);
|
||||
console.log(`User count: ${userDeadlines.totalRecords}`);
|
||||
|
||||
if (userDeadlines.deadlines.length > 0) {
|
||||
const first = userDeadlines.deadlines[0];
|
||||
console.log(`First User Deadline Approver: ${first.approverEmail} (User Email: ${regularUser.email})`);
|
||||
if (first.approverEmail !== regularUser.email) {
|
||||
console.warn('WARNING: Regular user sees a deadline they are not the approver for! (Wait, are they just a participant?)');
|
||||
// Check if they are actually the current approver
|
||||
console.log('Actual Approver in data:', first.approverEmail);
|
||||
} else {
|
||||
console.log('SUCCESS: User only sees their own deadlines.');
|
||||
}
|
||||
} else {
|
||||
console.log('No deadlines found for regular user.');
|
||||
}
|
||||
|
||||
// 3. Test getCriticalRequests
|
||||
console.log('\n--- Testing Critical Requests ---');
|
||||
const adminCritical = await dashboardMongoService.getCriticalRequests(adminUser.userId, 1, 10, false);
|
||||
const userCritical = await dashboardMongoService.getCriticalRequests(regularUser.userId, 1, 10, true);
|
||||
|
||||
console.log(`Admin count: ${adminCritical.totalRecords}`);
|
||||
console.log(`User count: ${userCritical.totalRecords}`);
|
||||
|
||||
// 4. Test getRecentActivity
|
||||
console.log('\n--- Testing Recent Activity ---');
|
||||
const adminActivity = await dashboardMongoService.getRecentActivity(adminUser.userId, 1, 10, false);
|
||||
const userActivity = await dashboardMongoService.getRecentActivity(regularUser.userId, 1, 10, true);
|
||||
|
||||
console.log(`Admin count: ${adminActivity.totalRecords}`);
|
||||
console.log(`User count: ${userActivity.totalRecords}`);
|
||||
|
||||
// 5. Test getRequestStats
|
||||
console.log('\n--- Testing Request Stats ---');
|
||||
// userId, dateRange, startDate, endDate, status, priority, templateType, department, initiator, approver, approverType, search, slaCompliance, viewAsUser
|
||||
const adminStats = await dashboardMongoService.getRequestStats(adminUser.userId, 'all', undefined, undefined, 'all', 'all', 'all', 'all', 'all', 'all', 'any', undefined, 'all', false);
|
||||
const userStats = await dashboardMongoService.getRequestStats(regularUser.userId, 'all', undefined, undefined, 'all', 'all', 'all', 'all', 'all', 'all', 'any', undefined, 'all', true);
|
||||
|
||||
console.log(`Admin Total Requests: ${adminStats.totalRequests}`);
|
||||
console.log(`User Total (Involved): ${userStats.totalRequests}`);
|
||||
|
||||
console.log('\nVerification Complete');
|
||||
} catch (error) {
|
||||
console.error('Verification failed:', error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
verify();
|
||||
Loading…
Reference in New Issue
Block a user