feat: Implement Phase 3 Applicant Assessment Experience (Items 15-17)

This commit is contained in:
Ruben Ramirez 2025-04-04 05:15:52 -05:00
parent 2bbe7efdfe
commit 742630778c
6 changed files with 734 additions and 564 deletions

View file

@ -20,13 +20,29 @@
var $prescreeningSection = $('#quiztech-prescreening-section'); var $prescreeningSection = $('#quiztech-prescreening-section');
var $assessmentSection = $('#quiztech-assessment-section'); var $assessmentSection = $('#quiztech-assessment-section');
var $submitButton = $prescreeningForm.find('button[type="submit"]'); var $submitButton = $prescreeningForm.find('button[type="submit"]');
var $formMessages = $('<div class="form-messages"></div>').insertBefore($submitButton); // Area for messages var $preScreenFormMessages = $('<div class="form-messages"></div>').insertBefore($submitButton); // Area for messages
// 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');
// State variables
var currentQuestionIndex = 0;
var totalQuestions = $questionContainers.length;
var timerInterval;
var timerSeconds = 0;
// --- Pre-Screening Form Handling --- // --- Pre-Screening Form Handling ---
$prescreeningForm.on('submit', function(event) { $prescreeningForm.on('submit', function(event) {
event.preventDefault(); // Stop traditional form submission event.preventDefault(); // Stop traditional form submission
$formMessages.empty().removeClass('error success'); // Clear previous messages // Use $preScreenFormMessages here
$preScreenFormMessages.empty().removeClass('error success'); // Clear previous messages
$submitButton.prop('disabled', true).text('Submitting...'); // Disable button $submitButton.prop('disabled', true).text('Submitting...'); // Disable button
var formData = $(this).serialize(); // Get form data var formData = $(this).serialize(); // Get form data
@ -44,27 +60,29 @@
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
// Success! Hide pre-screening, show assessment // Success! Hide pre-screening, show assessment
$formMessages.addClass('success').text(response.data.message || 'Success!'); // Show success message briefly $preScreenFormMessages.addClass('success').text(response.data.message || 'Success!'); // Show success message briefly & Use correct variable
$prescreeningSection.slideUp(); $prescreeningSection.slideUp();
$assessmentSection.slideDown(); $assessmentSection.slideDown();
startTimer(); // Start the assessment timer
updateNavigationButtons(); // Show initial nav state
// No need to re-enable button as the form is gone // No need to re-enable button as the form is gone
} else { } else {
// Handle WP JSON error // Handle WP JSON error
$formMessages.addClass('error').text(response.data.message || 'An error occurred.'); $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 $submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button
} }
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
// Handle general AJAX error // Handle general AJAX error
console.error("AJAX Error:", textStatus, errorThrown); console.error("AJAX Error:", textStatus, errorThrown);
$formMessages.addClass('error').text('A network error occurred. Please try again.'); $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 $submitButton.prop('disabled', false).text('Submit Pre-Screening & Start Assessment'); // Re-enable button
} }
}); });
}); });
// --- Assessment Answer Auto-Save --- // --- Assessment Answer Auto-Save ---
var $assessmentForm = $('#quiztech-assessment-form'); // var $assessmentForm = $('#quiztech-assessment-form'); // Already defined above
var autoSaveTimeout; // To debounce requests var autoSaveTimeout; // To debounce requests
// Target input/textarea/select elements within the assessment form for auto-save // Target input/textarea/select elements within the assessment form for auto-save
@ -72,7 +90,7 @@
clearTimeout(autoSaveTimeout); // Clear previous timeout if exists clearTimeout(autoSaveTimeout); // Clear previous timeout if exists
var $input = $(this); var $input = $(this);
var $questionGroup = $input.closest('.question-group'); var $questionGroup = $input.closest('.quiztech-question-container'); // Updated selector
var questionId = $questionGroup.data('question-id'); var questionId = $questionGroup.data('question-id');
var answer = $input.val(); var answer = $input.val();
@ -116,8 +134,52 @@
}); });
// --- 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();
}
});
function updateNavigationButtons() {
if (currentQuestionIndex >= totalQuestions - 1) {
// Last question
$nextButton.hide();
$submitAssessmentButton.show();
} else {
$nextButton.show();
$submitAssessmentButton.hide();
}
}
// --- Timer Functions ---
function startTimer() {
if (timerInterval) clearInterval(timerInterval); // Clear existing if any
timerSeconds = 0; // Reset timer
$timerDisplay.text(formatTime(timerSeconds)); // Initial display
timerInterval = setInterval(function() {
timerSeconds++;
$timerDisplay.text(formatTime(timerSeconds));
}, 1000);
}
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 --- // --- Final Assessment Submission ---
var $submitAssessmentButton = $('#quiztech-submit-assessment'); // var $submitAssessmentButton = $('#quiztech-submit-assessment'); // Already defined above
var $assessmentFormMessages = $('<div class="form-messages"></div>').insertAfter($submitAssessmentButton); // Area for messages var $assessmentFormMessages = $('<div class="form-messages"></div>').insertAfter($submitAssessmentButton); // Area for messages
$submitAssessmentButton.on('click', function(event) { $submitAssessmentButton.on('click', function(event) {
@ -141,10 +203,13 @@
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
// Success! Display message and potentially hide the form/button // Success! Display completion message and hide form/timer
$assessmentFormMessages.addClass('success').text(response.data.message || 'Assessment Submitted Successfully!'); // Use .html() to render potential basic HTML in the message
$assessmentForm.hide(); // Hide the form after successful submission $completionMessage.html(response.data.completionMessage || 'Assessment Submitted Successfully!').show();
// Optionally redirect: window.location.href = response.data.redirect_url; $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 { } else {
$assessmentFormMessages.addClass('error').text(response.data.message || 'An error occurred during submission.'); $assessmentFormMessages.addClass('error').text(response.data.message || 'An error occurred during submission.');
$submitAssessmentButton.prop('disabled', false).text('Submit Assessment'); // Re-enable button $submitAssessmentButton.prop('disabled', false).text('Submit Assessment'); // Re-enable button

View file

@ -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; } .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"] { padding: 10px 20px; background-color: #0073aa; color: #fff; border: none; border-radius: 3px; cursor: pointer; }
button[type="submit"]:hover { background-color: #005a87; } 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; }
</style> </style>
</head> </head>
<body> <body>
@ -101,22 +108,25 @@ if ( ! $invitation_data || ! $current_step ) {
<p><?php printf( esc_html__( 'You are about to begin Assessment ID: %d', 'quiztech' ), absint( $invitation_data->assessment_id ) ); ?></p> <p><?php printf( esc_html__( 'You are about to begin Assessment ID: %d', 'quiztech' ), absint( $invitation_data->assessment_id ) ); ?></p>
</div> </div>
<div id="quiztech-timer" class="quiztech-timer">00:00</div> <?php // Timer placeholder ?>
<?php <?php
$assessment_id = $invitation_data->assessment_id; $assessment_id = $invitation_data->assessment_id;
// Fetch question IDs associated with the assessment (assuming stored in 'question_ids' meta field) // 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 ) ) : if ( is_array( $question_ids ) && ! empty( $question_ids ) ) :
?> ?>
<form id="quiztech-assessment-form" method="post" action=""> <?php // Action handled by AJAX ?> <form id="quiztech-assessment-form" method="post" action=""> <?php // Action handled by AJAX ?>
<?php wp_nonce_field( 'quiztech_submit_assessment_' . $invitation_data->token, 'quiztech_assessment_nonce' ); ?> <?php // Nonce is checked via AJAX, hidden inputs might not be needed if not submitting traditionally ?>
<input type="hidden" name="quiztech_invitation_token" value="<?php echo esc_attr( $invitation_data->token ); ?>"> <?php // wp_nonce_field( 'quiztech_submit_assessment_' . $invitation_data->token, 'quiztech_assessment_nonce' ); ?>
<input type="hidden" name="quiztech_assessment_id" value="<?php echo esc_attr( $assessment_id ); ?>"> <?php // <input type="hidden" name="quiztech_invitation_token" value="<?php echo esc_attr( $invitation_data->token ); ?>"> ?>
<input type="hidden" name="action" value="quiztech_submit_assessment"> <?php // <input type="hidden" name="quiztech_assessment_id" value="<?php echo esc_attr( $assessment_id ); ?>"> ?>
<?php // <input type="hidden" name="action" value="quiztech_submit_assessment"> ?>
<h4><?php esc_html_e( 'Questions:', 'quiztech' ); ?></h4> <div id="quiztech-questions-container">
<?php foreach ( $question_ids as $question_id ) : ?> <?php foreach ( $question_ids as $index => $question_id ) : ?>
<?php <?php
$question_post = get_post( $question_id ); $question_post = get_post( $question_id );
if ( ! $question_post || 'question' !== $question_post->post_type ) { if ( ! $question_post || 'question' !== $question_post->post_type ) {
@ -125,7 +135,11 @@ if ( ! $invitation_data || ! $current_step ) {
$question_title = get_the_title( $question_post ); $question_title = get_the_title( $question_post );
$question_type = get_post_meta( $question_id, 'question_type', true ); $question_type = get_post_meta( $question_id, 'question_type', true );
?> ?>
<div class="form-group question-group" data-question-id="<?php echo esc_attr( $question_id ); ?>"> <div class="quiztech-question-container <?php echo $is_first_question ? '' : 'quiztech-question-hidden'; ?>"
data-question-id="<?php echo esc_attr( $question_id ); ?>"
data-question-index="<?php echo esc_attr( $index ); ?>">
<h4><?php printf( esc_html__( 'Question %d of %d', 'quiztech' ), $index + 1, count( $question_ids ) ); ?></h4>
<label><strong><?php echo esc_html( $question_title ); ?></strong></label> <label><strong><?php echo esc_html( $question_title ); ?></strong></label>
<?php // Render input based on question type ?> <?php // Render input based on question type ?>
@ -165,11 +179,21 @@ if ( ! $invitation_data || ! $current_step ) {
<p class="error"><?php esc_html_e( 'Unsupported question type.', 'quiztech' ); ?></p> <p class="error"><?php esc_html_e( 'Unsupported question type.', 'quiztech' ); ?></p>
<?php endswitch; ?> <?php endswitch; ?>
</div> </div>
<hr> <?php // Removed <hr> as container provides separation ?>
<?php endforeach; ?> <?php endforeach; ?>
</div> <!-- #quiztech-questions-container -->
<div class="quiztech-navigation">
<button type="button" id="quiztech-next-question"><?php esc_html_e( 'Next Question', 'quiztech' ); ?></button>
<button type="button" id="quiztech-submit-assessment" style="display: none;"><?php esc_html_e( 'Submit Assessment', 'quiztech' ); ?></button>
</div>
<button type="submit" id="quiztech-submit-assessment"><?php esc_html_e( 'Submit Assessment', 'quiztech' ); ?></button>
</form> </form>
<div id="quiztech-completion-message" style="display: none;">
<?php // Completion message will be inserted here by JS ?>
</div>
<?php <?php
else : else :
echo '<p class="error">' . esc_html__( 'Could not load questions for this assessment.', 'quiztech' ) . '</p>'; echo '<p class="error">' . esc_html__( 'Could not load questions for this assessment.', 'quiztech' ) . '</p>';

View file

@ -18,6 +18,7 @@ class AssessmentMetaboxes {
public function register_hooks() { public function register_hooks() {
add_action( 'add_meta_boxes', [ $this, 'add_assessment_metaboxes' ] ); 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_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' ), \__( 'Linked Questions', 'quiztech' ),
[ $this, 'render_linked_questions_metabox' ], [ $this, 'render_linked_questions_metabox' ],
'assessment', 'assessment',
'normal', // Context below editor 'normal', // Context
'high' '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 ( ! 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 ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; }
if ( ! \current_user_can( 'edit_post', $post_id ) ) { 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 // Process submitted IDs
$submitted_ids = []; $submitted_ids = [];
@ -106,4 +115,45 @@ class AssessmentMetaboxes {
// Update meta (even if empty array, to clear previous selections) // Update meta (even if empty array, to clear previous selections)
\update_post_meta( $post_id, '_quiztech_linked_question_ids', $submitted_ids ); \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 '<p>' . \esc_html__( 'Enter the message to display to applicants after they successfully submit this assessment. Basic HTML is allowed.', 'quiztech' ) . '</p>';
echo '<textarea id="quiztech_completion_message_field" name="quiztech_completion_message_field" rows="5" style="width:100%;">' . \esc_textarea( $completion_message ) . '</textarea>';
}
/**
* 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 );
}
} }

View file

@ -2,6 +2,8 @@
namespace Quiztech\AssessmentPlatform\Includes\Ajax; namespace Quiztech\AssessmentPlatform\Includes\Ajax;
use Quiztech\AssessmentPlatform\Includes\Invitations; // Added use statement
/** /**
* Handles AJAX requests related to the front-end assessment process. * Handles AJAX requests related to the front-end assessment process.
*/ */
@ -85,11 +87,6 @@ class AssessmentAjaxHandler {
} }
// 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. * Handles the AJAX submission of the pre-screening form.
* Expects 'nonce', 'invitation_id', and 'pre_screen_answer' array in $_POST. * Expects 'nonce', 'invitation_id', and 'pre_screen_answer' array in $_POST.
@ -129,10 +126,7 @@ class AssessmentAjaxHandler {
// 5. Update Invitation Status // 5. Update Invitation Status
try { try {
$invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations(); $invitations = new 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'); $updated = $invitations->update_status($invitation_id, 'pre-screening-complete');
if (!$updated) { if (!$updated) {
error_log("Quiztech AJAX Error: Failed to update invitation status for ID: " . $invitation_id); error_log("Quiztech AJAX Error: Failed to update invitation status for ID: " . $invitation_id);
@ -257,10 +251,8 @@ class AssessmentAjaxHandler {
// 4. Update Invitation Status // 4. Update Invitation Status
try { try {
$invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations(); $invitations = new Invitations();
// Note: The update_status method expects the invitation *record ID*, not the token. // Assuming $invitation_id passed in POST *is* the record ID.
// 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'); $updated = $invitations->update_status($invitation_id, 'assessment-complete');
if (!$updated) { if (!$updated) {
error_log("Quiztech AJAX Error: Failed to update invitation status to complete for ID: " . $invitation_id); error_log("Quiztech AJAX Error: Failed to update invitation status to complete for ID: " . $invitation_id);
@ -272,7 +264,7 @@ class AssessmentAjaxHandler {
} }
// 4. Update User Evaluation CPT Status to 'completed' // 5. Update User Evaluation CPT Status to 'completed'
$post_update_data = [ $post_update_data = [
'ID' => $evaluation_id, 'ID' => $evaluation_id,
'post_status' => 'completed', // Use a custom status if needed, but 'completed' seems appropriate 'post_status' => 'completed', // Use a custom status if needed, but 'completed' seems appropriate
@ -286,10 +278,23 @@ class AssessmentAjaxHandler {
} }
// 5. Send Response // 6. Get the Assessment ID associated with the invitation
// Future Enhancement: Consider adding a redirect URL or specific completion message/HTML // This requires a method in Invitations class to get the full record by ID
// to the response data based on plugin settings or other logic. $invitation_record = $invitations->get_invitation_by_id($invitation_id); // Assuming this method exists or will be added
wp_send_json_success(['message' => __('Assessment submitted successfully!', 'quiztech')]); $assessment_id = $invitation_record ? $invitation_record->assessment_id : 0;
// 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 // Ensure script execution stops
wp_die(); wp_die();

View file

@ -52,9 +52,9 @@ class FrontendHandler {
// Determine the current step (pre-screening or assessment) // Determine the current step (pre-screening or assessment)
$current_step = 'assessment'; // Default to 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. // If pre-screening questions exist AND the invitation status is still 'sent' (the initial state), show pre-screening.
// Assumes 'pending' is the initial status before viewing/pre-screening. Adjust if needed. // Once pre-screening is submitted, the status should change (e.g., to 'viewed', 'pre-screening complete').
if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'pending' ) { if ( ! empty( $pre_screening_questions ) && $invitation_data->status === 'sent' ) {
$current_step = 'pre_screening'; $current_step = 'pre_screening';
} }

View file

@ -24,7 +24,7 @@ class Invitations {
* @param int $job_id The ID of the job associated with the invitation. * @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 int $assessment_id The ID of the assessment associated with the invitation.
* @param string $applicant_email The email address of the applicant being invited. * @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. * @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 ) { public function create_invitation( $job_id, $assessment_id, $applicant_email ) {
global $wpdb; global $wpdb;
@ -37,7 +37,7 @@ class Invitations {
'job_id' => absint( $job_id ), 'job_id' => absint( $job_id ),
'assessment_id' => absint( $assessment_id ), 'assessment_id' => absint( $assessment_id ),
'applicant_email' => sanitize_email( $applicant_email ), 'applicant_email' => sanitize_email( $applicant_email ),
'status' => 'pending', 'status' => 'sent', // Updated initial status based on user feedback
'created_timestamp' => current_time( 'mysql', 1 ), // GMT time 'created_timestamp' => current_time( 'mysql', 1 ), // GMT time
// 'expiry_timestamp' => null, // Set if expiry is needed // 'expiry_timestamp' => null, // Set if expiry is needed
]; ];
@ -59,7 +59,8 @@ class Invitations {
return new \WP_Error( 'invitation_db_error', __( 'Could not save the invitation record.', 'quiztech' ), [ 'status' => 500 ] ); return new \WP_Error( 'invitation_db_error', __( 'Could not save the invitation record.', 'quiztech' ), [ 'status' => 500 ] );
} }
return $token; // Return the DB ID of the inserted record, not the token, as ID is used elsewhere
return $wpdb->insert_id;
} }
/** /**
@ -89,9 +90,10 @@ class Invitations {
/** /**
* Validate an incoming invitation token. * 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. * @param string $token The token to validate.
* @return bool|\WP_Error True if valid, false if invalid/expired/used, \WP_Error on error. * @return object|null The invitation data object if valid and ready for assessment, null otherwise.
*/ */
public function validate_token( $token ) { public function validate_token( $token ) {
global $wpdb; global $wpdb;
@ -99,7 +101,7 @@ class Invitations {
// Basic sanitization - ensure it looks like our expected token format (32 hex chars) // Basic sanitization - ensure it looks like our expected token format (32 hex chars)
if ( ! preg_match( '/^[a-f0-9]{32}$/', $token ) ) { if ( ! preg_match( '/^[a-f0-9]{32}$/', $token ) ) {
return false; // Invalid token format return null; // Invalid token format
} }
$invitation = $wpdb->get_row( $invitation = $wpdb->get_row(
@ -111,27 +113,51 @@ class Invitations {
if ( ! $invitation ) { if ( ! $invitation ) {
\error_log( "Quiztech Info: Invitation token not found: $token" ); \error_log( "Quiztech Info: Invitation token not found: $token" );
return false; // Token doesn't exist return null; // Token doesn't exist
} }
if ( 'pending' !== $invitation->status ) { // Allow access if status is 'sent' (initial) or 'pre-screening-complete'
\error_log( "Quiztech Info: Invitation token already used or expired: $token (Status: $invitation->status)" ); $allowed_statuses_for_access = ['sent', 'pre-screening-complete'];
return false; // Token not in pending state (already used, completed, expired etc.) 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 // Optional: Check expiry_timestamp if implemented
// if ( $invitation->expiry_timestamp && strtotime( $invitation->expiry_timestamp ) < time() ) { // if ( $invitation->expiry_timestamp && strtotime( $invitation->expiry_timestamp ) < time() ) {
// // Optionally update status to 'expired' here // // Optionally update status to 'expired' here
// return false; // Token expired // return null; // Token expired
// } // }
// Token is valid and pending // Token is valid and in an accessible state
// Optionally update status to 'viewed' here if needed return $invitation; // Return the invitation data object
// $wpdb->update($table_name, ['status' => 'viewed'], ['id' => $invitation->id], ['%s'], ['%d']);
return $invitation; // Return the invitation data object if valid
} }
/**
* 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. * Update the status of an invitation record.
* *
@ -154,7 +180,7 @@ class Invitations {
// Define allowed statuses to prevent arbitrary values // Define allowed statuses to prevent arbitrary values
$allowed_statuses = [ $allowed_statuses = [
'pending', 'sent', // Changed from 'pending'
'viewed', // Optional status if needed 'viewed', // Optional status if needed
'pre-screening-complete', 'pre-screening-complete',
'assessment-started', // Optional status 'assessment-started', // Optional status