diff --git a/public/js/assessment.js b/public/js/assessment.js index ac50c31..cacec2b 100644 --- a/public/js/assessment.js +++ b/public/js/assessment.js @@ -5,159 +5,224 @@ * and final assessment submission. */ (function($) { - 'use strict'; + 'use strict'; - $(function() { - console.log('Assessment script loaded.'); + $(function() { + 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.'); - return; - } + // Check if localized data is available + if (typeof quiztech_assessment_vars === 'undefined') { + console.error('Quiztech Assessment Error: Localized variables not found.'); + return; + } - var $prescreeningForm = $('#quiztech-prescreening-form'); - var $prescreeningSection = $('#quiztech-prescreening-section'); - var $assessmentSection = $('#quiztech-assessment-section'); - var $submitButton = $prescreeningForm.find('button[type="submit"]'); - var $formMessages = $('
').insertBefore($submitButton); // Area for messages + 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 - // --- Pre-Screening Form Handling --- - $prescreeningForm.on('submit', function(event) { - event.preventDefault(); // Stop traditional form submission + // Assessment specific elements + var $assessmentForm = $('#quiztech-assessment-form'); + var $questionsContainer = $('#quiztech-questions-container'); + var $questionContainers = $questionsContainer.find('.quiztech-question-container'); + var $timerDisplay = $('#quiztech-timer'); + var $nextButton = $('#quiztech-next-question'); + var $submitAssessmentButton = $('#quiztech-submit-assessment'); + var $completionMessage = $('#quiztech-completion-message'); - $formMessages.empty().removeClass('error success'); // Clear previous messages - $submitButton.prop('disabled', true).text('Submitting...'); // Disable button + // State variables + var currentQuestionIndex = 0; + var totalQuestions = $questionContainers.length; + var timerInterval; + var timerSeconds = 0; - var formData = $(this).serialize(); // Get form data + // --- Pre-Screening Form Handling --- + $prescreeningForm.on('submit', function(event) { + event.preventDefault(); // Stop traditional form submission - // Add required AJAX parameters - formData += '&action=quiztech_submit_prescreening'; - formData += '&nonce=' + quiztech_assessment_vars.prescreening_nonce; - formData += '&invitation_id=' + quiztech_assessment_vars.invitation_id; + // Use $preScreenFormMessages here + $preScreenFormMessages.empty().removeClass('error success'); // Clear previous messages + $submitButton.prop('disabled', true).text('Submitting...'); // Disable button - $.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 - $formMessages.addClass('success').text(response.data.message || 'Success!'); // Show success message briefly - $prescreeningSection.slideUp(); - $assessmentSection.slideDown(); - // No need to re-enable button as the form is gone - } else { - // Handle WP JSON error - $formMessages.addClass('error').text(response.data.message || 'An error occurred.'); - $submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button - } - }, - error: function(jqXHR, textStatus, errorThrown) { - // Handle general AJAX error - console.error("AJAX Error:", textStatus, errorThrown); - $formMessages.addClass('error').text('A network error occurred. Please try again.'); - $submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button - } - }); - }); + var formData = $(this).serialize(); // Get form data - // --- Assessment Answer Auto-Save --- - var $assessmentForm = $('#quiztech-assessment-form'); - var autoSaveTimeout; // To debounce requests + // Add required AJAX parameters + formData += '&action=quiztech_submit_prescreening'; + formData += '&nonce=' + quiztech_assessment_vars.prescreening_nonce; + formData += '&invitation_id=' + quiztech_assessment_vars.invitation_id; - // Target input/textarea/select elements within the assessment form for auto-save - $assessmentForm.on('change blur', 'input, textarea, select', function() { - clearTimeout(autoSaveTimeout); // Clear previous timeout if exists + $.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 + } 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 + } + }, + 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 + } + }); + }); - var $input = $(this); - var $questionGroup = $input.closest('.question-group'); - var questionId = $questionGroup.data('question-id'); - var answer = $input.val(); + // --- Assessment Answer Auto-Save --- + // var $assessmentForm = $('#quiztech-assessment-form'); // Already defined above + var autoSaveTimeout; // To debounce requests - // Add a small visual indicator within the question group - var $indicator = $questionGroup.find('.save-indicator'); - if ($indicator.length === 0) { - $indicator = $('').appendTo($questionGroup.find('label:first')); - } - $indicator.text('Saving...'); + // Target input/textarea/select elements within the assessment form for auto-save + $assessmentForm.on('change blur', 'input, textarea, select', function() { + clearTimeout(autoSaveTimeout); // Clear previous timeout if exists - // 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 - invitation_id: quiztech_assessment_vars.invitation_id, - question_id: questionId, - answer: answer - }, - dataType: 'json', - 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); - } 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); - } - }); - }, 500); // Wait 500ms after the last change/blur before sending - }); + var $input = $(this); + var $questionGroup = $input.closest('.quiztech-question-container'); // Updated selector + var questionId = $questionGroup.data('question-id'); + var answer = $input.val(); + + // Add a small visual indicator within the question group + var $indicator = $questionGroup.find('.save-indicator'); + if ($indicator.length === 0) { + $indicator = $('').appendTo($questionGroup.find('label:first')); + } + $indicator.text('Saving...'); + + // 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 + invitation_id: quiztech_assessment_vars.invitation_id, + question_id: questionId, + answer: answer + }, + dataType: 'json', + 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); + } 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); + } + }); + }, 500); // Wait 500ms after the last change/blur before sending + }); - // --- Final Assessment Submission --- - var $submitAssessmentButton = $('#quiztech-submit-assessment'); - var $assessmentFormMessages = $('
').insertAfter($submitAssessmentButton); // Area for messages + // --- Assessment Navigation --- + $nextButton.on('click', function() { + if (currentQuestionIndex < totalQuestions - 1) { + // Hide current, show next + $questionContainers.eq(currentQuestionIndex).addClass('quiztech-question-hidden'); + currentQuestionIndex++; + $questionContainers.eq(currentQuestionIndex).removeClass('quiztech-question-hidden'); + updateNavigationButtons(); + } + }); - $submitAssessmentButton.on('click', function(event) { - event.preventDefault(); // Stop traditional form submission (though AJAX auto-save handles data) + function updateNavigationButtons() { + if (currentQuestionIndex >= totalQuestions - 1) { + // Last question + $nextButton.hide(); + $submitAssessmentButton.show(); + } else { + $nextButton.show(); + $submitAssessmentButton.hide(); + } + } - if (!confirm('Are you sure you want to submit your assessment?')) { - return; // User cancelled - } + // --- Timer Functions --- + function startTimer() { + if (timerInterval) clearInterval(timerInterval); // Clear existing if any + timerSeconds = 0; // Reset timer + $timerDisplay.text(formatTime(timerSeconds)); // Initial display - $assessmentFormMessages.empty().removeClass('error success'); - $submitAssessmentButton.prop('disabled', true).text('Submitting...'); + timerInterval = setInterval(function() { + timerSeconds++; + $timerDisplay.text(formatTime(timerSeconds)); + }, 1000); + } - $.ajax({ - type: 'POST', - url: quiztech_assessment_vars.ajax_url, - data: { - action: 'quiztech_submit_assessment', - nonce: quiztech_assessment_vars.assessment_nonce, // Reuse assessment nonce - invitation_id: quiztech_assessment_vars.invitation_id - }, - dataType: 'json', - success: function(response) { - if (response.success) { - // Success! Display message and potentially hide the form/button - $assessmentFormMessages.addClass('success').text(response.data.message || 'Assessment Submitted Successfully!'); - $assessmentForm.hide(); // Hide the form after successful submission - // Optionally redirect: window.location.href = response.data.redirect_url; - } else { - $assessmentFormMessages.addClass('error').text(response.data.message || 'An error occurred during submission.'); - $submitAssessmentButton.prop('disabled', false).text('Submit Assessment'); // Re-enable button - } - }, - 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 - } - }); - }); + 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; + } - }); // End document ready + // --- 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) + + if (!confirm('Are you sure you want to submit your assessment?')) { + return; // User cancelled + } + + $assessmentFormMessages.empty().removeClass('error success'); + $submitAssessmentButton.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 + 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 + $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 + } + }, + 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 + } + }); + }); + + }); // End document ready })(jQuery); \ No newline at end of file diff --git a/public/templates/assessment-shell.php b/public/templates/assessment-shell.php index cf85986..ce011e0 100644 --- a/public/templates/assessment-shell.php +++ b/public/templates/assessment-shell.php @@ -48,6 +48,13 @@ if ( ! $invitation_data || ! $current_step ) { .form-group textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 3px; min-height: 80px; } button[type="submit"] { padding: 10px 20px; background-color: #0073aa; color: #fff; border: none; border-radius: 3px; cursor: pointer; } button[type="submit"]:hover { background-color: #005a87; } + .quiztech-timer { font-size: 1.2em; font-weight: bold; text-align: right; margin-bottom: 15px; padding: 5px; background-color: #f0f0f0; border-radius: 3px; } + .quiztech-question-container { border: 1px solid #eee; padding: 20px; margin-bottom: 20px; } + .quiztech-question-hidden { display: none; } + .quiztech-navigation { margin-top: 20px; text-align: right; } + .quiztech-navigation button { margin-left: 10px; padding: 10px 20px; background-color: #0073aa; color: #fff; border: none; border-radius: 3px; cursor: pointer; } + .quiztech-navigation button:hover { background-color: #005a87; } + #quiztech-completion-message { margin-top: 20px; padding: 15px; background-color: #dff0d8; border: 1px solid #d6e9c6; color: #3c763d; border-radius: 4px; } @@ -101,22 +108,25 @@ if ( ! $invitation_data || ! $current_step ) {

assessment_id ) ); ?>

+
00:00
+ assessment_id; // Fetch question IDs associated with the assessment (assuming stored in 'question_ids' meta field) - $question_ids = get_post_meta( $assessment_id, 'question_ids', true ); + $question_ids = get_post_meta( $assessment_id, '_quiztech_question_ids', true ); // Corrected meta key if ( is_array( $question_ids ) && ! empty( $question_ids ) ) : ?>
- token, 'quiztech_assessment_nonce' ); ?> - - - + + token, 'quiztech_assessment_nonce' ); ?> + token ); ?>"> ?> + "> ?> + ?> -

+
- + $question_id ) : ?> post_type ) { @@ -125,7 +135,11 @@ if ( ! $invitation_data || ! $current_step ) { $question_title = get_the_title( $question_post ); $question_type = get_post_meta( $question_id, 'question_type', true ); ?> -
+
+ +

@@ -165,11 +179,21 @@ if ( ! $invitation_data || ! $current_step ) {

-
+ as container provides separation ?> +
+ +
+ + +
- + + + ' . esc_html__( 'Could not load questions for this assessment.', 'quiztech' ) . '

'; diff --git a/src/Admin/AssessmentMetaboxes.php b/src/Admin/AssessmentMetaboxes.php index 27216bd..051c345 100644 --- a/src/Admin/AssessmentMetaboxes.php +++ b/src/Admin/AssessmentMetaboxes.php @@ -18,6 +18,7 @@ class AssessmentMetaboxes { public function register_hooks() { add_action( 'add_meta_boxes', [ $this, 'add_assessment_metaboxes' ] ); add_action( 'save_post_assessment', [ $this, 'save_linked_questions_meta' ] ); + add_action( 'save_post_assessment', [ $this, 'save_completion_message_meta' ] ); // Added hook for completion message save } /** @@ -32,9 +33,18 @@ class AssessmentMetaboxes { \__( 'Linked Questions', 'quiztech' ), [ $this, 'render_linked_questions_metabox' ], 'assessment', - 'normal', // Context below editor + 'normal', // Context 'high' ); + // Added metabox for completion message + \add_meta_box( + 'quiztech_completion_message_metabox', + \__( 'Completion Message', 'quiztech' ), + [ $this, 'render_completion_message_metabox' ], + 'assessment', + 'normal', // Context + 'low' // Priority + ); } } @@ -94,7 +104,6 @@ class AssessmentMetaboxes { if ( ! isset( $_POST['quiztech_linked_questions_nonce'] ) || ! \wp_verify_nonce( \sanitize_key( $_POST['quiztech_linked_questions_nonce'] ), 'quiztech_save_linked_questions_meta' ) ) { return; } if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! \current_user_can( 'edit_post', $post_id ) ) { return; } - // No need to check post type here, as the action is specific ('save_post_assessment') // Process submitted IDs $submitted_ids = []; @@ -106,4 +115,45 @@ class AssessmentMetaboxes { // Update meta (even if empty array, to clear previous selections) \update_post_meta( $post_id, '_quiztech_linked_question_ids', $submitted_ids ); } + + /** + * Renders the meta box content for the completion message. + * + * @param \WP_Post $post The post object. + */ + public function render_completion_message_metabox( $post ) { + \wp_nonce_field( 'quiztech_save_completion_message_meta', 'quiztech_completion_message_nonce' ); + $completion_message = \get_post_meta( $post->ID, '_quiztech_completion_message', true ); + + echo '

' . \esc_html__( 'Enter the message to display to applicants after they successfully submit this assessment. Basic HTML is allowed.', 'quiztech' ) . '

'; + echo ''; + } + + /** + * Saves the meta box data for the completion message. + * + * @param int $post_id The ID of the post being saved. + */ + public function save_completion_message_meta( $post_id ) { + // Basic checks (nonce, autosave, permissions) + if ( ! isset( $_POST['quiztech_completion_message_nonce'] ) || ! \wp_verify_nonce( \sanitize_key( $_POST['quiztech_completion_message_nonce'] ), 'quiztech_save_completion_message_meta' ) ) { return; } + if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } + if ( ! \current_user_can( 'edit_post', $post_id ) ) { return; } + + // Process submitted message + $new_message = ''; + if ( isset( $_POST['quiztech_completion_message_field'] ) ) { + // Allow basic HTML like links, paragraphs, bold, italics + $allowed_html = [ + 'a' => [ 'href' => [], 'title' => [], 'target' => [] ], + 'br' => [], + 'em' => [], + 'strong' => [], + 'p' => [], + ]; + $new_message = \wp_kses( wp_unslash( $_POST['quiztech_completion_message_field'] ), $allowed_html ); + } + + \update_post_meta( $post_id, '_quiztech_completion_message', $new_message ); + } } \ No newline at end of file diff --git a/src/Includes/Ajax/AssessmentAjaxHandler.php b/src/Includes/Ajax/AssessmentAjaxHandler.php index 2268691..3a909a4 100644 --- a/src/Includes/Ajax/AssessmentAjaxHandler.php +++ b/src/Includes/Ajax/AssessmentAjaxHandler.php @@ -2,296 +2,301 @@ namespace Quiztech\AssessmentPlatform\Includes\Ajax; +use Quiztech\AssessmentPlatform\Includes\Invitations; // Added use statement + /** * Handles AJAX requests related to the front-end assessment process. */ class AssessmentAjaxHandler { - /** - * Constructor. Registers AJAX hooks. - */ - public function __construct() { - add_action('wp_ajax_quiztech_submit_prescreening', [$this, 'handle_submit_prescreening']); - add_action('wp_ajax_quiztech_save_answer', [$this, 'handle_save_answer']); - add_action('wp_ajax_quiztech_submit_assessment', [$this, 'handle_submit_assessment']); - } + /** + * Constructor. Registers AJAX hooks. + */ + public function __construct() { + add_action('wp_ajax_quiztech_submit_prescreening', [$this, 'handle_submit_prescreening']); + add_action('wp_ajax_quiztech_save_answer', [$this, 'handle_save_answer']); + add_action('wp_ajax_quiztech_submit_assessment', [$this, 'handle_submit_assessment']); + } - /** - * Initialize the handler. - * Static method to instantiate the class and register hooks. - */ - public static function init() { - new self(); - } + /** + * Initialize the handler. + * Static method to instantiate the class and register hooks. + */ + public static function init() { + new self(); + } - /** - * Helper method to find an existing user_evaluation CPT by invitation ID - * or create a new one if it doesn't exist. - * - * @param int $invitation_id The database ID of the invitation record. - * @return int The post ID of the user_evaluation CPT, or 0 on failure. - */ - private function get_or_create_user_evaluation(int $invitation_id): int { - if ( ! $invitation_id ) { - error_log("Quiztech AJAX Error: get_or_create_user_evaluation called with invalid invitation ID: " . $invitation_id); - return 0; - } + /** + * Helper method to find an existing user_evaluation CPT by invitation ID + * or create a new one if it doesn't exist. + * + * @param int $invitation_id The database ID of the invitation record. + * @return int The post ID of the user_evaluation CPT, or 0 on failure. + */ + private function get_or_create_user_evaluation(int $invitation_id): int { + if ( ! $invitation_id ) { + error_log("Quiztech AJAX Error: get_or_create_user_evaluation called with invalid invitation ID: " . $invitation_id); + return 0; + } - $args = [ - 'post_type' => 'user_evaluation', - 'post_status' => 'any', // Find it regardless of status initially - 'meta_query' => [ - [ - 'key' => 'quiztech_invitation_id', - 'value' => $invitation_id, - 'compare' => '=', - ] - ], - 'posts_per_page' => 1, - 'fields' => 'ids', // Only get the ID - ]; - $evaluation_posts = get_posts($args); + $args = [ + 'post_type' => 'user_evaluation', + 'post_status' => 'any', // Find it regardless of status initially + 'meta_query' => [ + [ + 'key' => 'quiztech_invitation_id', + 'value' => $invitation_id, + 'compare' => '=', + ] + ], + 'posts_per_page' => 1, + 'fields' => 'ids', // Only get the ID + ]; + $evaluation_posts = get_posts($args); - if ( ! empty( $evaluation_posts ) ) { - // Found existing evaluation - return $evaluation_posts[0]; - } else { - // Not found, create a new one - $post_data = [ - 'post_type' => 'user_evaluation', - 'post_status' => 'publish', // Start as published (or maybe 'pending'/'in-progress' if custom statuses are added) - 'post_title' => sprintf( __( 'Evaluation for Invitation #%d', 'quiztech' ), $invitation_id ), - 'post_content' => '', // No content needed initially - // 'post_author' => ?? // Assign to an admin or system user? Default is current user (likely none in AJAX) - ]; - $evaluation_id = wp_insert_post( $post_data, true ); // Pass true for WP_Error on failure + if ( ! empty( $evaluation_posts ) ) { + // Found existing evaluation + return $evaluation_posts[0]; + } else { + // Not found, create a new one + $post_data = [ + 'post_type' => 'user_evaluation', + 'post_status' => 'publish', // Start as published (or maybe 'pending'/'in-progress' if custom statuses are added) + 'post_title' => sprintf( __( 'Evaluation for Invitation #%d', 'quiztech' ), $invitation_id ), + 'post_content' => '', // No content needed initially + // 'post_author' => ?? // Assign to an admin or system user? Default is current user (likely none in AJAX) + ]; + $evaluation_id = wp_insert_post( $post_data, true ); // Pass true for WP_Error on failure - if ( is_wp_error( $evaluation_id ) ) { - error_log("Quiztech AJAX Error: Failed to create user_evaluation CPT for invitation ID {$invitation_id}: " . $evaluation_id->get_error_message()); - return 0; - } + if ( is_wp_error( $evaluation_id ) ) { + error_log("Quiztech AJAX Error: Failed to create user_evaluation CPT for invitation ID {$invitation_id}: " . $evaluation_id->get_error_message()); + return 0; + } - // Add the linking meta field - $meta_updated = update_post_meta( $evaluation_id, 'quiztech_invitation_id', $invitation_id ); - if ( ! $meta_updated ) { - // Log error, but maybe don't fail the whole request? Or should we delete the post? - error_log("Quiztech AJAX Warning: Failed to add quiztech_invitation_id meta to new evaluation ID {$evaluation_id} for invitation ID {$invitation_id}."); - // Depending on requirements, might return 0 here or proceed. Let's proceed for now. - } + // Add the linking meta field + $meta_updated = update_post_meta( $evaluation_id, 'quiztech_invitation_id', $invitation_id ); + if ( ! $meta_updated ) { + // Log error, but maybe don't fail the whole request? Or should we delete the post? + error_log("Quiztech AJAX Warning: Failed to add quiztech_invitation_id meta to new evaluation ID {$evaluation_id} for invitation ID {$invitation_id}."); + // Depending on requirements, might return 0 here or proceed. Let's proceed for now. + } - error_log("Quiztech AJAX Info: Created new user_evaluation CPT ID {$evaluation_id} for invitation ID {$invitation_id}."); - return $evaluation_id; - } - } + error_log("Quiztech AJAX Info: Created new user_evaluation CPT ID {$evaluation_id} for invitation ID {$invitation_id}."); + return $evaluation_id; + } + } - // AJAX handler methods will be added below: - // - handle_submit_prescreening() - // - handle_save_answer() - // - handle_submit_assessment() + /** + * Handles the AJAX submission of the pre-screening form. + * Expects 'nonce', 'invitation_id', and 'pre_screen_answer' array in $_POST. + */ + public function handle_submit_prescreening() { + // 1. Verify Nonce + check_ajax_referer('quiztech_prescreening_nonce', 'nonce'); - /** - * Handles the AJAX submission of the pre-screening form. - * Expects 'nonce', 'invitation_id', and 'pre_screen_answer' array in $_POST. - */ - public function handle_submit_prescreening() { - // 1. Verify Nonce - check_ajax_referer('quiztech_prescreening_nonce', 'nonce'); + // 2. Get and Sanitize Core Data + $invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0; + if ( ! $invitation_id ) { + wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400); + } - // 2. Get and Sanitize Core Data - $invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0; - if ( ! $invitation_id ) { - wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400); - } + // 3. Get or Create User Evaluation Record + $evaluation_id = $this->get_or_create_user_evaluation($invitation_id); + if ( ! $evaluation_id ) { + error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id); + wp_send_json_error(['message' => __('Could not process evaluation record.', 'quiztech')], 500); + } - // 3. Get or Create User Evaluation Record - $evaluation_id = $this->get_or_create_user_evaluation($invitation_id); - if ( ! $evaluation_id ) { - error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id); - wp_send_json_error(['message' => __('Could not process evaluation record.', 'quiztech')], 500); - } + // 4. Sanitize Submitted Answers + $submitted_answers = isset($_POST['pre_screen_answer']) && is_array($_POST['pre_screen_answer']) ? $_POST['pre_screen_answer'] : []; + $sanitized_answers = []; + foreach ($submitted_answers as $index => $answer) { + // Use sanitize_textarea_field as pre-screening questions are currently textareas + $sanitized_answers[sanitize_key($index)] = sanitize_textarea_field(wp_unslash($answer)); + } - // 4. Sanitize Submitted Answers - $submitted_answers = isset($_POST['pre_screen_answer']) && is_array($_POST['pre_screen_answer']) ? $_POST['pre_screen_answer'] : []; - $sanitized_answers = []; - foreach ($submitted_answers as $index => $answer) { - // Use sanitize_textarea_field as pre-screening questions are currently textareas - $sanitized_answers[sanitize_key($index)] = sanitize_textarea_field(wp_unslash($answer)); - } + // 4. Save Answers (as user_evaluation CPT meta) + if (!empty($sanitized_answers)) { + update_post_meta($evaluation_id, 'quiztech_prescreening_answers', $sanitized_answers); + } else { + // Handle case where no answers were submitted? Or rely on form 'required' attribute? + // For now, proceed even if empty. + } - // 4. Save Answers (as user_evaluation CPT meta) - if (!empty($sanitized_answers)) { - update_post_meta($evaluation_id, 'quiztech_prescreening_answers', $sanitized_answers); - } else { - // Handle case where no answers were submitted? Or rely on form 'required' attribute? - // For now, proceed even if empty. - } - - // 5. Update Invitation Status - try { - $invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations(); - // Note: The update_status method expects the invitation *record ID*, not the token. - // We need to retrieve the invitation ID based on the token if we only have the token here. - // Assuming $invitation_id passed in POST *is* the record ID for now. If it's the token, this needs adjustment. - $updated = $invitations->update_status($invitation_id, 'pre-screening-complete'); - if (!$updated) { - error_log("Quiztech AJAX Error: Failed to update invitation status for ID: " . $invitation_id); - // Decide if this should be a user-facing error or just logged - } - } catch (\Exception $e) { - error_log("Quiztech AJAX Error: Exception updating invitation status: " . $e->getMessage()); - // Decide if this should be a user-facing error - } + // 5. Update Invitation Status + try { + $invitations = new Invitations(); + $updated = $invitations->update_status($invitation_id, 'pre-screening-complete'); + if (!$updated) { + error_log("Quiztech AJAX Error: Failed to update invitation status for ID: " . $invitation_id); + // Decide if this should be a user-facing error or just logged + } + } catch (\Exception $e) { + error_log("Quiztech AJAX Error: Exception updating invitation status: " . $e->getMessage()); + // Decide if this should be a user-facing error + } - // 6. Send Response - wp_send_json_success(['message' => __('Pre-screening submitted successfully. Starting assessment...', 'quiztech')]); + // 6. Send Response + wp_send_json_success(['message' => __('Pre-screening submitted successfully. Starting assessment...', 'quiztech')]); - // Ensure script execution stops - wp_die(); - } + // Ensure script execution stops + wp_die(); + } - /** - * Handles the AJAX auto-save of a single assessment answer. - * Expects 'nonce', 'invitation_id', 'question_id', and 'answer' in $_POST. - */ - public function handle_save_answer() { - // 1. Verify Nonce - check_ajax_referer('quiztech_assessment_nonce', 'nonce'); + /** + * Handles the AJAX auto-save of a single assessment answer. + * Expects 'nonce', 'invitation_id', 'question_id', and 'answer' in $_POST. + */ + public function handle_save_answer() { + // 1. Verify Nonce + check_ajax_referer('quiztech_assessment_nonce', 'nonce'); - // 2. Get and Sanitize Data - $invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0; - $question_id = isset($_POST['question_id']) ? absint($_POST['question_id']) : 0; - $answer = isset($_POST['answer']) ? wp_unslash($_POST['answer']) : ''; // Sanitize based on question type later + // 2. Get and Sanitize Data + $invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0; + $question_id = isset($_POST['question_id']) ? absint($_POST['question_id']) : 0; + $answer = isset($_POST['answer']) ? wp_unslash($_POST['answer']) : ''; // Sanitize based on question type later - // Basic validation for required IDs before querying - if ( ! $invitation_id || ! $question_id ) { - wp_send_json_error(['message' => __('Missing required data for saving answer.', 'quiztech')], 400); - } + // Basic validation for required IDs before querying + if ( ! $invitation_id || ! $question_id ) { + wp_send_json_error(['message' => __('Missing required data for saving answer.', 'quiztech')], 400); + } - // 3. Get or Create User Evaluation Record - $evaluation_id = $this->get_or_create_user_evaluation($invitation_id); - if ( ! $evaluation_id ) { - error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during answer save."); - wp_send_json_error(['message' => __('Could not process evaluation record for saving answer.', 'quiztech')], 500); - } + // 3. Get or Create User Evaluation Record + $evaluation_id = $this->get_or_create_user_evaluation($invitation_id); + if ( ! $evaluation_id ) { + error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during answer save."); + wp_send_json_error(['message' => __('Could not process evaluation record for saving answer.', 'quiztech')], 500); + } - // 4. Fetch the question type meta for the given question ID - $question_type = \get_post_meta($question_id, '_quiztech_question_type', true); - if ( ! $question_type ) { - // Log if type is missing, but proceed with default sanitization - error_log("Quiztech AJAX Warning: Missing question type meta for question ID: " . $question_id); - $question_type = 'text'; // Default to text if not set - } + // 4. Fetch the question type meta for the given question ID + $question_type = \get_post_meta($question_id, '_quiztech_question_type', true); + if ( ! $question_type ) { + // Log if type is missing, but proceed with default sanitization + error_log("Quiztech AJAX Warning: Missing question type meta for question ID: " . $question_id); + $question_type = 'text'; // Default to text if not set + } - // Sanitize the answer based on question type - $sanitized_answer = ''; // Initialize + // Sanitize the answer based on question type + $sanitized_answer = ''; // Initialize - if (is_array($answer)) { - // Handle array answers (likely checkboxes) - if ('checkbox' === $question_type) { - // Sanitize each value in the array - $sanitized_answer = array_map('sanitize_text_field', $answer); - // Note: update_post_meta can handle arrays directly, storing them serialized. - } else { - // Unexpected array answer for this question type - error_log("Quiztech AJAX Error: Received array answer for non-checkbox question ID: " . $question_id); - // Sanitize by joining elements (simple approach, might need refinement) - $sanitized_answer = sanitize_text_field(implode(', ', $answer)); - } - } else { - // Handle string/scalar answers - switch ($question_type) { - case 'textarea': - $sanitized_answer = sanitize_textarea_field($answer); - break; - case 'numeric': - // Allow integers and potentially floats. Use floatval for broader acceptance. - // Ensure it's actually numeric before casting to avoid warnings/errors. - $sanitized_answer = is_numeric($answer) ? floatval($answer) : 0; - break; - case 'multiple-choice': // Assuming the value is a simple key/identifier - $sanitized_answer = sanitize_key($answer); - break; - case 'text': - default: // Default to sanitize_text_field for 'text' or unknown/missing types - $sanitized_answer = sanitize_text_field($answer); - break; - } - } + if (is_array($answer)) { + // Handle array answers (likely checkboxes) + if ('checkbox' === $question_type) { + // Sanitize each value in the array + $sanitized_answer = array_map('sanitize_text_field', $answer); + // Note: update_post_meta can handle arrays directly, storing them serialized. + } else { + // Unexpected array answer for this question type + error_log("Quiztech AJAX Error: Received array answer for non-checkbox question ID: " . $question_id); + // Sanitize by joining elements (simple approach, might need refinement) + $sanitized_answer = sanitize_text_field(implode(', ', $answer)); + } + } else { + // Handle string/scalar answers + switch ($question_type) { + case 'textarea': + $sanitized_answer = sanitize_textarea_field($answer); + break; + case 'numeric': + // Allow integers and potentially floats. Use floatval for broader acceptance. + // Ensure it's actually numeric before casting to avoid warnings/errors. + $sanitized_answer = is_numeric($answer) ? floatval($answer) : 0; + break; + case 'multiple-choice': // Assuming the value is a simple key/identifier + $sanitized_answer = sanitize_key($answer); + break; + case 'text': + default: // Default to sanitize_text_field for 'text' or unknown/missing types + $sanitized_answer = sanitize_text_field($answer); + break; + } + } - // 3. Save Answer (as user_evaluation CPT meta) - // Use a meta key structure like 'quiztech_answer_{question_id}' or store in a single array meta field. - // Using individual meta keys might be simpler for querying later if needed. - $meta_key = 'quiztech_answer_' . $question_id; - update_post_meta($evaluation_id, $meta_key, $sanitized_answer); + // 3. Save Answer (as user_evaluation CPT meta) + // Use a meta key structure like 'quiztech_answer_{question_id}' or store in a single array meta field. + // Using individual meta keys might be simpler for querying later if needed. + $meta_key = 'quiztech_answer_' . $question_id; + update_post_meta($evaluation_id, $meta_key, $sanitized_answer); - // 4. Send Response - wp_send_json_success(['message' => __('Answer saved.', 'quiztech')]); + // 4. Send Response + wp_send_json_success(['message' => __('Answer saved.', 'quiztech')]); - // Ensure script execution stops - wp_die(); - } + // Ensure script execution stops + wp_die(); + } - /** - * Handles the final AJAX submission of the assessment. - * Expects 'nonce' and 'invitation_id' in $_POST. - */ - public function handle_submit_assessment() { - // 1. Verify Nonce - check_ajax_referer('quiztech_assessment_nonce', 'nonce'); // Reuse assessment nonce + /** + * Handles the final AJAX submission of the assessment. + * Expects 'nonce' and 'invitation_id' in $_POST. + */ + public function handle_submit_assessment() { + // 1. Verify Nonce + check_ajax_referer('quiztech_assessment_nonce', 'nonce'); // Reuse assessment nonce - // 2. Get Data - $invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0; - if ( ! $invitation_id ) { - wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400); - } + // 2. Get Data + $invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0; + if ( ! $invitation_id ) { + wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400); + } - // 3. Get or Create User Evaluation Record - $evaluation_id = $this->get_or_create_user_evaluation($invitation_id); - if ( ! $evaluation_id ) { - error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during final submission."); - wp_send_json_error(['message' => __('Could not process evaluation record for submission.', 'quiztech')], 500); - } + // 3. Get or Create User Evaluation Record + $evaluation_id = $this->get_or_create_user_evaluation($invitation_id); + if ( ! $evaluation_id ) { + error_log("Quiztech AJAX Error: Failed to get or create user_evaluation for invitation ID: " . $invitation_id . " during final submission."); + wp_send_json_error(['message' => __('Could not process evaluation record for submission.', 'quiztech')], 500); + } - // 4. Update Invitation Status - try { - $invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations(); - // Note: The update_status method expects the invitation *record ID*, not the token. - // We need to retrieve the invitation ID based on the token if we only have the token here. - // Assuming $invitation_id passed in POST *is* the record ID for now. If it's the token, this needs adjustment. - $updated = $invitations->update_status($invitation_id, 'assessment-complete'); - if (!$updated) { - error_log("Quiztech AJAX Error: Failed to update invitation status to complete for ID: " . $invitation_id); - // Decide if this should be a user-facing error or just logged - } - } catch (\Exception $e) { - error_log("Quiztech AJAX Error: Exception updating invitation status to complete: " . $e->getMessage()); - // Decide if this should be a user-facing error - } + // 4. Update Invitation Status + try { + $invitations = new Invitations(); + // Assuming $invitation_id passed in POST *is* the record ID. + $updated = $invitations->update_status($invitation_id, 'assessment-complete'); + if (!$updated) { + error_log("Quiztech AJAX Error: Failed to update invitation status to complete for ID: " . $invitation_id); + // Decide if this should be a user-facing error or just logged + } + } catch (\Exception $e) { + error_log("Quiztech AJAX Error: Exception updating invitation status to complete: " . $e->getMessage()); + // Decide if this should be a user-facing error + } - // 4. Update User Evaluation CPT Status to 'completed' - $post_update_data = [ - 'ID' => $evaluation_id, - 'post_status' => 'completed', // Use a custom status if needed, but 'completed' seems appropriate - ]; - $post_updated = wp_update_post($post_update_data, true); // Pass true for WP_Error object on failure + // 5. Update User Evaluation CPT Status to 'completed' + $post_update_data = [ + 'ID' => $evaluation_id, + 'post_status' => 'completed', // Use a custom status if needed, but 'completed' seems appropriate + ]; + $post_updated = wp_update_post($post_update_data, true); // Pass true for WP_Error object on failure - if (is_wp_error($post_updated)) { - error_log("Quiztech AJAX Error: Failed to update user_evaluation CPT status for ID {$evaluation_id}: " . $post_updated->get_error_message()); - // Decide if this should be a user-facing error - // wp_send_json_error(['message' => __('Failed to finalize assessment record.', 'quiztech')], 500); - } + if (is_wp_error($post_updated)) { + error_log("Quiztech AJAX Error: Failed to update user_evaluation CPT status for ID {$evaluation_id}: " . $post_updated->get_error_message()); + // Decide if this should be a user-facing error + // wp_send_json_error(['message' => __('Failed to finalize assessment record.', 'quiztech')], 500); + } - // 5. Send Response - // Future Enhancement: Consider adding a redirect URL or specific completion message/HTML - // to the response data based on plugin settings or other logic. - wp_send_json_success(['message' => __('Assessment submitted successfully!', 'quiztech')]); + // 6. Get the Assessment ID associated with the invitation + // This requires a method in Invitations class to get the full record by ID + $invitation_record = $invitations->get_invitation_by_id($invitation_id); // Assuming this method exists or will be added + $assessment_id = $invitation_record ? $invitation_record->assessment_id : 0; - // Ensure script execution stops - wp_die(); - } + // 7. Get the custom completion message + $completion_message = ''; + if ($assessment_id) { + $completion_message = \get_post_meta($assessment_id, '_quiztech_completion_message', true); + } + // Use a default message if the custom one is empty + if (empty($completion_message)) { + $completion_message = __('Assessment submitted successfully!', 'quiztech'); + } + + // 8. Send Response + wp_send_json_success(['completionMessage' => $completion_message]); // Send completion message + + // Ensure script execution stops + wp_die(); + } } \ No newline at end of file diff --git a/src/Includes/FrontendHandler.php b/src/Includes/FrontendHandler.php index 10cd1cf..9f92620 100644 --- a/src/Includes/FrontendHandler.php +++ b/src/Includes/FrontendHandler.php @@ -52,9 +52,9 @@ class FrontendHandler { // Determine the current step (pre-screening or assessment) $current_step = 'assessment'; // Default to assessment - // If pre-screening questions exist AND the invitation status is still 'pending' (or similar initial state), show pre-screening. - // Assumes 'pending' is the initial status before viewing/pre-screening. Adjust if needed. - if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'pending' ) { + // If pre-screening questions exist AND the invitation status is still 'sent' (the initial state), show pre-screening. + // Once pre-screening is submitted, the status should change (e.g., to 'viewed', 'pre-screening complete'). + if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'sent' ) { $current_step = 'pre_screening'; } diff --git a/src/Includes/Invitations.php b/src/Includes/Invitations.php index f833506..519d994 100644 --- a/src/Includes/Invitations.php +++ b/src/Includes/Invitations.php @@ -6,185 +6,211 @@ namespace Quiztech\AssessmentPlatform\Includes; */ class Invitations { - /** - * Generate a cryptographically secure unique token for an invitation. - * - * @return string The generated unique token. - */ - public function generate_unique_token() { - // Placeholder for token generation logic - // Consider using wp_generate_password() or random_bytes() - \error_log('Invitation Token Generation Called - Placeholder'); - return bin2hex(random_bytes(16)); // Example placeholder - } + /** + * Generate a cryptographically secure unique token for an invitation. + * + * @return string The generated unique token. + */ + public function generate_unique_token() { + // Placeholder for token generation logic + // Consider using wp_generate_password() or random_bytes() + \error_log('Invitation Token Generation Called - Placeholder'); + return bin2hex(random_bytes(16)); // Example placeholder + } - /** - * Create and store an invitation record. - * - * @param int $job_id The ID of the job associated with the invitation. - * @param int $assessment_id The ID of the assessment associated with the invitation. - * @param string $applicant_email The email address of the applicant being invited. - * @return string|\WP_Error The generated token on success, or \WP_Error on failure. - */ - public function create_invitation( $job_id, $assessment_id, $applicant_email ) { - global $wpdb; - $table_name = $wpdb->prefix . 'quiztech_invitations'; - - $token = $this->generate_unique_token(); - - $data = [ - 'token' => $token, - 'job_id' => absint( $job_id ), - 'assessment_id' => absint( $assessment_id ), - 'applicant_email' => sanitize_email( $applicant_email ), - 'status' => 'pending', - 'created_timestamp' => current_time( 'mysql', 1 ), // GMT time - // 'expiry_timestamp' => null, // Set if expiry is needed - ]; - - $format = [ - '%s', // token - '%d', // job_id - '%d', // assessment_id - '%s', // applicant_email - '%s', // status - '%s', // created_timestamp - // '%s', // expiry_timestamp - ]; - - $inserted = $wpdb->insert( $table_name, $data, $format ); - - if ( false === $inserted ) { - \error_log( 'Quiztech Error: Failed to insert invitation record. DB Error: ' . $wpdb->last_error ); - return new \WP_Error( 'invitation_db_error', __( 'Could not save the invitation record.', 'quiztech' ), [ 'status' => 500 ] ); - } - - return $token; - } + /** + * Create and store an invitation record. + * + * @param int $job_id The ID of the job associated with the invitation. + * @param int $assessment_id The ID of the assessment associated with the invitation. + * @param string $applicant_email The email address of the applicant being invited. + * @return int|\WP_Error The database ID of the new invitation record on success, or \WP_Error on failure. + */ + public function create_invitation( $job_id, $assessment_id, $applicant_email ) { + global $wpdb; + $table_name = $wpdb->prefix . 'quiztech_invitations'; - /** - * Send the invitation email to the applicant. - * - * @param string $applicant_email The recipient's email address. - * @param string $token The unique invitation token. - * @param array $job_details Optional details about the job for the email body. - * @return bool True on success, false on failure. - */ - public function send_invitation_email( $applicant_email, $token, $job_details = [] ) { - // Placeholder for email sending logic - // 1. Construct the invitation URL (e.g., \site_url('/assessment-invite/?token=' . $token)) - // 2. Create email subject and body (using $job_details if provided). - // 3. Use \wp_mail() to send the email. - // 4. Handle success/failure of \wp_mail(). + $token = $this->generate_unique_token(); - \error_log('Send Invitation Email Called - Placeholder'); - $invite_url = \site_url('/assessment-invite/?token=' . $token); // Corrected line 71 - $subject = 'You are invited to take an assessment'; - $message = "Please click the following link to take your assessment:\n\n" . $invite_url; - // $headers = ['Content-Type: text/html; charset=UTF-8']; // If sending HTML email + $data = [ + 'token' => $token, + 'job_id' => absint( $job_id ), + 'assessment_id' => absint( $assessment_id ), + 'applicant_email' => sanitize_email( $applicant_email ), + 'status' => 'sent', // Updated initial status based on user feedback + 'created_timestamp' => current_time( 'mysql', 1 ), // GMT time + // 'expiry_timestamp' => null, // Set if expiry is needed + ]; - $sent = \wp_mail($applicant_email, $subject, $message); // Corrected line 76 - return $sent; - } + $format = [ + '%s', // token + '%d', // job_id + '%d', // assessment_id + '%s', // applicant_email + '%s', // status + '%s', // created_timestamp + // '%s', // expiry_timestamp + ]; - /** - * Validate an incoming invitation token. - * - * @param string $token The token to validate. - * @return bool|\WP_Error True if valid, false if invalid/expired/used, \WP_Error on error. - */ - public function validate_token( $token ) { - global $wpdb; - $table_name = $wpdb->prefix . 'quiztech_invitations'; - - // Basic sanitization - ensure it looks like our expected token format (32 hex chars) - if ( ! preg_match( '/^[a-f0-9]{32}$/', $token ) ) { - return false; // Invalid token format - } - - $invitation = $wpdb->get_row( - $wpdb->prepare( - "SELECT * FROM $table_name WHERE token = %s", - $token - ) - ); - - if ( ! $invitation ) { - \error_log( "Quiztech Info: Invitation token not found: $token" ); - return false; // Token doesn't exist - } - - if ( 'pending' !== $invitation->status ) { - \error_log( "Quiztech Info: Invitation token already used or expired: $token (Status: $invitation->status)" ); - return false; // Token not in pending state (already used, completed, expired etc.) - } - - // Optional: Check expiry_timestamp if implemented - // if ( $invitation->expiry_timestamp && strtotime( $invitation->expiry_timestamp ) < time() ) { - // // Optionally update status to 'expired' here - // return false; // Token expired - // } - - // Token is valid and pending - // Optionally update status to 'viewed' here if needed - // $wpdb->update($table_name, ['status' => 'viewed'], ['id' => $invitation->id], ['%s'], ['%d']); - - return $invitation; // Return the invitation data object if valid - } + $inserted = $wpdb->insert( $table_name, $data, $format ); - /** - * Update the status of an invitation record. - * - * @param int $invitation_id The ID of the invitation record to update. - * @param string $new_status The new status to set (e.g., 'pre-screening-complete', 'assessment-complete', 'expired'). - * @return bool True on successful update, false on failure or invalid input. - */ - public function update_status( $invitation_id, $new_status ) { - global $wpdb; - $table_name = $wpdb->prefix . 'quiztech_invitations'; + if ( false === $inserted ) { + \error_log( 'Quiztech Error: Failed to insert invitation record. DB Error: ' . $wpdb->last_error ); + return new \WP_Error( 'invitation_db_error', __( 'Could not save the invitation record.', 'quiztech' ), [ 'status' => 500 ] ); + } - // Validate input - $invitation_id = absint( $invitation_id ); - $new_status = sanitize_text_field( $new_status ); // Basic sanitization + // Return the DB ID of the inserted record, not the token, as ID is used elsewhere + return $wpdb->insert_id; + } - if ( ! $invitation_id || empty( $new_status ) ) { - \error_log( 'Quiztech Error: Invalid input provided to update_status.' ); - return false; - } + /** + * Send the invitation email to the applicant. + * + * @param string $applicant_email The recipient's email address. + * @param string $token The unique invitation token. + * @param array $job_details Optional details about the job for the email body. + * @return bool True on success, false on failure. + */ + public function send_invitation_email( $applicant_email, $token, $job_details = [] ) { + // Placeholder for email sending logic + // 1. Construct the invitation URL (e.g., \site_url('/assessment-invite/?token=' . $token)) + // 2. Create email subject and body (using $job_details if provided). + // 3. Use \wp_mail() to send the email. + // 4. Handle success/failure of \wp_mail(). - // Define allowed statuses to prevent arbitrary values - $allowed_statuses = [ - 'pending', - 'viewed', // Optional status if needed - 'pre-screening-complete', - 'assessment-started', // Optional status - 'assessment-complete', - 'expired', - 'cancelled', // Optional status - ]; + \error_log('Send Invitation Email Called - Placeholder'); + $invite_url = \site_url('/assessment-invite/?token=' . $token); // Corrected line 71 + $subject = 'You are invited to take an assessment'; + $message = "Please click the following link to take your assessment:\n\n" . $invite_url; + // $headers = ['Content-Type: text/html; charset=UTF-8']; // If sending HTML email - if ( ! in_array( $new_status, $allowed_statuses, true ) ) { - \error_log( "Quiztech Error: Invalid status '{$new_status}' provided to update_status for invitation ID {$invitation_id}." ); - return false; - } + $sent = \wp_mail($applicant_email, $subject, $message); // Corrected line 76 + return $sent; + } - // Prepare data and format for update - $data = [ 'status' => $new_status ]; - $where = [ 'id' => $invitation_id ]; // Assuming 'id' is the primary key column name - $format = [ '%s' ]; // Format for data - $where_format = [ '%d' ]; // Format for where clause + /** + * Validate an incoming invitation token. + * Checks if token exists and is in 'sent' or 'pre-screening-complete' status. + * + * @param string $token The token to validate. + * @return object|null The invitation data object if valid and ready for assessment, null otherwise. + */ + public function validate_token( $token ) { + global $wpdb; + $table_name = $wpdb->prefix . 'quiztech_invitations'; - $updated = $wpdb->update( $table_name, $data, $where, $format, $where_format ); + // Basic sanitization - ensure it looks like our expected token format (32 hex chars) + if ( ! preg_match( '/^[a-f0-9]{32}$/', $token ) ) { + return null; // Invalid token format + } - if ( false === $updated ) { - \error_log( "Quiztech Error: Failed to update invitation status for ID {$invitation_id}. DB Error: " . $wpdb->last_error ); - return false; - } + $invitation = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM $table_name WHERE token = %s", + $token + ) + ); - // $updated contains the number of rows affected. - // Return true if one or more rows were updated (or potentially 0 if the status was already set to the new value). - // We consider 0 rows affected as success in the case the status was already correct. - return true; - } + if ( ! $invitation ) { + \error_log( "Quiztech Info: Invitation token not found: $token" ); + return null; // Token doesn't exist + } + + // Allow access if status is 'sent' (initial) or 'pre-screening-complete' + $allowed_statuses_for_access = ['sent', 'pre-screening-complete']; + if ( ! in_array($invitation->status, $allowed_statuses_for_access, true) ) { + \error_log( "Quiztech Info: Invitation token not in an accessible state: $token (Status: $invitation->status)" ); + return null; // Token not in an accessible state (already used, completed, expired etc.) + } + + // Optional: Check expiry_timestamp if implemented + // if ( $invitation->expiry_timestamp && strtotime( $invitation->expiry_timestamp ) < time() ) { + // // Optionally update status to 'expired' here + // return null; // Token expired + // } + + // Token is valid and in an accessible state + return $invitation; // Return the invitation data object + } + + /** + * Retrieve an invitation record by its database ID. + * + * @param int $invitation_id The ID of the invitation record. + * @return object|null The invitation data object if found, null otherwise. + */ + public function get_invitation_by_id( int $invitation_id ) { + global $wpdb; + $table_name = $wpdb->prefix . 'quiztech_invitations'; + + if ( $invitation_id <= 0 ) { + return null; + } + + $invitation = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM $table_name WHERE id = %d", + $invitation_id + ) + ); + + return $invitation; // Returns object or null if not found + } + + + /** + * Update the status of an invitation record. + * + * @param int $invitation_id The ID of the invitation record to update. + * @param string $new_status The new status to set (e.g., 'pre-screening-complete', 'assessment-complete', 'expired'). + * @return bool True on successful update, false on failure or invalid input. + */ + public function update_status( $invitation_id, $new_status ) { + global $wpdb; + $table_name = $wpdb->prefix . 'quiztech_invitations'; + + // Validate input + $invitation_id = absint( $invitation_id ); + $new_status = sanitize_text_field( $new_status ); // Basic sanitization + + if ( ! $invitation_id || empty( $new_status ) ) { + \error_log( 'Quiztech Error: Invalid input provided to update_status.' ); + return false; + } + + // Define allowed statuses to prevent arbitrary values + $allowed_statuses = [ + 'sent', // Changed from 'pending' + 'viewed', // Optional status if needed + 'pre-screening-complete', + 'assessment-started', // Optional status + 'assessment-complete', + 'expired', + 'cancelled', // Optional status + ]; + + if ( ! in_array( $new_status, $allowed_statuses, true ) ) { + \error_log( "Quiztech Error: Invalid status '{$new_status}' provided to update_status for invitation ID {$invitation_id}." ); + return false; + } + + // Prepare data and format for update + $data = [ 'status' => $new_status ]; + $where = [ 'id' => $invitation_id ]; // Assuming 'id' is the primary key column name + $format = [ '%s' ]; // Format for data + $where_format = [ '%d' ]; // Format for where clause + + $updated = $wpdb->update( $table_name, $data, $where, $format, $where_format ); + + if ( false === $updated ) { + \error_log( "Quiztech Error: Failed to update invitation status for ID {$invitation_id}. DB Error: " . $wpdb->last_error ); + return false; + } + + // $updated contains the number of rows affected. + // Return true if one or more rows were updated (or potentially 0 if the status was already set to the new value). + // We consider 0 rows affected as success in the case the status was already correct. + return true; + } } \ No newline at end of file