242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
import { WorkflowTemplate } from '../models/WorkflowTemplate';
|
|
import { WorkflowRequest } from '../models/WorkflowRequest';
|
|
import { ApprovalLevel } from '../models/ApprovalLevel';
|
|
import { TemplateFieldResolver, FormStepConfig } from './templateFieldResolver.service';
|
|
import logger from '../utils/logger';
|
|
|
|
/**
|
|
* Enhanced Template Service
|
|
* Handles template-based workflow operations with dynamic form configuration
|
|
*/
|
|
export class EnhancedTemplateService {
|
|
private fieldResolver = new TemplateFieldResolver();
|
|
|
|
/**
|
|
* Get form configuration for a template with resolved user references
|
|
*/
|
|
async getFormConfig(
|
|
templateId: string,
|
|
requestId?: string,
|
|
currentUserId?: string
|
|
): Promise<FormStepConfig[]> {
|
|
try {
|
|
const template = await WorkflowTemplate.findByPk(templateId);
|
|
if (!template) {
|
|
throw new Error('Template not found');
|
|
}
|
|
|
|
const stepsConfig = (template.formStepsConfig || []) as FormStepConfig[];
|
|
|
|
// If request exists, resolve user references
|
|
if (requestId && currentUserId) {
|
|
const request = await WorkflowRequest.findByPk(requestId, {
|
|
include: [{ model: ApprovalLevel, as: 'approvalLevels' }]
|
|
});
|
|
if (request) {
|
|
return await this.resolveStepsWithUserData(stepsConfig, request, currentUserId);
|
|
}
|
|
}
|
|
|
|
return stepsConfig;
|
|
} catch (error) {
|
|
logger.error('[EnhancedTemplateService] Error getting form config:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve user references in all steps
|
|
*/
|
|
private async resolveStepsWithUserData(
|
|
steps: FormStepConfig[],
|
|
request: WorkflowRequest,
|
|
currentUserId: string
|
|
): Promise<FormStepConfig[]> {
|
|
try {
|
|
// Get all approvers for context
|
|
const approvers = await ApprovalLevel.findAll({
|
|
where: { requestId: request.requestId }
|
|
});
|
|
const approverMap = new Map(
|
|
approvers.map(a => [a.levelNumber, a])
|
|
);
|
|
|
|
const resolvedSteps = await Promise.all(
|
|
steps.map(async (step) => {
|
|
const resolvedFields = await this.fieldResolver.resolveUserReferences(
|
|
step,
|
|
request,
|
|
currentUserId,
|
|
{
|
|
currentLevel: request.currentLevel,
|
|
approvers: approverMap
|
|
}
|
|
);
|
|
|
|
// Merge resolved values into field defaults
|
|
const enrichedFields = step.fields.map(field => ({
|
|
...field,
|
|
defaultValue: resolvedFields[field.fieldId] || field.defaultValue
|
|
}));
|
|
|
|
return {
|
|
...step,
|
|
fields: enrichedFields
|
|
};
|
|
})
|
|
);
|
|
|
|
return resolvedSteps;
|
|
} catch (error) {
|
|
logger.error('[EnhancedTemplateService] Error resolving steps:', error);
|
|
return steps; // Return original steps on error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate and save form data for a step
|
|
*/
|
|
async saveStepData(
|
|
templateId: string,
|
|
requestId: string,
|
|
stepNumber: number,
|
|
formData: Record<string, any>,
|
|
userId: string
|
|
): Promise<void> {
|
|
try {
|
|
const template = await WorkflowTemplate.findByPk(templateId);
|
|
if (!template) {
|
|
throw new Error('Template not found');
|
|
}
|
|
|
|
const stepsConfig = (template.formStepsConfig || []) as FormStepConfig[];
|
|
const stepConfig = stepsConfig.find(s => s.stepNumber === stepNumber);
|
|
|
|
if (!stepConfig) {
|
|
throw new Error(`Step ${stepNumber} not found in template`);
|
|
}
|
|
|
|
// Validate required fields
|
|
this.validateStepData(stepConfig, formData);
|
|
|
|
// Save to template-specific storage
|
|
await this.saveToTemplateStorage(template.workflowType, requestId, stepNumber, formData);
|
|
} catch (error) {
|
|
logger.error('[EnhancedTemplateService] Error saving step data:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate step data against configuration
|
|
*/
|
|
private validateStepData(stepConfig: FormStepConfig, formData: Record<string, any>): void {
|
|
for (const field of stepConfig.fields) {
|
|
if (field.required && !formData[field.fieldId]) {
|
|
throw new Error(`Field ${field.label} is required`);
|
|
}
|
|
|
|
// Apply validation rules
|
|
if (field.validation && formData[field.fieldId]) {
|
|
const value = formData[field.fieldId];
|
|
if (field.validation.min !== undefined && value < field.validation.min) {
|
|
throw new Error(`${field.label} must be at least ${field.validation.min}`);
|
|
}
|
|
if (field.validation.max !== undefined && value > field.validation.max) {
|
|
throw new Error(`${field.label} must be at most ${field.validation.max}`);
|
|
}
|
|
if (field.validation.pattern) {
|
|
const regex = new RegExp(field.validation.pattern);
|
|
if (!regex.test(String(value))) {
|
|
throw new Error(`${field.label} format is invalid`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save to template-specific storage based on workflow type
|
|
*/
|
|
private async saveToTemplateStorage(
|
|
workflowType: string,
|
|
requestId: string,
|
|
stepNumber: number,
|
|
formData: Record<string, any>
|
|
): Promise<void> {
|
|
switch (workflowType) {
|
|
case 'CLAIM_MANAGEMENT':
|
|
await this.saveClaimManagementStepData(requestId, stepNumber, formData);
|
|
break;
|
|
default:
|
|
// Generic storage for custom templates
|
|
logger.warn(`[EnhancedTemplateService] No specific storage handler for workflow type: ${workflowType}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save claim management step data
|
|
*/
|
|
private async saveClaimManagementStepData(
|
|
requestId: string,
|
|
stepNumber: number,
|
|
formData: Record<string, any>
|
|
): Promise<void> {
|
|
const { DealerClaimDetails } = await import('../models/DealerClaimDetails');
|
|
const { DealerProposalDetails } = await import('../models/DealerProposalDetails');
|
|
const { DealerCompletionDetails } = await import('../models/DealerCompletionDetails');
|
|
|
|
switch (stepNumber) {
|
|
case 1:
|
|
// Save to dealer_claim_details
|
|
await DealerClaimDetails.upsert({
|
|
requestId,
|
|
activityName: formData.activity_name,
|
|
activityType: formData.activity_type,
|
|
dealerCode: formData.dealer_code,
|
|
dealerName: formData.dealer_name,
|
|
dealerEmail: formData.dealer_email,
|
|
dealerPhone: formData.dealer_phone,
|
|
dealerAddress: formData.dealer_address,
|
|
activityDate: formData.activity_date,
|
|
location: formData.location,
|
|
periodStartDate: formData.period_start_date,
|
|
periodEndDate: formData.period_end_date,
|
|
estimatedBudget: formData.estimated_budget,
|
|
});
|
|
break;
|
|
case 2:
|
|
// Save to dealer_proposal_details
|
|
await DealerProposalDetails.upsert({
|
|
requestId,
|
|
costBreakup: formData.cost_breakup,
|
|
totalEstimatedBudget: formData.total_estimated_budget,
|
|
timelineMode: formData.timeline_mode,
|
|
expectedCompletionDate: formData.expected_completion_date,
|
|
expectedCompletionDays: formData.expected_completion_days,
|
|
dealerComments: formData.dealer_comments,
|
|
proposalDocumentPath: formData.proposal_document_path,
|
|
proposalDocumentUrl: formData.proposal_document_url,
|
|
submittedAt: new Date(),
|
|
});
|
|
break;
|
|
case 5:
|
|
// Save to dealer_completion_details
|
|
await DealerCompletionDetails.upsert({
|
|
requestId,
|
|
activityCompletionDate: formData.activity_completion_date,
|
|
numberOfParticipants: formData.number_of_participants,
|
|
closedExpenses: formData.closed_expenses,
|
|
totalClosedExpenses: formData.total_closed_expenses,
|
|
completionDocuments: formData.completion_documents,
|
|
activityPhotos: formData.activity_photos,
|
|
submittedAt: new Date(),
|
|
});
|
|
break;
|
|
default:
|
|
logger.warn(`[EnhancedTemplateService] No storage handler for claim management step ${stepNumber}`);
|
|
}
|
|
}
|
|
}
|
|
|