Re_Backend/docs/DYNAMIC_TEMPLATE_SYSTEM.md

33 KiB
Raw Permalink Blame History

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

-- 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

// 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

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)

{
  "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)

{
  "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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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