kaauh_ats/templates/forms/form_wizard.html
2025-10-07 16:22:56 +03:00

1157 lines
41 KiB
HTML

<!-- templates/form_wizard.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Application Form</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* KAAT-S Theme Variables */
:root {
--kaauh-teal: #00636e; /* Main Primary Color */
--kaauh-teal-dark: #004a53; /* Dark Primary Color */
/* Mapping wizard defaults to theme colors */
--primary: var(--kaauh-teal);
--primary-light: #007c89; /* Slightly lighter shade for subtle hover/border */
--secondary: var(--kaauh-teal-dark);
--success: #198754; /* Keeping a standard success green for Submit */
--error: #dc3545; /* Standard danger red */
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--light-gray: #e9ecef;
--border: #dee2e6;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--radius: 16px; /* Increased radius for a softer look */
--transition: all 0.3s ease;
}
body {
/* Dark gradient background to match the theme */
background: linear-gradient(135deg, var(--kaauh-teal-dark) 0%, #1e3a47 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.wizard-container {
width: 100%;
max-width: 800px; /* Increased max-width slightly for content */
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
height: 90vh;
}
/* Progress Bar */
.progress-container {
height: 8px; /* Slightly thicker bar */
background: var(--light-gray);
position: relative;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: var(--primary); /* Teal color */
transition: width 0.4s ease;
width: 0%;
}
/* Header */
.wizard-header {
padding: 25px 30px 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.4rem;
font-weight: 700;
color: var(--secondary); /* Dark teal for logo */
display: flex;
align-items: center;
gap: 10px;
}
.progress-text {
font-size: 0.9rem;
color: var(--gray);
font-weight: 500;
}
/* Main Content */
.wizard-content {
flex: 1;
padding: 0 30px 30px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.stage-container {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
padding-right: 15px; /* Space for scrollbar */
}
.stage-title {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 25px;
color: var(--dark);
line-height: 1.3;
}
.field-container {
margin-bottom: 25px;
}
.field-label {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
color: var(--dark);
}
.required-indicator {
color: var(--error);
font-weight: bold;
}
/* Input Styles */
.form-input {
width: 100%;
padding: 14px 16px;
border: 2px solid var(--border);
border-radius: 12px;
font-size: 1rem;
transition: var(--transition);
}
.form-input:focus {
outline: none;
border-color: var(--primary); /* Teal focus border */
box-shadow: 0 0 0 4px rgba(0, 99, 110, 0.2); /* Teal shadow */
}
.form-input.error {
border-color: var(--error);
box-shadow: 0 0 0 4px rgba(220, 53, 69, 0.2);
}
.error-message {
color: var(--error);
font-size: 0.85rem;
margin-top: 5px;
display: none;
}
.error-message.show {
display: block;
}
.form-textarea {
min-height: 120px;
resize: vertical;
}
/* File Upload Styles */
.file-upload-area {
border: 2px dashed var(--border);
border-radius: 12px;
padding: 25px;
text-align: center;
background: var(--light);
transition: var(--transition);
cursor: pointer;
}
.file-upload-area:hover {
border-color: var(--primary); /* Teal hover border */
background: rgba(0, 99, 110, 0.05); /* Light teal background */
}
.file-upload-area.error {
border-color: var(--error);
background: rgba(220, 53, 69, 0.05);
}
.file-upload-icon {
font-size: 2.5rem;
color: var(--primary); /* Teal icon */
margin-bottom: 15px;
}
.file-upload-text {
font-size: 1.1rem;
margin-bottom: 10px;
}
.file-upload-text strong {
color: var(--primary); /* Teal text */
}
.file-upload-info {
font-size: 0.9rem;
color: var(--gray);
}
.uploaded-file {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px 16px;
margin-top: 15px;
}
.file-info {
display: flex;
align-items: center;
gap: 12px;
}
.file-icon {
color: var(--primary); /* Teal icon */
font-size: 1.2rem;
}
.file-name {
font-weight: 600;
}
.file-size {
font-size: 0.85rem;
color: var(--gray);
}
.remove-file-btn {
background: none;
border: none;
color: var(--error);
cursor: pointer;
font-size: 1.2rem;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: var(--transition);
}
.remove-file-btn:hover {
background: rgba(220, 53, 69, 0.1);
}
/* Radio/Checkbox Styles */
.option-item {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 12px;
border: 2px solid var(--border);
border-radius: 12px;
transition: var(--transition);
cursor: pointer;
}
.option-item:hover {
border-color: var(--primary-light);
}
.option-item.selected {
border-color: var(--primary); /* Teal border */
background: rgba(0, 99, 110, 0.05); /* Light teal background */
}
.option-item.error {
border-color: var(--error);
background: rgba(220, 53, 69, 0.05);
}
/* Ensures radio/checkbox controls themselves use the primary color */
.option-item input[type="radio"]:checked,
.option-item input[type="checkbox"]:checked {
accent-color: var(--primary);
}
.option-item input {
margin-right: 12px;
width: 20px;
height: 20px;
}
/* Preview Styles */
.preview-container {
background: var(--light);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
}
.preview-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border);
}
.preview-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.preview-label {
font-weight: 600;
margin-bottom: 5px;
color: var(--dark);
}
.preview-value {
color: var(--gray);
}
/* Navigation */
.wizard-footer {
padding: 0 30px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-btn {
padding: 14px 28px;
border-radius: 12px;
border: none;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 10px;
}
.btn-back {
background: var(--light-gray); /* Match theme's light gray */
color: var(--gray);
}
.btn-back:hover {
background: #d8dadc;
}
.btn-next {
background: var(--primary); /* Teal color */
color: white;
box-shadow: 0 4px 12px rgba(0, 99, 110, 0.3); /* Teal shadow */
}
.btn-next:hover {
background: var(--secondary); /* Darker teal on hover */
transform: translateY(-2px);
}
.btn-submit {
background: var(--success); /* Green for submit */
color: white;
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.3);
}
.btn-submit:hover {
background: #157347;
transform: translateY(-2px);
}
/* Responsive */
@media (max-width: 600px) {
.wizard-container {
height: 100vh;
border-radius: 0;
max-width: 100%;
}
.stage-title {
font-size: 1.5rem;
}
.wizard-header {
padding: 20px;
}
.wizard-content {
padding: 0 20px 20px;
}
.wizard-footer {
padding: 0 20px 20px;
}
}
</style>
</head>
<body>
<div class="wizard-container">
<div class="progress-container">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="wizard-header">
<div class="logo">
<i class="fas fa-file-alt"></i>
<span id="formTitle">Application Form</span>
</div>
<div class="progress-text" id="progressText">1 of 1</div>
</div>
<div class="wizard-content">
<div class="stage-container" id="stageContainer">
</div>
<div class="preview-container" id="previewContainer" style="display: none;">
<h3 class="mb-4">Review Your Application</h3>
<div id="previewContent"></div>
</div>
</div>
<div class="wizard-footer">
<button id="backBtn" class="nav-btn btn-back" style="display: none;">
<i class="fas fa-arrow-left"></i> Back
</button>
<button id="nextBtn" class="nav-btn btn-next">
Next <i class="fas fa-arrow-right"></i>
</button>
<button id="submitBtn" class="nav-btn btn-submit" style="display: none;">
Submit Application <i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<script>
// Application State
const csrfToken = '{{ csrf_token }}';
const state = {
templateId: {{ template_id }},
stages: [],
currentStage: 0,
formData: {},
isPreview: false,
fieldErrors: {} // Store validation errors
};
// DOM Elements
const elements = {
progressBar: document.getElementById('progressBar'),
progressText: document.getElementById('progressText'),
formTitle: document.getElementById('formTitle'),
stageContainer: document.getElementById('stageContainer'),
previewContainer: document.getElementById('previewContainer'),
previewContent: document.getElementById('previewContent'),
backBtn: document.getElementById('backBtn'),
nextBtn: document.getElementById('nextBtn'),
submitBtn: document.getElementById('submitBtn')
};
// Validation Functions
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function validatePhone(phone) {
// Remove all non-digit characters
const digits = phone.replace(/\D/g, '');
// Should be between 10-15 digits
return digits.length >= 10 && digits.length <= 15;
}
function validateRequired(value) {
if (value === undefined || value === null) return false;
if (typeof value === 'string') return value.trim() !== '';
if (Array.isArray(value)) return value.length > 0;
if (value instanceof File) return true;
return Boolean(value);
}
function validateField(field, value) {
// Clear previous error
delete state.fieldErrors[field.id];
// Check required validation
if (field.required && !validateRequired(value)) {
state.fieldErrors[field.id] = 'This field is required';
return false;
}
// Skip validation if not required and empty
if (!field.required && !validateRequired(value)) {
return true;
}
// Field type specific validation
switch (field.type) {
case 'email':
if (!validateEmail(value)) {
state.fieldErrors[field.id] = 'Please enter a valid email address';
return false;
}
break;
case 'phone':
if (!validatePhone(value)) {
state.fieldErrors[field.id] = 'Please enter a valid phone number';
return false;
}
break;
case 'file':
if (value instanceof File) {
// Validate file size
const maxFileSize = field.maxFileSize || 5;
const fileSizeMB = value.size / (1024 * 1024);
if (fileSizeMB > maxFileSize) {
state.fieldErrors[field.id] = `File size exceeds ${maxFileSize}MB limit`;
return false;
}
// Validate file type
const allowedTypes = (field.fileTypes || '.pdf,.doc,.docx').split(',');
const fileType = '.' + value.name.split('.').pop().toLowerCase();
if (!allowedTypes.some(type => type.trim().toLowerCase() === fileType)) {
state.fieldErrors[field.id] = `File type not allowed. Allowed types: ${field.fileTypes || '.pdf, .doc, .docx'}`;
return false;
}
}
break;
case 'date':
if (value && !/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
state.fieldErrors[field.id] = 'Please select a valid date';
return false;
}
break;
}
return true;
}
function validateCurrentStage() {
const currentStage = state.stages[state.currentStage];
let isValid = true;
// Clear all errors for this stage
currentStage.fields.forEach(field => {
delete state.fieldErrors[field.id];
});
// Validate each field
currentStage.fields.forEach(field => {
const value = state.formData[field.id];
if (!validateField(field, value)) {
isValid = false;
}
});
// Show error messages
showFieldErrors();
return isValid;
}
function showFieldErrors() {
// Hide all error messages first
document.querySelectorAll('.error-message').forEach(el => {
el.classList.remove('show');
});
// Show errors for fields that have them
Object.keys(state.fieldErrors).forEach(fieldId => {
const errorElement = document.querySelector(`#error_${fieldId}`);
if (errorElement) {
errorElement.textContent = state.fieldErrors[fieldId];
errorElement.classList.add('show');
}
// Highlight the field
const fieldElement = document.querySelector(`#field_${fieldId}`);
if (fieldElement) {
fieldElement.classList.add('error');
}
// Highlight file upload area
const fileUploadArea = document.querySelector(`[data-field-id="${fieldId}"] .file-upload-area`);
if (fileUploadArea) {
fileUploadArea.classList.add('error');
}
// Highlight option items for radio/checkbox
const optionItems = document.querySelectorAll(`[name="field_${fieldId}"]`);
if (optionItems.length > 0) {
const parent = optionItems[0].closest('.field-container');
if (parent) {
parent.querySelectorAll('.option-item').forEach(item => {
item.classList.add('error');
});
}
}
});
}
function clearFieldError(fieldId) {
delete state.fieldErrors[fieldId];
// Remove error styling
const fieldElement = document.querySelector(`#field_${fieldId}`);
if (fieldElement) {
fieldElement.classList.remove('error');
}
const errorElement = document.querySelector(`#error_${fieldId}`);
if (errorElement) {
errorElement.classList.remove('show');
}
const fileUploadArea = document.querySelector(`[data-field-id="${fieldId}"] .file-upload-area`);
if (fileUploadArea) {
fileUploadArea.classList.remove('error');
}
const optionItems = document.querySelectorAll(`[name="field_${fieldId}"]`);
if (optionItems.length > 0) {
const parent = optionItems[0].closest('.field-container');
if (parent) {
parent.querySelectorAll('.option-item').forEach(item => {
item.classList.remove('error');
});
}
}
}
// Utility Functions
function getFieldIcon(type) {
const icons = {
'text': 'fas fa-font',
'email': 'fas fa-envelope',
'phone': 'fas fa-phone',
'textarea': 'fas fa-align-left',
'file': 'fas fa-file-upload',
'date': 'fas fa-calendar',
'select': 'fas fa-caret-square-down',
'radio': 'fas fa-dot-circle',
'checkbox': 'fas fa-check-square'
};
return icons[type] || 'fas fa-question';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// API Functions
async function loadFormTemplate() {
try {
const response = await fetch(`/api/templates/${state.templateId}/`);
const result = await response.json();
if (result.success) {
const templateData = result.template;
state.stages = templateData.stages;
elements.formTitle.textContent = templateData.name;
updateProgress();
renderCurrentStage();
}
} catch (error) {
console.error('Error loading template:', error);
alert('Error loading form template.');
}
}
async function submitForm() {
// Validate all stages before submission
let allValid = true;
for (let i = 0; i < state.stages.length; i++) {
state.currentStage = i;
if (!validateCurrentStage()) {
allValid = false;
break;
}
}
if (!allValid) {
alert('Please fix the validation errors before submitting.');
return;
}
const formData = new FormData();
// Add applicant info
formData.append('applicant_name', state.formData.applicant_name || '');
formData.append('applicant_email', state.formData.applicant_email || '');
// Add field responses
state.stages.forEach(stage => {
stage.fields.forEach(field => {
const value = state.formData[field.id];
if (value !== undefined && value !== null) {
if (field.type === 'file' && value instanceof File) {
formData.append(`field_${field.id}`, value);
} else if (field.type === 'checkbox') {
formData.append(`field_${field.id}`, JSON.stringify(value));
} else {
formData.append(`field_${field.id}`, value);
}
}
});
});
try {
const response = await fetch(`/forms/form/${state.templateId}/submit/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
},
body: formData
});
const result = await response.json();
if (result.success) {
alert('Application submitted successfully! Thank you for your submission.');
window.location.href = '/templates/'; // Redirect to templates list
} else {
alert('Error submitting form: ' + result.error);
}
} catch (error) {
console.error('Error:', error);
alert('Error submitting form. Please try again.');
}
}
// DOM Rendering Functions
function updateProgress() {
const totalStages = state.stages.length;
const progress = state.isPreview ? 100 : ((state.currentStage) / (totalStages - 1)) * 100;
elements.progressBar.style.width = `${progress}%`;
elements.progressText.textContent = state.isPreview ?
`Preview` :
`${state.currentStage + 1} of ${totalStages}`;
}
function renderCurrentStage() {
if (state.isPreview) {
renderPreview();
return;
}
const currentStage = state.stages[state.currentStage];
elements.stageContainer.innerHTML = '';
elements.previewContainer.style.display = 'none';
const stageTitle = document.createElement('h2');
stageTitle.className = 'stage-title';
stageTitle.textContent = currentStage.name;
elements.stageContainer.appendChild(stageTitle);
currentStage.fields.forEach(field => {
const fieldElement = createFieldElement(field);
elements.stageContainer.appendChild(fieldElement);
});
// Update navigation buttons
elements.backBtn.style.display = state.currentStage > 0 ? 'flex' : 'none';
elements.submitBtn.style.display = 'none';
elements.nextBtn.style.display = 'flex';
elements.nextBtn.textContent = state.currentStage === state.stages.length - 1 ?
'Preview' :
'Next'
}
function createFieldElement(field) {
const fieldDiv = document.createElement('div');
fieldDiv.className = 'field-container';
fieldDiv.dataset.fieldId = field.id;
const fieldLabel = document.createElement('label');
fieldLabel.className = 'field-label';
fieldLabel.innerHTML = `
${field.label}
${field.required ? '<span class="required-indicator"> *</span>' : ''}
`;
fieldDiv.appendChild(fieldLabel);
// Create input based on field type
if (field.type === 'text' || field.type === 'email' || field.type === 'phone') {
const input = document.createElement('input');
input.type = field.type === 'email' ? 'email' : field.type === 'phone' ? 'tel' : 'text';
input.className = 'form-input';
input.placeholder = field.placeholder || `Enter ${field.label.toLowerCase()}`;
input.id = `field_${field.id}`;
input.value = state.formData[field.id] || '';
input.required = field.required;
input.addEventListener('input', (e) => {
state.formData[field.id] = e.target.value;
clearFieldError(field.id);
});
input.addEventListener('blur', () => {
validateField(field, state.formData[field.id]);
showFieldErrors();
});
fieldDiv.appendChild(input);
// Add error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.id = `error_${field.id}`;
fieldDiv.appendChild(errorDiv);
}
else if (field.type === 'textarea') {
const textarea = document.createElement('textarea');
textarea.className = 'form-input form-textarea';
textarea.placeholder = field.placeholder || `Enter ${field.label.toLowerCase()}`;
textarea.id = `field_${field.id}`;
textarea.value = state.formData[field.id] || '';
textarea.required = field.required;
textarea.addEventListener('input', (e) => {
state.formData[field.id] = e.target.value;
clearFieldError(field.id);
});
textarea.addEventListener('blur', () => {
validateField(field, state.formData[field.id]);
showFieldErrors();
});
fieldDiv.appendChild(textarea);
// Add error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.id = `error_${field.id}`;
fieldDiv.appendChild(errorDiv);
}
else if (field.type === 'file') {
const fileUpload = document.createElement('div');
fileUpload.className = 'file-upload-area';
fileUpload.innerHTML = `
<div class="file-upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<div class="file-upload-text">
<p>Drag & drop your ${field.label.toLowerCase()} here or <strong>click to browse</strong></p>
</div>
<div class="file-upload-info">
<p>Supported formats: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)</p>
</div>
<input type="file" class="file-input" id="field_${field.id}" accept="${field.fileTypes || '.pdf,.doc,.docx'}">
`;
const fileInput = fileUpload.querySelector('.file-input');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
state.formData[field.id] = file;
clearFieldError(field.id);
// Show uploaded file preview
const uploadedFile = document.createElement('div');
uploadedFile.className = 'uploaded-file';
uploadedFile.innerHTML = `
<div class="file-info">
<i class="fas fa-file file-icon"></i>
<div>
<div class="file-name">${file.name}</div>
<div class="file-size">${formatFileSize(file.size)}</div>
</div>
</div>
<button class="remove-file-btn">
<i class="fas fa-times"></i>
</button>
`;
fileUpload.appendChild(uploadedFile);
// Add remove functionality
uploadedFile.querySelector('.remove-file-btn').addEventListener('click', () => {
delete state.formData[field.id];
uploadedFile.remove();
clearFieldError(field.id);
});
} else {
delete state.formData[field.id];
clearFieldError(field.id);
}
});
fieldDiv.appendChild(fileUpload);
// Add error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.id = `error_${field.id}`;
fieldDiv.appendChild(errorDiv);
}
else if (field.type === 'date') {
const input = document.createElement('input');
input.type = 'month';
input.className = 'form-input';
input.placeholder = field.placeholder || 'Select date';
input.id = `field_${field.id}`;
input.value = state.formData[field.id] || '';
input.required = field.required;
input.addEventListener('input', (e) => {
state.formData[field.id] = e.target.value;
clearFieldError(field.id);
});
input.addEventListener('blur', () => {
validateField(field, state.formData[field.id]);
showFieldErrors();
});
fieldDiv.appendChild(input);
// Add error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.id = `error_${field.id}`;
fieldDiv.appendChild(errorDiv);
}
else if (field.type === 'select') {
const select = document.createElement('select');
select.className = 'form-input';
select.id = `field_${field.id}`;
select.required = field.required;
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = `Select ${field.label.toLowerCase()}`;
select.appendChild(defaultOption);
field.options.forEach(option => {
const optionEl = document.createElement('option');
optionEl.value = option;
optionEl.textContent = option;
if (state.formData[field.id] === option) {
optionEl.selected = true;
}
select.appendChild(optionEl);
});
select.addEventListener('change', (e) => {
state.formData[field.id] = e.target.value;
clearFieldError(field.id);
});
select.addEventListener('blur', () => {
validateField(field, state.formData[field.id]);
showFieldErrors();
});
fieldDiv.appendChild(select);
// Add error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.id = `error_${field.id}`;
fieldDiv.appendChild(errorDiv);
}
else if (field.type === 'radio') {
const optionsDiv = document.createElement('div');
field.options.forEach((option, idx) => {
const optionItem = document.createElement('div');
optionItem.className = 'option-item';
optionItem.innerHTML = `
<input type="radio"
id="field_${field.id}_${idx}"
name="field_${field.id}"
value="${option}"
${state.formData[field.id] === option ? 'checked' : ''}>
<label for="field_${field.id}_${idx}">${option}</label>
`;
optionsDiv.appendChild(optionItem);
const radioInput = optionItem.querySelector('input');
radioInput.addEventListener('change', () => {
state.formData[field.id] = option;
clearFieldError(field.id);
});
});
fieldDiv.appendChild(optionsDiv);
// Add error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.id = `error_${field.id}`;
fieldDiv.appendChild(errorDiv);
}
else if (field.type === 'checkbox') {
const optionsDiv = document.createElement('div');
field.options.forEach((option, idx) => {
const optionItem = document.createElement('div');
optionItem.className = 'option-item';
optionItem.innerHTML = `
<input type="checkbox"
id="field_${field.id}_${idx}"
value="${option}"
${Array.isArray(state.formData[field.id]) && state.formData[field.id].includes(option) ? 'checked' : ''}>
<label for="field_${field.id}_${idx}">${option}</label>
`;
optionsDiv.appendChild(optionItem);
const checkboxInput = optionItem.querySelector('input');
checkboxInput.addEventListener('change', () => {
if (!state.formData[field.id]) {
state.formData[field.id] = [];
}
if (checkboxInput.checked) {
state.formData[field.id].push(option);
} else {
state.formData[field.id] = state.formData[field.id].filter(item => item !== option);
}
clearFieldError(field.id);
});
});
fieldDiv.appendChild(optionsDiv);
// Add error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.id = `error_${field.id}`;
fieldDiv.appendChild(errorDiv);
}
return fieldDiv;
}
function renderPreview() {
elements.stageContainer.style.display = 'none';
elements.previewContainer.style.display = 'block';
elements.previewContent.innerHTML = '';
// Add applicant info if available
if (state.formData.applicant_name || state.formData.applicant_email) {
const applicantDiv = document.createElement('div');
applicantDiv.className = 'preview-item';
applicantDiv.innerHTML = `
<div class="preview-label">Applicant Information</div>
<div class="preview-value">
${state.formData.applicant_name ? `<strong>Name:</strong> ${state.formData.applicant_name}<br>` : ''}
${state.formData.applicant_email ? `<strong>Email:</strong> ${state.formData.applicant_email}` : ''}
</div>
`;
elements.previewContent.appendChild(applicantDiv);
}
// Add stage data
state.stages.forEach(stage => {
const stageDiv = document.createElement('div');
stageDiv.className = 'preview-item';
const stageTitle = document.createElement('div');
stageTitle.className = 'preview-label';
stageTitle.textContent = stage.name;
stageDiv.appendChild(stageTitle);
const stageContent = document.createElement('div');
stageContent.className = 'preview-value';
stage.fields.forEach(field => {
let value = state.formData[field.id];
if (value === undefined || value === null || value === '') {
value = '<em>Not provided</em>';
} else if (field.type === 'file' && value instanceof File) {
value = value.name;
} else if (field.type === 'checkbox' && Array.isArray(value)) {
value = value.join(', ');
}
const fieldDiv = document.createElement('div');
fieldDiv.innerHTML = `<strong>${field.label}:</strong> ${value}`;
stageContent.appendChild(fieldDiv);
});
stageDiv.appendChild(stageContent);
elements.previewContent.appendChild(stageDiv);
});
// Update navigation buttons
elements.backBtn.style.display = 'flex';
elements.nextBtn.style.display = 'none';
elements.submitBtn.style.display = 'flex';
}
// Navigation Functions
function nextStage() {
if (state.isPreview) {
submitForm();
return;
}
if (!validateCurrentStage()) {
// Scroll to first error
const firstError = document.querySelector('.error-message.show');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return;
}
if (state.currentStage === state.stages.length - 1) {
// Go to preview
state.isPreview = true;
renderCurrentStage();
updateProgress();
} else {
// Go to next stage
state.currentStage++;
renderCurrentStage();
updateProgress();
}
}
function prevStage() {
if (state.isPreview) {
// Go back to last stage
state.isPreview = false;
renderCurrentStage();
updateProgress();
} else if (state.currentStage > 0) {
state.currentStage--;
renderCurrentStage();
updateProgress();
}
}
// Initialize Application
function init() {
// Load form template
loadFormTemplate();
// Set up event listeners
elements.nextBtn.addEventListener('click', nextStage);
elements.backBtn.addEventListener('click', prevStage);
elements.submitBtn.addEventListener('click', submitForm);
}
// Start the application
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>