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() {
+ ?>
+
+
+
+
+
+
+
+
+
+ '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