1276 lines
52 KiB
HTML
1276 lines
52 KiB
HTML
{% extends 'applicant/partials/candidate_facing_base.html'%}
|
|
{% load static i18n %}
|
|
{% block content %}
|
|
<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 {
|
|
/* Remove centering/flex properties to allow for normal document flow and scrolling */
|
|
padding-top: 56px; /* Space for the sticky navbar */
|
|
|
|
/* Dark gradient background to match the theme */
|
|
background: linear-gradient(
|
|
135deg,
|
|
var(--kaauh-teal-dark) 0%,
|
|
#1e3a47 100%
|
|
);
|
|
background-image: url("{% static 'image/vision.svg' %}");
|
|
@media (max-width: 768px) {
|
|
background-image: none;
|
|
}
|
|
background-repeat: no-repeat;
|
|
background-position: 60px;
|
|
background-size: 320px auto;
|
|
min-height: 100vh;
|
|
padding: 0; /* Remove padding from body */
|
|
font-family:
|
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
"Helvetica Neue", Arial, sans-serif;
|
|
}
|
|
|
|
/* Wrapper to center the wizard content below the navbar */
|
|
.page-content-wrapper {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 20px; /* Re-apply padding here for the content area */
|
|
min-height: calc(
|
|
100vh - 56px
|
|
); /* Adjust height to account for navbar */
|
|
}
|
|
|
|
.wizard-container {
|
|
width: 100%;
|
|
max-width: 900px; /* Increased max-width slightly for content */
|
|
background: white;
|
|
|
|
overflow: hidden;
|
|
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* Allow height to be determined by content, constrained by max-height */
|
|
height: auto;
|
|
max-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.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( --kaauh-teal-dark); /* Green for submit */
|
|
color: white;
|
|
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.3);
|
|
}
|
|
|
|
.btn-submit:hover {
|
|
background: var(--kaauh-teal);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 600px) {
|
|
.wizard-container {
|
|
height: 100vh;
|
|
border-radius: 0;
|
|
max-width: 100%;
|
|
max-height: 100vh;
|
|
}
|
|
|
|
.stage-title {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.wizard-header {
|
|
padding: 20px;
|
|
}
|
|
|
|
.wizard-content {
|
|
padding: 0 20px 20px;
|
|
}
|
|
|
|
.wizard-footer {
|
|
padding: 0 20px 20px;
|
|
}
|
|
}
|
|
|
|
/* === FIX FOR SMALL-SCREEN HAMBURGER MENU === */
|
|
@media (max-width: 991.98px) {
|
|
/* Add vertical spacing to the navigation items when the navbar is collapsed */
|
|
#navbarNav .nav-item {
|
|
margin-top: 5px;
|
|
margin-bottom: 5px;
|
|
padding: 5px 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
#navbarNav .nav-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
#navbarNav .nav-link {
|
|
padding: 8px 15px;
|
|
display: block;
|
|
}
|
|
|
|
/* Adjust the total margin of the top navbar container when collapsed */
|
|
#topNavbar.mb-3 {
|
|
margin-bottom: 0 !important;
|
|
}
|
|
}
|
|
|
|
#bottomNavbar {
|
|
/* Position the dark navbar 72px from the top of the viewport */
|
|
top: 72px;
|
|
/* The z-index is already 1030 in the inline style, which is correct */
|
|
}
|
|
</style>
|
|
|
|
<nav
|
|
id="bottomNavbar"
|
|
class="navbar navbar-expand-lg sticky-top"
|
|
style="background-color: var(--kaauh-teal); z-index: 1030"
|
|
>
|
|
<span class="ms-2 text-white">{% trans "JOB ID" %}: {{job_id}}</span>
|
|
</nav>
|
|
|
|
<div class="page-content-wrapper">
|
|
<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"
|
|
>{% trans "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">
|
|
{% trans "Review Your Application" %}
|
|
</h3>
|
|
<div id="previewContent"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wizard-footer mt-2">
|
|
<button
|
|
id="backBtn"
|
|
class="nav-btn btn-back"
|
|
style="display: none"
|
|
>
|
|
<i class="fas fa-arrow-left"></i> {% trans "Back" %}
|
|
</button>
|
|
|
|
<button id="nextBtn" class="nav-btn btn-next">
|
|
{% trans "Next" %}
|
|
<i class="fas fa-arrow-right"></i>
|
|
</button>
|
|
|
|
<button
|
|
id="submitBtn"
|
|
class="nav-btn btn-submit"
|
|
style="display: none"
|
|
>
|
|
{% trans "Submit Application" %}
|
|
<i class="fas fa-paper-plane"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Application State
|
|
const csrfToken = '{{ csrf_token }}';
|
|
|
|
const state = {
|
|
templateId: '{{ template_slug }}',
|
|
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':
|
|
// Regex for YYYY-MM-DD (ISO standard for <input type="date"> output)
|
|
const yyyyMmDdRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
|
|
|
|
if (value && !yyyyMmDdRegex.test(value)) {
|
|
// You might want to update the error message based on the input type
|
|
state.fieldErrors[field.id] = 'Please enter a valid date (e.g., YYYY-MM-DD).';
|
|
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/v1/templates/${state.templateId}/`);
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const templateData = result.template;
|
|
console.log(templateData);
|
|
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 CSRF token as a form field (CRITICAL FIX)
|
|
formData.append('csrfmiddlewaretoken', csrfToken);
|
|
|
|
// Add field responses
|
|
state.stages.forEach(stage => {
|
|
stage.fields.forEach(field => {
|
|
const value = state.formData[field.id];
|
|
|
|
// Always include the field, even if it's empty
|
|
if (field.type === 'file') {
|
|
if (value instanceof File) {
|
|
formData.append(`field_${field.id}`, value);
|
|
} else {
|
|
// Include empty file field
|
|
formData.append(`field_${field.id}`, '');
|
|
}
|
|
} else if (field.type === 'checkbox') {
|
|
// For checkboxes, send empty array if no selection
|
|
if (Array.isArray(value) && value.length > 0) {
|
|
formData.append(`field_${field.id}`, JSON.stringify(value));
|
|
} else {
|
|
formData.append(`field_${field.id}`, JSON.stringify([]));
|
|
}
|
|
} else {
|
|
// For other field types, send the value or empty string
|
|
formData.append(`field_${field.id}`, value || '');
|
|
}
|
|
});
|
|
});
|
|
|
|
try {
|
|
const response = await fetch(`/application/${state.templateId}/submit/`, {
|
|
method: 'POST',
|
|
body: formData
|
|
// IMPORTANT: Do NOT set Content-Type header when using FormData
|
|
// Do NOT set X-CSRFToken header when using csrfmiddlewaretoken in form data
|
|
});
|
|
|
|
// Check if response is OK
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
console.log('Application submitted successfully! Thank you for your submission.');
|
|
const redirect_url = result['redirect_url']
|
|
window.location.href = redirect_url; // Redirect to applications list
|
|
} else {
|
|
console.log(result)
|
|
console.log('Error submitting form: ' + (result.error || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
//console.error('Submission error:', error);
|
|
// Try to get response text for debugging
|
|
try {
|
|
const errorText = await response.text();
|
|
console.error('Response text:', errorText);
|
|
console.log('Error submitting form. Server response: ' + errorText);
|
|
} catch (e) {
|
|
console.log('Error submitting form: ' + error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = 'date';
|
|
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>
|
|
{% endblock content %} |