Re_Backend/docs/DYNAMIC_TEMPLATE_SYSTEM.md

1198 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Dynamic Multi-Step Template System
## Enhanced Admin Template Configuration with Dynamic User Field References
## Overview
This document outlines an enhanced template system that allows admins to:
1. **Create multi-step forms** (not just single-step)
2. **Dynamically reference user types** (initiator, dealer, approvers, team lead, etc.)
3. **Configure user details capture** per step
4. **Auto-populate user fields** based on workflow participants
---
## Enhanced Database Schema
### 1. Enhanced `workflow_templates` Table
```sql
-- Migration: 20251210-enhance-workflow-templates.ts
ALTER TABLE workflow_templates
ADD COLUMN IF NOT EXISTS form_steps_config JSONB,
ADD COLUMN IF NOT EXISTS user_field_mappings JSONB,
ADD COLUMN IF NOT EXISTS dynamic_approver_config JSONB;
-- Example structure:
-- form_steps_config: Array of step definitions
-- user_field_mappings: Maps user roles to field references
-- dynamic_approver_config: Configuration for dynamic approver selection
```
### 2. Template Form Steps Configuration
```typescript
// Structure for form_steps_config JSONB
interface FormStepConfig {
stepNumber: number;
stepName: string;
stepDescription?: string;
fields: FormFieldConfig[];
validationRules?: ValidationRule[];
conditionalLogic?: ConditionalLogic[];
userReferences?: UserReferenceConfig[];
}
interface FormFieldConfig {
fieldId: string; // Unique identifier
fieldType: 'text' | 'textarea' | 'select' | 'date' | 'number' | 'file' | 'user_picker' | 'dealer_picker' | 'approver_picker';
label: string;
placeholder?: string;
required: boolean;
defaultValue?: any;
options?: Array<{ value: string; label: string }>; // For select fields
validation?: {
min?: number;
max?: number;
pattern?: string;
customValidator?: string;
};
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
};
visibility?: {
dependsOn?: string; // Field ID this depends on
condition: 'equals' | 'not_equals' | 'contains' | 'greater_than' | 'less_than';
value: any;
};
}
interface UserReferenceConfig {
role: string;
captureFields: string[]; // Which fields to capture: ['name', 'email', 'phone', 'department']
autoPopulateFrom: 'workflow' | 'user_profile' | 'approval_level'; // Source of user data
allowOverride: boolean; // Can user override auto-populated values
}
```
### 3. Dynamic Approver Configuration
```typescript
interface DynamicApproverConfig {
enabled: boolean;
approverSelection: {
mode: 'static' | 'dynamic' | 'role_based' | 'department_based';
staticApprovers?: Array<{
level: number;
userId?: string;
userEmail?: string;
role?: string;
}>;
dynamicRules?: Array<{
level: number;
selectionCriteria: {
type: 'role' | 'department' | 'manager' | 'custom';
value: string;
fallback?: string;
};
captureUserDetails: boolean; // Capture approver details in form
captureFields: string[]; // Which fields to capture
}>;
};
approverDetailsInSteps: Array<{
stepNumber: number;
approverLevel: number;
captureFields: string[];
displayMode: 'readonly' | 'editable' | 'hidden';
}>;
}
```
---
## Example Template Configurations
### Example 1: Claim Management Template (8-Step)
```json
{
"template_name": "Dealer Claim Management",
"template_code": "CLAIM_MANAGEMENT",
"workflow_type": "CLAIM_MANAGEMENT",
"form_steps_config": [
{
"stepNumber": 1,
"stepName": "Basic Information",
"stepDescription": "Capture activity and dealer information",
"fields": [
{
"fieldId": "activity_name",
"fieldType": "text",
"label": "Activity Name",
"required": true,
"validation": {
"min": 3,
"max": 500
}
},
{
"fieldId": "activity_type",
"fieldType": "select",
"label": "Activity Type",
"required": true,
"options": [
{ "value": "RIDERS_MANIA", "label": "Riders Mania Claims" },
{ "value": "MARKETING_COST", "label": "Marketing Cost Bike to Vendor" },
{ "value": "MEDIA_BIKE_SERVICE", "label": "Media Bike Service" }
]
},
{
"fieldId": "dealer_code",
"fieldType": "dealer_picker",
"label": "Dealer Code",
"required": true,
"userReference": {
"role": "dealer",
"field": "all",
"autoPopulate": false,
"editable": false
}
},
{
"fieldId": "dealer_name",
"fieldType": "text",
"label": "Dealer Name",
"required": true,
"userReference": {
"role": "dealer",
"field": "name",
"autoPopulate": true,
"editable": false
}
},
{
"fieldId": "dealer_email",
"fieldType": "text",
"label": "Dealer Email",
"required": true,
"userReference": {
"role": "dealer",
"field": "email",
"autoPopulate": true,
"editable": true
}
},
{
"fieldId": "initiator_name",
"fieldType": "text",
"label": "Request Initiator",
"required": false,
"userReference": {
"role": "initiator",
"field": "name",
"autoPopulate": true,
"editable": false
},
"visibility": {
"dependsOn": null,
"condition": "always_visible"
}
},
{
"fieldId": "activity_date",
"fieldType": "date",
"label": "Activity Date",
"required": true
},
{
"fieldId": "location",
"fieldType": "text",
"label": "Location",
"required": true
}
],
"userReferences": [
{
"role": "initiator",
"captureFields": ["name", "email", "department"],
"autoPopulateFrom": "user_profile",
"allowOverride": false
},
{
"role": "dealer",
"captureFields": ["name", "email", "phone", "address"],
"autoPopulateFrom": "workflow",
"allowOverride": true
}
]
},
{
"stepNumber": 2,
"stepName": "Dealer Proposal Submission",
"stepDescription": "Dealer submits proposal with cost breakdown",
"fields": [
{
"fieldId": "proposal_document",
"fieldType": "file",
"label": "Proposal Document",
"required": true,
"validation": {
"accept": ".pdf,.doc,.docx",
"maxSize": 10485760
}
},
{
"fieldId": "cost_breakup",
"fieldType": "textarea",
"label": "Cost Breakup (JSON)",
"required": true
},
{
"fieldId": "dealer_comments",
"fieldType": "textarea",
"label": "Dealer Comments",
"required": true
},
{
"fieldId": "dealer_submitted_by",
"fieldType": "text",
"label": "Submitted By",
"required": false,
"userReference": {
"role": "dealer",
"field": "name",
"autoPopulate": true,
"editable": false
}
}
],
"userReferences": [
{
"role": "dealer",
"captureFields": ["name", "email"],
"autoPopulateFrom": "workflow",
"allowOverride": false
}
]
},
{
"stepNumber": 3,
"stepName": "Requestor Evaluation",
"stepDescription": "Initiator reviews and confirms proposal",
"fields": [
{
"fieldId": "evaluation_comments",
"fieldType": "textarea",
"label": "Evaluation Comments",
"required": true
},
{
"fieldId": "requestor_name",
"fieldType": "text",
"label": "Evaluated By",
"required": false,
"userReference": {
"role": "initiator",
"field": "name",
"autoPopulate": true,
"editable": false
}
},
{
"fieldId": "requestor_department",
"fieldType": "text",
"label": "Department",
"required": false,
"userReference": {
"role": "initiator",
"field": "department",
"autoPopulate": true,
"editable": false
}
}
],
"userReferences": [
{
"role": "initiator",
"captureFields": ["name", "email", "department"],
"autoPopulateFrom": "user_profile",
"allowOverride": false
}
]
},
{
"stepNumber": 4,
"stepName": "Department Lead Approval",
"stepDescription": "Department lead approves and blocks IO budget",
"fields": [
{
"fieldId": "io_number",
"fieldType": "text",
"label": "IO Number",
"required": true
},
{
"fieldId": "amount_to_block",
"fieldType": "number",
"label": "Amount to Block",
"required": true,
"validation": {
"min": 0
}
},
{
"fieldId": "dept_lead_name",
"fieldType": "text",
"label": "Approved By",
"required": false,
"userReference": {
"role": "approver",
"level": 3,
"field": "name",
"autoPopulate": true,
"editable": false
}
},
{
"fieldId": "dept_lead_email",
"fieldType": "text",
"label": "Department Lead Email",
"required": false,
"userReference": {
"role": "approver",
"level": 3,
"field": "email",
"autoPopulate": true,
"editable": false
}
},
{
"fieldId": "dept_lead_department",
"fieldType": "text",
"label": "Department",
"required": false,
"userReference": {
"role": "approver",
"level": 3,
"field": "department",
"autoPopulate": true,
"editable": false
}
}
],
"userReferences": [
{
"role": "approver",
"captureFields": ["name", "email", "department"],
"autoPopulateFrom": "approval_level",
"allowOverride": false
}
]
}
// ... more steps
],
"dynamic_approver_config": {
"enabled": true,
"approverSelection": {
"mode": "role_based",
"dynamicRules": [
{
"level": 3,
"selectionCriteria": {
"type": "role",
"value": "DEPARTMENT_LEAD",
"fallback": "MANAGEMENT"
},
"captureUserDetails": true,
"captureFields": ["name", "email", "department", "employee_id"]
},
{
"level": 4,
"selectionCriteria": {
"type": "department",
"value": "${initiator.department}",
"fallback": null
},
"captureUserDetails": true,
"captureFields": ["name", "email"]
}
]
},
"approverDetailsInSteps": [
{
"stepNumber": 4,
"approverLevel": 3,
"captureFields": ["name", "email", "department"],
"displayMode": "readonly"
}
]
}
}
```
### Example 2: Simple Vendor Payment Template (Admin-Created)
```json
{
"template_name": "Vendor Payment Request",
"template_code": "VENDOR_PAYMENT",
"workflow_type": "VENDOR_PAYMENT",
"form_steps_config": [
{
"stepNumber": 1,
"stepName": "Vendor Details",
"fields": [
{
"fieldId": "vendor_name",
"fieldType": "text",
"label": "Vendor Name",
"required": true
},
{
"fieldId": "invoice_number",
"fieldType": "text",
"label": "Invoice Number",
"required": true
},
{
"fieldId": "initiator_info",
"fieldType": "text",
"label": "Requested By",
"required": false,
"userReference": {
"role": "initiator",
"field": "all",
"autoPopulate": true,
"editable": false
}
}
]
},
{
"stepNumber": 2,
"stepName": "Team Lead Approval",
"fields": [
{
"fieldId": "team_lead_name",
"fieldType": "text",
"label": "Team Lead",
"required": false,
"userReference": {
"role": "approver",
"level": 1,
"field": "name",
"autoPopulate": true,
"editable": false
}
},
{
"fieldId": "team_lead_approval",
"fieldType": "select",
"label": "Approval Status",
"required": true,
"options": [
{ "value": "approved", "label": "Approved" },
{ "value": "rejected", "label": "Rejected" }
]
}
]
}
],
"dynamic_approver_config": {
"enabled": true,
"approverSelection": {
"mode": "role_based",
"dynamicRules": [
{
"level": 1,
"selectionCriteria": {
"type": "role",
"value": "TEAM_LEAD",
"fallback": "MANAGEMENT"
},
"captureUserDetails": true,
"captureFields": ["name", "email", "department"]
}
]
}
}
}
```
---
## Service Layer Implementation
### 1. Template Service with Dynamic Field Resolution
```typescript
// Re_Backend/src/services/templateFieldResolver.service.ts
import { WorkflowRequest } from '../models/WorkflowRequest';
import { ApprovalLevel } from '../models/ApprovalLevel';
import { User } from '../models/User';
import { Participant } from '../models/Participant';
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> = {};
for (const field of stepConfig.fields) {
if (field.userReference) {
const userData = await this.getUserDataForReference(
field.userReference,
request,
currentUserId,
context
);
if (field.userReference.autoPopulate) {
resolvedFields[field.fieldId] = this.extractUserField(
userData,
field.userReference.field
);
}
}
}
return resolvedFields;
}
/**
* Get user data based on reference configuration
*/
private async getUserDataForReference(
userRef: UserReference,
request: WorkflowRequest,
currentUserId: string,
context?: any
): Promise<User | null> {
switch (userRef.role) {
case 'initiator':
return await User.findByPk(request.initiatorId);
case 'dealer':
// Get dealer from participants or claim details
const dealerParticipant = await Participant.findOne({
where: {
requestId: request.requestId,
participantType: 'DEALER'
},
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
}
});
if (currentLevel?.approverId) {
return await User.findByPk(currentLevel.approverId);
}
return null;
case 'team_lead':
// Find team lead based on initiator's department or manager
const initiator = await User.findByPk(request.initiatorId);
if (initiator?.manager) {
return await User.findOne({
where: {
email: initiator.manager,
role: 'MANAGEMENT' // or specific role check
}
});
}
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'
},
order: [['created_at', 'DESC']]
});
}
return null;
case 'current_approver':
const currentApprovalLevel = await ApprovalLevel.findOne({
where: {
requestId: request.requestId,
status: 'PENDING'
},
order: [['level_number', 'ASC']]
});
if (currentApprovalLevel?.approverId) {
return await User.findByPk(currentApprovalLevel.approverId);
}
return null;
case 'previous_approver':
const previousLevel = request.currentLevel - 1;
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;
}
}
/**
* Extract specific field from user data
*/
private extractUserField(user: User | null, field: string): any {
if (!user) return null;
switch (field) {
case 'name':
return user.displayName || `${user.firstName} ${user.lastName}`;
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,
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: DynamicApproverConfig,
request: WorkflowRequest
): Promise<User | null> {
if (!config.enabled || !config.approverSelection.dynamicRules) {
return null;
}
const rule = config.approverSelection.dynamicRules.find(r => 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 || '');
return await User.findOne({
where: {
department: deptValue,
role: 'MANAGEMENT'
}
});
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;
}
}
}
```
### 2. Enhanced Template Service
```typescript
// Re_Backend/src/services/enhancedTemplate.service.ts
import { WorkflowTemplate } from '../models/WorkflowTemplate';
import { TemplateFieldResolver } from './templateFieldResolver.service';
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[]> {
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);
if (request) {
return await this.resolveStepsWithUserData(stepsConfig, request, currentUserId);
}
}
return stepsConfig;
}
/**
* Resolve user references in all steps
*/
private async resolveStepsWithUserData(
steps: FormStepConfig[],
request: WorkflowRequest,
currentUserId: string
): Promise<FormStepConfig[]> {
// 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;
}
/**
* Validate and save form data for a step
*/
async saveStepData(
templateId: string,
requestId: string,
stepNumber: number,
formData: Record<string, any>,
userId: string
): Promise<void> {
const template = await WorkflowTemplate.findByPk(templateId);
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);
}
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 && value < field.validation.min) {
throw new Error(`${field.label} must be at least ${field.validation.min}`);
}
if (field.validation.max && value > field.validation.max) {
throw new Error(`${field.label} must be at most ${field.validation.max}`);
}
}
}
}
private async saveToTemplateStorage(
workflowType: string,
requestId: string,
stepNumber: number,
formData: Record<string, any>
): Promise<void> {
// Save to appropriate extension table based on workflow type
switch (workflowType) {
case 'CLAIM_MANAGEMENT':
await this.saveClaimManagementStepData(requestId, stepNumber, formData);
break;
case 'VENDOR_PAYMENT':
await this.saveVendorPaymentStepData(requestId, stepNumber, formData);
break;
default:
// Generic storage for custom templates
await this.saveGenericStepData(requestId, stepNumber, formData);
}
}
private async saveClaimManagementStepData(
requestId: string,
stepNumber: number,
formData: Record<string, any>
): Promise<void> {
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,
// ... map all fields
});
break;
case 2:
// Save to dealer_proposal_details
await DealerProposalDetails.upsert({
requestId,
costBreakup: formData.cost_breakup,
dealerComments: formData.dealer_comments,
// ...
});
break;
// ... other steps
}
}
}
```
---
## API Endpoints
### 1. Get Template Form Configuration
```typescript
// GET /api/v1/templates/:templateId/form-config
// Query params: ?requestId=xxx&stepNumber=1
async getTemplateFormConfig(req: Request, res: Response) {
const { templateId } = req.params;
const { requestId, stepNumber } = req.query;
const userId = req.user?.userId;
const templateService = new EnhancedTemplateService();
const formConfig = await templateService.getFormConfig(
templateId,
requestId as string,
userId
);
// Filter by step if specified
const steps = stepNumber
? formConfig.filter(s => s.stepNumber === parseInt(stepNumber as string))
: formConfig;
return ResponseHandler.success(res, { steps });
}
```
### 2. Save Step Data
```typescript
// POST /api/v1/templates/:templateId/requests/:requestId/steps/:stepNumber
async saveStepData(req: AuthenticatedRequest, res: Response) {
const { templateId, requestId, stepNumber } = req.params;
const formData = req.body;
const userId = req.user?.userId;
const templateService = new EnhancedTemplateService();
await templateService.saveStepData(
templateId,
requestId,
parseInt(stepNumber),
formData,
userId
);
return ResponseHandler.success(res, { message: 'Step data saved' });
}
```
### 3. Get Resolved User Data
```typescript
// GET /api/v1/templates/:templateId/user-references
// Query: ?role=approver&level=3&requestId=xxx
async getUserReferenceData(req: Request, res: Response) {
const { templateId } = req.params;
const { role, level, requestId } = req.query;
const template = await WorkflowTemplate.findByPk(templateId);
const request = requestId ? await WorkflowRequest.findByPk(requestId as string) : null;
const fieldResolver = new TemplateFieldResolver();
const userData = await fieldResolver.getUserDataForReference(
{
role: role as string,
level: level ? parseInt(level as string) : undefined,
field: 'all',
autoPopulate: true,
editable: false
},
request!,
req.user?.userId || ''
);
return ResponseHandler.success(res, { userData });
}
```
---
## Frontend Integration
### 1. Dynamic Form Renderer
```typescript
// Frontend: components/DynamicFormRenderer.tsx
interface DynamicFormRendererProps {
stepConfig: FormStepConfig;
formData: Record<string, any>;
onChange: (fieldId: string, value: any) => void;
userReferences?: Record<string, any>;
}
export function DynamicFormRenderer({
stepConfig,
formData,
onChange,
userReferences
}: DynamicFormRendererProps) {
return (
<div className="space-y-4">
{stepConfig.fields.map(field => {
// Auto-populate from user references
const value = userReferences?.[field.fieldId] || formData[field.fieldId] || field.defaultValue;
return (
<div key={field.fieldId}>
<Label>{field.label}</Label>
{field.userReference?.autoPopulate && userReferences?.[field.fieldId] && (
<Badge variant="info">Auto-filled from {field.userReference.role}</Badge>
)}
{renderField(field, value, onChange, field.userReference?.editable !== false)}
</div>
);
})}
</div>
);
}
```
### 2. Multi-Step Wizard
```typescript
// Frontend: components/TemplateWizard.tsx
export function TemplateWizard({ templateId, requestId }: Props) {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<Record<string, any>>({});
const [userReferences, setUserReferences] = useState<Record<string, any>>({});
// Fetch form config with resolved user references
const { data: formConfig } = useQuery(
['template-form-config', templateId, requestId],
() => api.getTemplateFormConfig(templateId, { requestId })
);
// Auto-populate user references when step changes
useEffect(() => {
if (formConfig?.steps) {
const step = formConfig.steps.find(s => s.stepNumber === currentStep);
if (step?.userReferences) {
// Fetch user reference data
step.userReferences.forEach(ref => {
api.getUserReferenceData(templateId, {
role: ref.role,
level: ref.level,
requestId
}).then(data => {
setUserReferences(prev => ({
...prev,
...data.userData
}));
});
});
}
}
}, [currentStep, formConfig]);
const handleSubmit = async () => {
await api.saveStepData(templateId, requestId, currentStep, formData);
// Move to next step or submit
};
return (
<Wizard currentStep={currentStep} totalSteps={formConfig?.steps.length || 0}>
{formConfig?.steps.map(step => (
<WizardStep key={step.stepNumber} stepNumber={step.stepNumber}>
<DynamicFormRenderer
stepConfig={step}
formData={formData}
onChange={(fieldId, value) => {
setFormData(prev => ({ ...prev, [fieldId]: value }));
}}
userReferences={userReferences}
/>
</WizardStep>
))}
</Wizard>
);
}
```
---
## Admin UI for Template Creation
### Template Builder Component
```typescript
// Frontend: admin/TemplateBuilder.tsx
export function TemplateBuilder() {
const [steps, setSteps] = useState<FormStepConfig[]>([]);
const [currentStep, setCurrentStep] = useState(1);
const addField = (stepNumber: number) => {
const newField: FormFieldConfig = {
fieldId: `field_${Date.now()}`,
fieldType: 'text',
label: 'New Field',
required: false
};
setSteps(prev => prev.map(step =>
step.stepNumber === stepNumber
? { ...step, fields: [...step.fields, newField] }
: step
));
};
const configureUserReference = (stepNumber: number, fieldId: string) => {
// Open modal to configure user reference
// Allow admin to select:
// - Role (initiator, dealer, approver, team_lead, etc.)
// - Level (for approver)
// - Field to capture (name, email, all, etc.)
// - Auto-populate toggle
// - Editable toggle
};
return (
<div>
<StepBuilder
steps={steps}
onAddStep={() => {/* ... */}}
onAddField={addField}
onConfigureUserReference={configureUserReference}
/>
</div>
);
}
```
---
## Summary
This enhanced system provides:
1.**Multi-step forms** - Admins can configure multiple steps per template
2.**Dynamic user references** - Fields can auto-populate from initiator, dealer, approvers, team lead, etc.
3.**Flexible approver selection** - Dynamic approver assignment based on role, department, manager
4.**Auto-population** - User details automatically filled based on workflow context
5.**Extensibility** - Easy to add new user reference types
6.**Admin-friendly** - Visual template builder for creating templates
7.**Type-safe** - Structured configuration with validation
The system automatically:
- Captures initiator details when request is created
- Captures dealer details when dealer is selected
- Captures approver details when approver is assigned (dynamically or statically)
- Updates fields when approvers change
- Supports team lead, department lead, and custom role references