diff --git a/public/js/assessment.js b/public/js/assessment.js index cacec2b..5f2fc9b 100644 --- a/public/js/assessment.js +++ b/public/js/assessment.js @@ -7,23 +7,34 @@ (function($) { 'use strict'; - $(function() { + $(function() { // Document Ready console.log('Assessment script loaded.'); // Check if localized data is available if (typeof quiztech_assessment_vars === 'undefined') { console.error('Quiztech Assessment Error: Localized variables not found.'); + // Optionally display an error message to the user + $('.assessment-container').prepend('
A configuration error occurred. Please contact support.
'); return; } - var $prescreeningForm = $('#quiztech-prescreening-form'); - var $prescreeningSection = $('#quiztech-prescreening-section'); - var $assessmentSection = $('#quiztech-assessment-section'); - var $submitButton = $prescreeningForm.find('button[type="submit"]'); - var $preScreenFormMessages = $('
').insertBefore($submitButton); // Area for messages + // --- Cache jQuery Selectors --- + var $landingSection = $('#quiztech-landing-section'); + var $prescreeningForm = $('#quiztech-prescreening-form'); // May not exist + var $prescreeningSection = $('#quiztech-prescreening-section'); // May not exist + var $beginAssessmentButton = $('#quiztech-begin-assessment'); + var $landingMessages = $landingSection.find('.form-messages'); // Placeholder for messages in landing area + if ($landingMessages.length === 0 && $prescreeningForm.length > 0) { + // If message area doesn't exist in landing but pre-screen form does, add one near the button + $landingMessages = $('').insertBefore($beginAssessmentButton); + } else if ($landingMessages.length === 0) { + // If no message area and no pre-screen form, add one anyway + $landingMessages = $('').insertBefore($beginAssessmentButton); + } - // Assessment specific elements + var $assessmentSection = $('#quiztech-assessment-section'); var $assessmentForm = $('#quiztech-assessment-form'); + var $assessmentMessages = $assessmentForm.find('.form-messages'); // Placeholder in assessment form var $questionsContainer = $('#quiztech-questions-container'); var $questionContainers = $questionsContainer.find('.quiztech-question-container'); var $timerDisplay = $('#quiztech-timer'); @@ -31,84 +42,185 @@ var $submitAssessmentButton = $('#quiztech-submit-assessment'); var $completionMessage = $('#quiztech-completion-message'); - // State variables + // --- State Variables --- var currentQuestionIndex = 0; var totalQuestions = $questionContainers.length; var timerInterval; var timerSeconds = 0; + var isSubmittingPreScreen = false; // Prevent double clicks - // --- Pre-Screening Form Handling --- - $prescreeningForm.on('submit', function(event) { - event.preventDefault(); // Stop traditional form submission + // --- Initial UI State --- + $assessmentSection.hide(); // Ensure assessment is hidden initially + $landingSection.show(); // Ensure landing is visible - // Use $preScreenFormMessages here - $preScreenFormMessages.empty().removeClass('error success'); // Clear previous messages - $submitButton.prop('disabled', true).text('Submitting...'); // Disable button + // --- Helper Functions --- - var formData = $(this).serialize(); // Get form data + /** + * Displays messages (errors or success) + * @param {jQuery} $targetArea - The jQuery object where the message should be displayed. + * @param {string} message - The message text. + * @param {boolean} isError - True for error styling, false for success. + */ + function showMessage($targetArea, message, isError) { + if (!$targetArea || $targetArea.length === 0) { + console.warn("Message area not found for:", message); + return; + } + $targetArea.empty().removeClass('error success') + .addClass(isError ? 'error' : 'success') + .text(message) + .show(); + } - // Add required AJAX parameters - formData += '&action=quiztech_submit_prescreening'; - formData += '&nonce=' + quiztech_assessment_vars.prescreening_nonce; - formData += '&invitation_id=' + quiztech_assessment_vars.invitation_id; + /** + * Hides any messages in the target area. + * @param {jQuery} $targetArea - The jQuery object where the message should be hidden. + */ + function clearMessage($targetArea) { + if ($targetArea && $targetArea.length > 0) { + $targetArea.empty().hide(); + } + } - $.ajax({ - type: 'POST', - url: quiztech_assessment_vars.ajax_url, - data: formData, - dataType: 'json', // Expect JSON response from server - success: function(response) { - if (response.success) { - // Success! Hide pre-screening, show assessment - $preScreenFormMessages.addClass('success').text(response.data.message || 'Success!'); // Show success message briefly & Use correct variable - $prescreeningSection.slideUp(); - $assessmentSection.slideDown(); - startTimer(); // Start the assessment timer - updateNavigationButtons(); // Show initial nav state - // No need to re-enable button as the form is gone + /** + * Transitions the UI from the landing/pre-screening view to the assessment view. + */ + function startAssessmentUI() { + $landingSection.slideUp(); + $assessmentSection.slideDown(); + if (totalQuestions > 0) { + startTimer(); + updateNavigationButtons(); + // Ensure only the first question is visible initially + $questionContainers.addClass('quiztech-question-hidden'); + $questionContainers.eq(0).removeClass('quiztech-question-hidden'); + } else { + // Handle case with no questions if needed + showMessage($assessmentMessages, 'No questions found for this assessment.', true); + $timerDisplay.hide(); + $nextButton.hide(); + $submitAssessmentButton.hide(); + } + } + + // --- Event Handlers --- + + // Handle "Begin Assessment" button click + $beginAssessmentButton.on('click', function() { + var $button = $(this); + clearMessage($landingMessages); // Clear previous messages + + // Check if pre-screening form exists and is visible + var needsPreScreening = $prescreeningForm.length > 0 && $prescreeningSection.is(':visible'); + + if (needsPreScreening) { + // Validate pre-screening form before submitting + var isValid = true; + $prescreeningForm.find('[required]').each(function() { + if (!$(this).val()) { + isValid = false; + $(this).css('border-color', 'red'); // Highlight missing fields } else { - // Handle WP JSON error - $preScreenFormMessages.addClass('error').text(response.data.message || 'An error occurred.'); // Use correct variable - $submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button + $(this).css('border-color', ''); // Reset border color } - }, - error: function(jqXHR, textStatus, errorThrown) { - // Handle general AJAX error - console.error("AJAX Error:", textStatus, errorThrown); - $preScreenFormMessages.addClass('error').text('A network error occurred. Please try again.'); // Use correct variable - $submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button + }); + + if (!isValid) { + showMessage($landingMessages, 'Please fill out all required pre-screening questions.', true); + return; // Stop if validation fails } - }); + + // Submit pre-screening via AJAX + if (isSubmittingPreScreen) return; // Prevent double submission + isSubmittingPreScreen = true; + $button.prop('disabled', true).text('Submitting...'); + + var formData = $prescreeningForm.serialize(); + formData += '&action=quiztech_submit_prescreening'; + formData += '&nonce=' + quiztech_assessment_vars.prescreening_nonce; + formData += '&invitation_id=' + quiztech_assessment_vars.invitation_id; + + $.ajax({ + type: 'POST', + url: quiztech_assessment_vars.ajax_url, + data: formData, + dataType: 'json', + success: function(response) { + if (response.success) { + // Pre-screening successful, start assessment UI + startAssessmentUI(); + } else { + // Handle WP JSON error from pre-screening submission + showMessage($landingMessages, response.data.message || 'An error occurred submitting pre-screening.', true); + $button.prop('disabled', false).text('Begin Assessment'); + } + }, + error: function(jqXHR, textStatus, errorThrown) { + // Handle general AJAX error + console.error("AJAX Error (Pre-Screening):", textStatus, errorThrown); + showMessage($landingMessages, 'A network error occurred. Please try again.', true); + $button.prop('disabled', false).text('Begin Assessment'); + }, + complete: function() { + isSubmittingPreScreen = false; // Re-enable submission possibility + } + }); + + } else { + // No pre-screening needed, directly start the assessment UI + startAssessmentUI(); + } }); // --- Assessment Answer Auto-Save --- - // var $assessmentForm = $('#quiztech-assessment-form'); // Already defined above - var autoSaveTimeout; // To debounce requests - - // Target input/textarea/select elements within the assessment form for auto-save + // (Keep existing auto-save logic - it targets elements within $assessmentForm) + var autoSaveTimeout; $assessmentForm.on('change blur', 'input, textarea, select', function() { - clearTimeout(autoSaveTimeout); // Clear previous timeout if exists + clearTimeout(autoSaveTimeout); var $input = $(this); - var $questionGroup = $input.closest('.quiztech-question-container'); // Updated selector - var questionId = $questionGroup.data('question-id'); - var answer = $input.val(); + var $questionGroup = $input.closest('.quiztech-question-container'); + if ($questionGroup.length === 0) return; // Should not happen - // Add a small visual indicator within the question group + var questionId = $questionGroup.data('question-id'); + var answer = ''; + + // Handle different input types for getting the value + if ($input.is(':radio')) { + if ($input.is(':checked')) { + answer = $input.val(); + } else { + // If a radio button is changed, but not this one, don't save yet. + // The 'change' event on the *checked* radio will trigger the save. + // However, handle the case where *no* radio in the group is checked (e.g., initial state or clearing) + var groupName = $input.attr('name'); + if ($assessmentForm.find('input[name="' + groupName + '"]:checked').length === 0) { + answer = ''; // Or handle as needed - maybe save only on explicit next/submit? + } else { + return; // Let the checked radio handle the save + } + } + } else if ($input.is(':checkbox')) { + // Example for checkboxes (if added later) - might need array handling + answer = $input.is(':checked') ? $input.val() : ''; // Simplistic, adjust if multiple checkboxes per question + } else { + answer = $input.val(); // For textarea, text input, select + } + + // Add visual indicator var $indicator = $questionGroup.find('.save-indicator'); if ($indicator.length === 0) { - $indicator = $('').appendTo($questionGroup.find('label:first')); + $indicator = $('').appendTo($questionGroup.find('.question-answer-area')); } - $indicator.text('Saving...'); + $indicator.text('Saving...').css('color', 'grey').show(); - // Debounce the AJAX request slightly autoSaveTimeout = setTimeout(function() { $.ajax({ type: 'POST', url: quiztech_assessment_vars.ajax_url, data: { action: 'quiztech_save_answer', - nonce: quiztech_assessment_vars.assessment_nonce, // Use the correct nonce + nonce: quiztech_assessment_vars.assessment_nonce, invitation_id: quiztech_assessment_vars.invitation_id, question_id: questionId, answer: answer @@ -117,28 +229,52 @@ success: function(response) { if (response.success) { $indicator.text('Saved ✓').css('color', 'green'); - // Optionally fade out the indicator after a delay - setTimeout(function() { $indicator.fadeOut().remove(); }, 2000); + setTimeout(function() { $indicator.fadeOut(function() { $(this).remove(); }); }, 2000); } else { $indicator.text('Error!').css('color', 'red'); console.error("Auto-save error:", response.data.message); - // Consider more prominent error display if needed } }, error: function(jqXHR, textStatus, errorThrown) { $indicator.text('Network Error!').css('color', 'red'); - console.error("AJAX Error:", textStatus, errorThrown); + console.error("AJAX Error (Auto-Save):", textStatus, errorThrown); } }); - }, 500); // Wait 500ms after the last change/blur before sending + }, 750); // Slightly longer debounce }); - // --- Assessment Navigation --- $nextButton.on('click', function() { if (currentQuestionIndex < totalQuestions - 1) { + // Basic validation before proceeding (optional but good) + var $currentQuestion = $questionContainers.eq(currentQuestionIndex); + var $requiredInputs = $currentQuestion.find('[required]'); + var isValid = true; + $requiredInputs.each(function() { + var $input = $(this); + var value = ''; + if ($input.is(':radio')) { + var groupName = $input.attr('name'); + value = $assessmentForm.find('input[name="' + groupName + '"]:checked').val(); + } else { + value = $input.val(); + } + if (!value) { + isValid = false; + // Maybe add a visual cue near the input? + } + }); + + if (!isValid) { + showMessage($assessmentMessages, 'Please answer the current question before proceeding.', true); + // Auto-hide message after a delay + setTimeout(function() { clearMessage($assessmentMessages); }, 3000); + return; + } + clearMessage($assessmentMessages); // Clear any previous error + // Hide current, show next - $questionContainers.eq(currentQuestionIndex).addClass('quiztech-question-hidden'); + $currentQuestion.addClass('quiztech-question-hidden'); currentQuestionIndex++; $questionContainers.eq(currentQuestionIndex).removeClass('quiztech-question-hidden'); updateNavigationButtons(); @@ -146,6 +282,11 @@ }); function updateNavigationButtons() { + if (totalQuestions <= 0) { + $nextButton.hide(); + $submitAssessmentButton.hide(); + return; + } if (currentQuestionIndex >= totalQuestions - 1) { // Last question $nextButton.hide(); @@ -158,9 +299,9 @@ // --- Timer Functions --- function startTimer() { - if (timerInterval) clearInterval(timerInterval); // Clear existing if any - timerSeconds = 0; // Reset timer - $timerDisplay.text(formatTime(timerSeconds)); // Initial display + if (timerInterval) clearInterval(timerInterval); + timerSeconds = 0; + $timerDisplay.text(formatTime(timerSeconds)).show(); // Ensure visible timerInterval = setInterval(function() { timerSeconds++; @@ -168,57 +309,80 @@ }, 1000); } + function stopTimer() { + clearInterval(timerInterval); + } + function formatTime(totalSeconds) { var hours = Math.floor(totalSeconds / 3600); var minutes = Math.floor((totalSeconds % 3600) / 60); var seconds = totalSeconds % 60; - // Pad with leading zeros minutes = String(minutes).padStart(2, '0'); seconds = String(seconds).padStart(2, '0'); return (hours > 0 ? String(hours).padStart(2, '0') + ':' : '') + minutes + ':' + seconds; } // --- Final Assessment Submission --- - // var $submitAssessmentButton = $('#quiztech-submit-assessment'); // Already defined above - var $assessmentFormMessages = $('
').insertAfter($submitAssessmentButton); // Area for messages - $submitAssessmentButton.on('click', function(event) { - event.preventDefault(); // Stop traditional form submission (though AJAX auto-save handles data) + event.preventDefault(); + + // Final validation (optional, ensure last question answered) + var $currentQuestion = $questionContainers.eq(currentQuestionIndex); + var $requiredInputs = $currentQuestion.find('[required]'); + var isValid = true; + $requiredInputs.each(function() { + var $input = $(this); + var value = ''; + if ($input.is(':radio')) { + var groupName = $input.attr('name'); + value = $assessmentForm.find('input[name="' + groupName + '"]:checked').val(); + } else { + value = $input.val(); + } + if (!value) { + isValid = false; + } + }); + + if (!isValid) { + showMessage($assessmentMessages, 'Please answer the final question before submitting.', true); + setTimeout(function() { clearMessage($assessmentMessages); }, 3000); + return; + } + clearMessage($assessmentMessages); if (!confirm('Are you sure you want to submit your assessment?')) { - return; // User cancelled + return; } - $assessmentFormMessages.empty().removeClass('error success'); - $submitAssessmentButton.prop('disabled', true).text('Submitting...'); + var $button = $(this); + $button.prop('disabled', true).text('Submitting...'); $.ajax({ type: 'POST', url: quiztech_assessment_vars.ajax_url, data: { action: 'quiztech_submit_assessment', - nonce: quiztech_assessment_vars.assessment_nonce, // Reuse assessment nonce + nonce: quiztech_assessment_vars.assessment_nonce, invitation_id: quiztech_assessment_vars.invitation_id }, dataType: 'json', success: function(response) { if (response.success) { // Success! Display completion message and hide form/timer - // Use .html() to render potential basic HTML in the message + stopTimer(); + $assessmentForm.hide(); + $timerDisplay.hide(); $completionMessage.html(response.data.completionMessage || 'Assessment Submitted Successfully!').show(); - $assessmentForm.hide(); // Hide the form elements (questions, nav) - $timerDisplay.hide(); // Hide timer - clearInterval(timerInterval); // Stop timer - $assessmentFormMessages.remove(); // Remove the temporary message area if not needed } else { - $assessmentFormMessages.addClass('error').text(response.data.message || 'An error occurred during submission.'); - $submitAssessmentButton.prop('disabled', false).text('Submit Assessment'); // Re-enable button + showMessage($assessmentMessages, response.data.message || 'An error occurred during submission.', true); + $button.prop('disabled', false).text('Submit Assessment'); } }, error: function(jqXHR, textStatus, errorThrown) { - console.error("AJAX Error:", textStatus, errorThrown); - $assessmentFormMessages.addClass('error').text('A network error occurred. Please try again.'); - $submitAssessmentButton.prop('disabled', false).text('Submit Assessment'); // Re-enable button + console.error("AJAX Error (Submission):", textStatus, errorThrown); + showMessage($assessmentMessages, 'A network error occurred. Please try again.', true); + $button.prop('disabled', false).text('Submit Assessment'); } }); }); diff --git a/public/templates/assessment-shell.php b/public/templates/assessment-shell.php index ce011e0..ac95c4f 100644 --- a/public/templates/assessment-shell.php +++ b/public/templates/assessment-shell.php @@ -18,169 +18,199 @@ if ( ! defined( 'ABSPATH' ) ) { // Retrieve data passed from the handler $invitation_data = get_query_var( 'quiztech_invitation_data' ); -$current_step = get_query_var( 'quiztech_current_step' ); +$current_step = get_query_var( 'quiztech_current_step' ); // Note: This might be less relevant now with the landing page approach $pre_screening_questions = get_query_var( 'quiztech_pre_screening_questions' ); // Basic security check - ensure we have invitation data -if ( ! $invitation_data || ! $current_step ) { +if ( ! $invitation_data ) { // Should not happen if loaded correctly by the handler, but good practice. - wp_die( esc_html__( 'Could not load assessment data.', 'quiztech' ) ); + wp_die( esc_html__( 'Could not load assessment data. Invalid invitation.', 'quiztech' ) ); } -// Minimal HTML structure +// Fetch associated Job and Assessment details +$job_post = get_post( $invitation_data->job_id ); +$assessment_post = get_post( $invitation_data->assessment_id ); + +if ( ! $job_post || ! $assessment_post ) { + wp_die( esc_html__( 'Could not load associated job or assessment details.', 'quiztech' ) ); +} + +$job_title = get_the_title( $job_post ); +$assessment_title = get_the_title( $assessment_post ); +// Consider fetching job description/content if needed: $job_content = apply_filters('the_content', $job_post->post_content); + +// Determine if pre-screening should be displayed +$show_pre_screening = ! empty( $pre_screening_questions ) && is_array( $pre_screening_questions ) && $invitation_data->status === 'sent'; + ?> > - <?php echo esc_html( get_the_title( $invitation_data->assessment_id ) ?: __( 'Assessment', 'quiztech' ) ); ?> + <?php echo esc_html( $assessment_title ?: __( 'Assessment Invitation', 'quiztech' ) ); ?>
-

+ +
+

- -
+

+

+ + + ' . wp_kses_post( $job_content ) . '
'; + } + */ ?> + + +
+
+

+

+
+ +
+ + + + + $question ) : ?> +
+ + +
+ + +
+
+ + + + +
+ + +
-

-

-
- -
- - token, 'quiztech_prescreening_nonce' ); ?> - - token ); ?>"> ?> - - - - $question ) : ?> -
- - -
- - -

- Pre-screening Data: ' . esc_html( print_r( $pre_screening_questions, true ) ) . ''; - ?> - - - - -
-
- - -