From 00892b36c98c8a6d0c42567c6b5dcb723bfc165f Mon Sep 17 00:00:00 2001 From: Ruben Ramirez Date: Thu, 3 Apr 2025 16:30:09 -0500 Subject: [PATCH] feat: Implement Phase 2, Step 7 - WP Admin Interfaces --- quiztech-assessment-platform.php | 11 +- src/Admin/AdminListTables.php | 282 ++++++++++++++++++++ src/Admin/SettingsPage.php | 170 ++++++++++++ src/Includes/Ajax/AssessmentAjaxHandler.php | 205 +++++++++++--- src/Includes/Invitations.php | 56 ++++ src/Includes/post-types.php | 114 +++++++- 6 files changed, 793 insertions(+), 45 deletions(-) create mode 100644 src/Admin/AdminListTables.php create mode 100644 src/Admin/SettingsPage.php diff --git a/quiztech-assessment-platform.php b/quiztech-assessment-platform.php index 6700982..34421aa 100644 --- a/quiztech-assessment-platform.php +++ b/quiztech-assessment-platform.php @@ -120,8 +120,17 @@ function quiztech_init() { // Initialize AJAX handler for assessment interactions \Quiztech\AssessmentPlatform\Includes\Ajax\AssessmentAjaxHandler::init(); + // Initialize Admin-specific features + if ( is_admin() ) { + $admin_list_tables = new \Quiztech\AssessmentPlatform\Admin\AdminListTables(); + $admin_list_tables->register_hooks(); + + $settings_page = new \Quiztech\AssessmentPlatform\Admin\SettingsPage(); + $settings_page->register_hooks(); + } + // TODO: Instantiate other core classes and call their init methods here - // e.g., Admin menu handler, AJAX handlers, Shortcode handlers etc. + // e.g., Shortcode handlers etc. } add_action( 'plugins_loaded', 'quiztech_init' ); diff --git a/src/Admin/AdminListTables.php b/src/Admin/AdminListTables.php new file mode 100644 index 0000000..12034c0 --- /dev/null +++ b/src/Admin/AdminListTables.php @@ -0,0 +1,282 @@ + $title) { + $new_columns[$key] = $title; + if ($key === 'title') { + $new_columns['question_type'] = __( 'Question Type', 'quiztech' ); + } + } + // If taxonomy column isn't added automatically, add it here: + // if (!isset($new_columns['taxonomy-quiztech_category'])) { + // $new_columns['taxonomy-quiztech_category'] = __('Categories', 'quiztech'); + // } + return $new_columns; + } + + /** + * Renders the content for custom columns in the Question CPT list table. + * + * @param string $column_name The name of the column to render. + * @param int $post_id The ID of the current post. + */ + public function render_question_custom_columns( $column_name, $post_id ) { + if ( 'question_type' === $column_name ) { + $question_type = get_post_meta( $post_id, '_quiztech_question_type', true ); + echo esc_html( ucwords( str_replace( '-', ' ', $question_type ?: 'N/A' ) ) ); + } + } + + // --- Assessment CPT --- + + /** + * Filters the columns for the Assessment CPT list table. + * + * @param array $columns Existing columns. + * @return array Modified columns. + */ + public function filter_assessment_columns( $columns ) { + $new_columns = []; + foreach ($columns as $key => $title) { + $new_columns[$key] = $title; + if ($key === 'title') { + $new_columns['num_questions'] = __( 'No. Questions', 'quiztech' ); + $new_columns['credit_cost'] = __( 'Credit Cost', 'quiztech' ); + } + } + // Ensure Categories column exists + // if (!isset($new_columns['taxonomy-quiztech_category'])) { + // $new_columns['taxonomy-quiztech_category'] = __('Categories', 'quiztech'); + // } + return $new_columns; + } + + /** + * Renders the content for custom columns in the Assessment CPT list table. + * + * @param string $column_name The name of the column to render. + * @param int $post_id The ID of the current post. + */ + public function render_assessment_custom_columns( $column_name, $post_id ) { + switch ( $column_name ) { + case 'num_questions': + // TODO: Implement logic to get question count (e.g., from '_quiztech_question_ids' meta) + $question_ids = get_post_meta( $post_id, '_quiztech_question_ids', true ); + echo is_array( $question_ids ) ? count( $question_ids ) : '0'; + break; + case 'credit_cost': + // TODO: Implement logic to calculate credit cost based on associated questions + echo 'N/A'; // Placeholder + break; + } + } + + // --- Job CPT --- + + /** + * Filters the columns for the Job CPT list table. + * + * @param array $columns Existing columns. + * @return array Modified columns. + */ + public function filter_job_columns( $columns ) { + $new_columns = []; + // Keep standard columns like title, date + foreach ($columns as $key => $title) { + if ($key === 'date') continue; // Move date to end + $new_columns[$key] = $title; + if ($key === 'title') { + $new_columns['associated_assessment'] = __( 'Assessment', 'quiztech' ); + $new_columns['job_status'] = __( 'Status', 'quiztech' ); + $new_columns['invitations_sent'] = __( 'Invites Sent', 'quiztech' ); + $new_columns['evaluations_completed'] = __( 'Evaluations Completed', 'quiztech' ); + } + } + $new_columns['date'] = $columns['date']; // Add date back at the end + return $new_columns; + } + + /** + * Renders the content for custom columns in the Job CPT list table. + * + * @param string $column_name The name of the column to render. + * @param int $post_id The ID of the current post. + */ + public function render_job_custom_columns( $column_name, $post_id ) { + global $wpdb; + $invitation_table = $wpdb->prefix . 'quiztech_invitations'; + + switch ( $column_name ) { + case 'associated_assessment': + // TODO: Need meta field '_quiztech_associated_assessment_id' + $assessment_id = get_post_meta( $post_id, '_quiztech_associated_assessment_id', true ); + if ( $assessment_id && $assessment_title = get_the_title( $assessment_id ) ) { + // Optional: Link to assessment edit screen + $edit_link = get_edit_post_link( $assessment_id ); + if ($edit_link) { + echo '' . esc_html( $assessment_title ) . ''; + } else { + echo esc_html( $assessment_title ); + } + } else { + echo 'N/A'; + } + break; + case 'job_status': + // TODO: Need meta field '_quiztech_job_status' + $status = get_post_meta( $post_id, '_quiztech_job_status', true ); + echo esc_html( ucwords( $status ?: 'N/A' ) ); + break; + case 'invitations_sent': + $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$invitation_table} WHERE job_id = %d", $post_id ) ); + echo esc_html( $count ?: '0' ); + break; + case 'evaluations_completed': + // This requires linking evaluations back to jobs, likely via the invitation ID stored in evaluation meta + $invitation_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM {$invitation_table} WHERE job_id = %d", $post_id ) ); + if (empty($invitation_ids)) { + echo '0'; + break; + } + $args = [ + 'post_type' => 'user_evaluation', + 'post_status' => 'completed', // Assuming 'completed' is the status set on final submit + 'posts_per_page' => -1, // Count all + 'meta_query' => [ + [ + 'key' => 'quiztech_invitation_id', // Assumes this meta key links evaluation to invitation + 'value' => $invitation_ids, + 'compare' => 'IN', + ] + ], + 'fields' => 'ids', // Only need count + ]; + $evaluation_query = new \WP_Query($args); + echo esc_html( $evaluation_query->post_count ); + break; + } + } + + // --- User Evaluation CPT --- + + /** + * Filters the columns for the User Evaluation CPT list table. + * + * @param array $columns Existing columns. + * @return array Modified columns. + */ + public function filter_user_evaluation_columns( $columns ) { + // Remove 'title' maybe, replace with more useful info + unset($columns['title']); + + $new_columns = [ + 'cb' => $columns['cb'], // Checkbox + 'applicant_email' => __( 'Applicant Email', 'quiztech' ), + 'job_title' => __( 'Job', 'quiztech' ), + 'assessment_title' => __( 'Assessment', 'quiztech' ), + 'status' => __( 'Status', 'quiztech' ), + 'date_submitted' => __( 'Date Submitted', 'quiztech' ), // Use 'date' column key? + ]; + // Add back any other standard columns if needed, like 'date' if not used for 'date_submitted' + // $new_columns['date'] = $columns['date']; + return $new_columns; + } + + /** + * Renders the content for custom columns in the User Evaluation CPT list table. + * + * @param string $column_name The name of the column to render. + * @param int $post_id The ID of the current post. + */ + public function render_user_evaluation_custom_columns( $column_name, $post_id ) { + global $wpdb; + $invitation_table = $wpdb->prefix . 'quiztech_invitations'; + + // Get linked invitation ID first, as it's needed for multiple columns + $invitation_id = get_post_meta( $post_id, 'quiztech_invitation_id', true ); + $invitation_data = null; + if ($invitation_id) { + $invitation_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$invitation_table} WHERE id = %d", $invitation_id ) ); + } + + switch ( $column_name ) { + case 'applicant_email': + echo $invitation_data ? esc_html( $invitation_data->applicant_email ) : 'N/A'; + break; + case 'job_title': + if ($invitation_data && $invitation_data->job_id) { + $job_title = get_the_title($invitation_data->job_id); + $edit_link = get_edit_post_link( $invitation_data->job_id ); + if ($edit_link) { + echo '' . esc_html( $job_title ) . ''; + } else { + echo esc_html( $job_title ); + } + } else { + echo 'N/A'; + } + break; + case 'assessment_title': + if ($invitation_data && $invitation_data->assessment_id) { + $assessment_title = get_the_title($invitation_data->assessment_id); + $edit_link = get_edit_post_link( $invitation_data->assessment_id ); + if ($edit_link) { + echo '' . esc_html( $assessment_title ) . ''; + } else { + echo esc_html( $assessment_title ); + } + } else { + echo 'N/A'; + } + break; + case 'status': + $post_status_obj = get_post_status_object( get_post_status( $post_id ) ); + echo esc_html( $post_status_obj ? $post_status_obj->label : get_post_status( $post_id ) ); + break; + case 'date_submitted': + // Use the post date as submission date + echo esc_html( get_the_date( '', $post_id ) ); + break; + } + } +} \ No newline at end of file diff --git a/src/Admin/SettingsPage.php b/src/Admin/SettingsPage.php new file mode 100644 index 0000000..4e62c12 --- /dev/null +++ b/src/Admin/SettingsPage.php @@ -0,0 +1,170 @@ +page_slug, // Menu Slug + [ $this, 'render_settings_page' ] // Callback function to render the page + ); + } + + /** + * Registers settings, sections, and fields using the Settings API. + */ + public function register_settings() { + register_setting( + $this->option_group, // Option group + $this->option_name, // Option name + [ $this, 'sanitize_settings' ] // Sanitization callback + ); + + // Stripe Section + add_settings_section( + 'quiztech_stripe_section', // Section ID + __( 'Stripe API Keys', 'quiztech' ), // Section Title + '__return_false', // Section callback (optional description) + $this->page_slug // Page slug where section appears + ); + + // Stripe Public Key Field + add_settings_field( + 'quiztech_stripe_public_key', // Field ID + __( 'Stripe Public Key', 'quiztech' ), // Field Title + [ $this, 'render_text_field' ], // Field render callback + $this->page_slug, // Page slug + 'quiztech_stripe_section', // Section ID + [ // Arguments for callback + 'id' => 'quiztech_stripe_public_key', + 'option_name' => $this->option_name, + 'key' => 'stripe_public_key', + 'description' => __( 'Enter your Stripe publishable API key.', 'quiztech' ) + ] + ); + + // Stripe Secret Key Field + add_settings_field( + 'quiztech_stripe_secret_key', // Field ID + __( 'Stripe Secret Key', 'quiztech' ), // Field Title + [ $this, 'render_text_field' ], // Field render callback + $this->page_slug, // Page slug + 'quiztech_stripe_section', // Section ID + [ // Arguments for callback + 'id' => 'quiztech_stripe_secret_key', + 'option_name' => $this->option_name, + 'key' => 'stripe_secret_key', + 'type' => 'password', // Mask the input + 'description' => __( 'Enter your Stripe secret API key. This is kept confidential.', 'quiztech' ) + ] + ); + + // TODO: Add more sections/fields as needed (e.g., email settings, default credit costs) + } + + /** + * Renders the main settings page container and form. + */ + public function render_settings_page() { + ?> +
+

+
+ option_group ); + + // Output setting sections and their fields + do_settings_sections( $this->page_slug ); + + // Output save settings button + submit_button( __( 'Save Settings', 'quiztech' ) ); + ?> +
+
+ + + +

