288 lines
8.3 KiB
TypeScript
288 lines
8.3 KiB
TypeScript
import { WorkflowRequest } from '../models/WorkflowRequest';
|
|
import { ApprovalLevel } from '../models/ApprovalLevel';
|
|
import { User } from '../models/User';
|
|
import { Participant } from '../models/Participant';
|
|
import logger from '../utils/logger';
|
|
|
|
/**
|
|
* Interface for user reference configuration in form fields
|
|
*/
|
|
export interface UserReference {
|
|
role: 'initiator' | 'dealer' | 'approver' | 'team_lead' | 'department_lead' | 'current_approver' | 'previous_approver';
|
|
level?: number; // For approver: which approval level
|
|
field: 'name' | 'email' | 'phone' | 'department' | 'employee_id' | 'all'; // Which user field to reference
|
|
autoPopulate: boolean; // Auto-fill from user data
|
|
editable: boolean; // Can user edit the auto-populated value
|
|
}
|
|
|
|
/**
|
|
* Interface for form step configuration
|
|
*/
|
|
export interface FormStepConfig {
|
|
stepNumber: number;
|
|
stepName: string;
|
|
stepDescription?: string;
|
|
fields: FormFieldConfig[];
|
|
userReferences?: UserReferenceConfig[];
|
|
}
|
|
|
|
export interface FormFieldConfig {
|
|
fieldId: string;
|
|
fieldType: string;
|
|
label: string;
|
|
required: boolean;
|
|
defaultValue?: any;
|
|
userReference?: UserReference;
|
|
}
|
|
|
|
export interface UserReferenceConfig {
|
|
role: string;
|
|
captureFields: string[];
|
|
autoPopulateFrom: 'workflow' | 'user_profile' | 'approval_level';
|
|
allowOverride: boolean;
|
|
}
|
|
|
|
/**
|
|
* Service to resolve user references in template forms
|
|
*/
|
|
export class TemplateFieldResolver {
|
|
/**
|
|
* Resolve user reference fields in a step
|
|
*/
|
|
async resolveUserReferences(
|
|
stepConfig: FormStepConfig,
|
|
request: WorkflowRequest,
|
|
currentUserId: string,
|
|
context?: {
|
|
currentLevel?: number;
|
|
approvers?: Map<number, ApprovalLevel>;
|
|
}
|
|
): Promise<Record<string, any>> {
|
|
const resolvedFields: Record<string, any> = {};
|
|
|
|
try {
|
|
for (const field of stepConfig.fields) {
|
|
if (field.userReference) {
|
|
const userData = await this.getUserDataForReference(
|
|
field.userReference,
|
|
request,
|
|
currentUserId,
|
|
context
|
|
);
|
|
|
|
if (field.userReference.autoPopulate && userData) {
|
|
resolvedFields[field.fieldId] = this.extractUserField(
|
|
userData,
|
|
field.userReference.field
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('[TemplateFieldResolver] Error resolving user references:', error);
|
|
}
|
|
|
|
return resolvedFields;
|
|
}
|
|
|
|
/**
|
|
* Get user data based on reference configuration
|
|
*/
|
|
private async getUserDataForReference(
|
|
userRef: UserReference,
|
|
request: WorkflowRequest,
|
|
currentUserId: string,
|
|
context?: any
|
|
): Promise<User | null> {
|
|
try {
|
|
switch (userRef.role) {
|
|
case 'initiator':
|
|
return await User.findByPk(request.initiatorId);
|
|
|
|
case 'dealer':
|
|
// Get dealer from participants
|
|
const dealerParticipant = await Participant.findOne({
|
|
where: {
|
|
requestId: request.requestId,
|
|
participantType: 'DEALER' as any,
|
|
isActive: true
|
|
},
|
|
include: [{ model: User, as: 'user' }]
|
|
});
|
|
return dealerParticipant?.user || null;
|
|
|
|
case 'approver':
|
|
if (userRef.level && context?.approvers) {
|
|
const approverLevel = context.approvers.get(userRef.level);
|
|
if (approverLevel?.approverId) {
|
|
return await User.findByPk(approverLevel.approverId);
|
|
}
|
|
}
|
|
// Fallback to current approver
|
|
const currentLevel = await ApprovalLevel.findOne({
|
|
where: {
|
|
requestId: request.requestId,
|
|
levelNumber: context?.currentLevel || request.currentLevel,
|
|
status: 'PENDING' as any
|
|
}
|
|
});
|
|
if (currentLevel?.approverId) {
|
|
return await User.findByPk(currentLevel.approverId);
|
|
}
|
|
return null;
|
|
|
|
case 'team_lead':
|
|
// Find team lead based on initiator's manager
|
|
const initiator = await User.findByPk(request.initiatorId);
|
|
if (initiator?.manager) {
|
|
return await User.findOne({
|
|
where: {
|
|
email: initiator.manager,
|
|
role: 'MANAGEMENT' as any
|
|
}
|
|
});
|
|
}
|
|
return null;
|
|
|
|
case 'department_lead':
|
|
const initiatorUser = await User.findByPk(request.initiatorId);
|
|
if (initiatorUser?.department) {
|
|
return await User.findOne({
|
|
where: {
|
|
department: initiatorUser.department,
|
|
role: 'MANAGEMENT' as any
|
|
},
|
|
order: [['created_at', 'DESC']]
|
|
});
|
|
}
|
|
return null;
|
|
|
|
case 'current_approver':
|
|
const currentApprovalLevel = await ApprovalLevel.findOne({
|
|
where: {
|
|
requestId: request.requestId,
|
|
status: 'PENDING' as any
|
|
},
|
|
order: [['level_number', 'ASC']]
|
|
});
|
|
if (currentApprovalLevel?.approverId) {
|
|
return await User.findByPk(currentApprovalLevel.approverId);
|
|
}
|
|
return null;
|
|
|
|
case 'previous_approver':
|
|
const previousLevel = request.currentLevel - 1;
|
|
if (previousLevel > 0) {
|
|
const previousApprovalLevel = await ApprovalLevel.findOne({
|
|
where: {
|
|
requestId: request.requestId,
|
|
levelNumber: previousLevel
|
|
}
|
|
});
|
|
if (previousApprovalLevel?.approverId) {
|
|
return await User.findByPk(previousApprovalLevel.approverId);
|
|
}
|
|
}
|
|
return null;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
logger.error(`[TemplateFieldResolver] Error getting user data for role ${userRef.role}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract specific field from user data
|
|
*/
|
|
private extractUserField(user: User, field: string): any {
|
|
if (!user) return null;
|
|
|
|
switch (field) {
|
|
case 'name':
|
|
return user.displayName || `${user.firstName || ''} ${user.lastName || ''}`.trim();
|
|
case 'email':
|
|
return user.email;
|
|
case 'phone':
|
|
return user.phone || user.mobilePhone;
|
|
case 'department':
|
|
return user.department;
|
|
case 'employee_id':
|
|
return user.employeeId;
|
|
case 'all':
|
|
return {
|
|
name: user.displayName || `${user.firstName || ''} ${user.lastName || ''}`.trim(),
|
|
email: user.email,
|
|
phone: user.phone || user.mobilePhone,
|
|
department: user.department,
|
|
employeeId: user.employeeId
|
|
};
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve dynamic approver based on configuration
|
|
*/
|
|
async resolveDynamicApprover(
|
|
level: number,
|
|
config: any, // DynamicApproverConfig
|
|
request: WorkflowRequest
|
|
): Promise<User | null> {
|
|
if (!config?.enabled || !config?.approverSelection?.dynamicRules) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const rule = config.approverSelection.dynamicRules.find((r: any) => r.level === level);
|
|
if (!rule) return null;
|
|
|
|
const criteria = rule.selectionCriteria;
|
|
|
|
switch (criteria.type) {
|
|
case 'role':
|
|
return await User.findOne({
|
|
where: {
|
|
role: criteria.value as any
|
|
},
|
|
order: [['created_at', 'DESC']]
|
|
});
|
|
|
|
case 'department':
|
|
const initiator = await User.findByPk(request.initiatorId);
|
|
const deptValue = criteria.value?.replace('${initiator.department}', initiator?.department || '') || initiator?.department;
|
|
if (deptValue) {
|
|
return await User.findOne({
|
|
where: {
|
|
department: deptValue,
|
|
role: 'MANAGEMENT' as any
|
|
}
|
|
});
|
|
}
|
|
return null;
|
|
|
|
case 'manager':
|
|
const initiatorUser = await User.findByPk(request.initiatorId);
|
|
if (initiatorUser?.manager) {
|
|
return await User.findOne({
|
|
where: {
|
|
email: initiatorUser.manager
|
|
}
|
|
});
|
|
}
|
|
return null;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
logger.error('[TemplateFieldResolver] Error resolving dynamic approver:', error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|