33 KiB
33 KiB
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:
- Create multi-step forms (not just single-step)
- Dynamically reference user types (initiator, dealer, approvers, team lead, etc.)
- Configure user details capture per step
- 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:
- ✅ Multi-step forms - Admins can configure multiple steps per template
- ✅ Dynamic user references - Fields can auto-populate from initiator, dealer, approvers, team lead, etc.
- ✅ Flexible approver selection - Dynamic approver assignment based on role, department, manager
- ✅ Auto-population - User details automatically filled based on workflow context
- ✅ Extensibility - Easy to add new user reference types
- ✅ Admin-friendly - Visual template builder for creating templates
- ✅ 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