+ + '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 ( 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. + } + + 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() @@ -43,32 +104,14 @@ class AssessmentAjaxHandler { wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400); } - // --- TODO: Ensure user_evaluation CPT is created earlier (e.g., in FrontendHandler) --- - // For now, query for it based on invitation ID (assuming meta field 'quiztech_invitation_id' exists on evaluation) - $args = [ - 'post_type' => 'user_evaluation', - 'post_status' => 'any', // Find it regardless of status - 'meta_query' => [ - [ - 'key' => 'quiztech_invitation_id', // Assumed meta key linking evaluation to invitation - 'value' => $invitation_id, - 'compare' => '=', - ] - ], - 'posts_per_page' => 1, - 'fields' => 'ids', // Only get the ID - ]; - $evaluation_posts = get_posts($args); - $evaluation_id = !empty($evaluation_posts) ? $evaluation_posts[0] : 0; - + // 3. Get or Create User Evaluation Record + $evaluation_id = $this->get_or_create_user_evaluation($invitation_id); if ( ! $evaluation_id ) { - // If not found, something is wrong with the flow (should have been created earlier) - error_log("Quiztech AJAX Error: Could not find user_evaluation CPT for invitation ID: " . $invitation_id); - wp_send_json_error(['message' => __('Could not find associated evaluation record.', 'quiztech')], 404); + 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); } - // --- End TODO section --- - // 3. Sanitize Submitted Answers + // 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) { @@ -87,7 +130,9 @@ class AssessmentAjaxHandler { // 5. Update Invitation Status try { $invitations = new \Quiztech\AssessmentPlatform\Includes\Invitations(); - // TODO: Create the update_status method in Invitations class + // 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); @@ -119,16 +164,62 @@ class AssessmentAjaxHandler { $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 - // TODO: Get the associated user_evaluation CPT ID based on invitation_id (similar to pre-screening handler) - $evaluation_id = 0; // Placeholder - - if ( ! $invitation_id || ! $question_id || ! $evaluation_id ) { - wp_send_json_error(['message' => __('Invalid request 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); + } + + // 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 + + 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; + } } - // TODO: Fetch question type for $question_id to apply appropriate sanitization to $answer - // Example: if type is 'text', use sanitize_textarea_field; if 'multiple-choice', check against allowed values. - $sanitized_answer = sanitize_text_field($answer); // Basic sanitization for now // 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. @@ -153,19 +244,47 @@ class AssessmentAjaxHandler { // 2. Get Data $invitation_id = isset($_POST['invitation_id']) ? absint($_POST['invitation_id']) : 0; - - // TODO: Get the associated user_evaluation CPT ID based on invitation_id (similar to other handlers) - $evaluation_id = 0; // Placeholder - - if ( ! $invitation_id || ! $evaluation_id ) { - wp_send_json_error(['message' => __('Invalid request data for final submission.', 'quiztech')], 400); + if ( ! $invitation_id ) { + wp_send_json_error(['message' => __('Missing invitation ID.', 'quiztech')], 400); } - // 3. Update Invitation Status - // TODO: Call Invitations->update_status($invitation_id, 'assessment-complete'); + // 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 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); + } - // 4. Update User Evaluation CPT Status - // TODO: Update post status of $evaluation_id to 'completed' using wp_update_post() // 5. Send Response // TODO: Consider adding a redirect URL or specific completion message to the response data diff --git a/src/Includes/Invitations.php b/src/Includes/Invitations.php index 58f6c02..8891f36 100644 --- a/src/Includes/Invitations.php +++ b/src/Includes/Invitations.php @@ -133,4 +133,60 @@ class Invitations { return $invitation; // Return the invitation data object if valid } + + /** + * 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 = [ + '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 diff --git a/src/Includes/post-types.php b/src/Includes/post-types.php index daf1fbb..b399ab8 100644 --- a/src/Includes/post-types.php +++ b/src/Includes/post-types.php @@ -10,7 +10,7 @@ namespace Quiztech\AssessmentPlatform\Includes; // If this file is called directly, abort. if ( ! \defined( 'WPINC' ) ) { - \die; + die; } /** @@ -172,4 +172,116 @@ function quiztech_register_post_types() { } \add_action( 'init', __NAMESPACE__ . '\quiztech_register_post_types' ); + +// --- Meta Box for Question Type --- + +/** + * Adds the meta box container for Question Type. + * + * @param string $post_type The post type slug. + */ +function quiztech_add_question_meta_boxes( $post_type ) { + // Limit meta box to specific post type + if ( 'question' === $post_type ) { + \add_meta_box( + 'quiztech_question_type_metabox', // ID + \__( 'Question Type', 'quiztech' ), // Title + __NAMESPACE__ . '\quiztech_render_question_type_metabox', // Callback function + 'question', // Post type + 'side', // Context (normal, side, advanced) + 'high' // Priority (high, core, default, low) + ); + } +} +\add_action( 'add_meta_boxes', __NAMESPACE__ . '\quiztech_add_question_meta_boxes' ); + +/** + * Renders the meta box content for Question Type. + * + * @param \WP_Post $post The post object. + */ +function quiztech_render_question_type_metabox( $post ) { + // Add a nonce field so we can check for it later. + \wp_nonce_field( 'quiztech_save_question_type_meta', 'quiztech_question_type_nonce' ); + + // Use get_post_meta to retrieve an existing value from the database. + $value = \get_post_meta( $post->ID, '_quiztech_question_type', true ); + + // Define the available question types + $question_types = [ + 'text' => \__( 'Text (Single Line)', 'quiztech' ), + 'textarea' => \__( 'Text Area (Multi-line)', 'quiztech' ), + 'multiple-choice' => \__( 'Multiple Choice (Single Answer)', 'quiztech' ), + 'checkbox' => \__( 'Checkboxes (Multiple Answers)', 'quiztech' ), + 'numeric' => \__( 'Numeric', 'quiztech' ), + // Add more types as needed + ]; + + // Display the form field. + echo ' '; + echo ''; +} + +/** + * Saves the meta box data for Question Type. + * + * @param int $post_id The ID of the post being saved. + */ +function quiztech_save_question_type_meta( $post_id ) { + // Check if our nonce is set. + if ( ! isset( $_POST['quiztech_question_type_nonce'] ) ) { + return; + } + + // Verify that the nonce is valid. + if ( ! \wp_verify_nonce( \sanitize_key( $_POST['quiztech_question_type_nonce'] ), 'quiztech_save_question_type_meta' ) ) { + return; + } + + // If this is an autosave, our form has not been submitted, so we don't want to do anything. + if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + // Check the user's permissions. + if ( isset( $_POST['post_type'] ) && 'question' === $_POST['post_type'] ) { + if ( ! \current_user_can( 'edit_post', $post_id ) ) { + return; + } + } else { + // Assuming other post types don't use this meta box + return; + } + + // Make sure that the field is set. + if ( ! isset( $_POST['quiztech_question_type_field'] ) ) { + return; + } + + // Sanitize user input. + $new_meta_value = \sanitize_text_field( \wp_unslash( $_POST['quiztech_question_type_field'] ) ); + + // Define allowed types again for validation + $allowed_types = ['text', 'textarea', 'multiple-choice', 'checkbox', 'numeric']; // Keep this in sync with the render function + + // Update the meta field in the database if the value is allowed or empty. + if ( in_array( $new_meta_value, $allowed_types, true ) || '' === $new_meta_value ) { + \update_post_meta( $post_id, '_quiztech_question_type', $new_meta_value ); + } else { + // Optionally delete meta if invalid value submitted, or log an error + \delete_post_meta( $post_id, '_quiztech_question_type' ); + } +} +// Hook into the 'save_post' action specifically for the 'question' post type +\add_action( 'save_post_question', __NAMESPACE__ . '\quiztech_save_question_type_meta' ); + ?> \ No newline at end of file