'entry', 'plural' => 'entries', 'ajax' => false, ) ); } /** * Define Table Columns * * @return array Column slug => Column title. */ public function get_columns() { $columns = array( 'cb' => '', 'submitted_at' => esc_html__( 'Date / Time', 'forge-trueconversion' ), 'form_plugin' => esc_html__( 'Form Plugin', 'forge-trueconversion' ), 'form_name' => esc_html__( 'Form Name', 'forge-trueconversion' ), 'submitter_name' => esc_html__( 'Submitter', 'forge-trueconversion' ), 'message_excerpt' => esc_html__( 'Message', 'forge-trueconversion' ), 'traffic_source' => esc_html__( 'Traffic Source', 'forge-trueconversion' ), 'is_conversion' => esc_html__( 'Conversion', 'forge-trueconversion' ), ); /** * Filter the entries table columns. * * Lets an add-on (e.g. TrueConversion Pro) append its own columns * without duplicating this whole table/class. * * @param array $columns Column slug => column title. */ return apply_filters( 'ftcon_entries_table_columns', $columns ); } /** * Define Sortable Columns * * @return array */ public function get_sortable_columns() { return array( 'submitted_at' => array( 'submitted_at', true ), 'form_plugin' => array( 'form_plugin', false ), 'submitter_name' => array( 'submitter_name', false ), 'traffic_source' => array( 'traffic_source', false ), 'is_conversion' => array( 'is_conversion', false ), ); } /** * Define Bulk Actions * * @return array */ public function get_bulk_actions() { // Conversion marking is available on all plans. $actions = array( 'delete' => __( 'Delete', 'forge-trueconversion' ), 'mark_conversion' => __( 'Mark as Conversion', 'forge-trueconversion' ), 'unmark_conversion' => __( 'Unmark Conversion', 'forge-trueconversion' ), ); return $actions; } /** * Checkbox Column * * @param object $item The current entry. * @return string HTML checkbox. */ public function column_cb( $item ) { return sprintf( '', (int) $item->id ); } /** * Date/Time Column * * @param object $item The current entry. * @return string Formatted date. */ public function column_submitted_at( $item ) { $date = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $item->submitted_at ) ); $actions = array( 'view' => sprintf( '%s', (int) $item->id, esc_html__( 'View Details', 'forge-trueconversion' ) ), 'delete' => sprintf( '%s', wp_nonce_url( admin_url( 'admin.php?page=forge-trueconversion&action=delete&entry_id=' . (int) $item->id ), 'ftcon_delete_entry' ), esc_js( __( 'Delete this entry?', 'forge-trueconversion' ) ), esc_html__( 'Delete', 'forge-trueconversion' ) ), ); return $date . $this->row_actions( $actions ); } /** * Submitter Column * * @param object $item The current entry. * @return string Name and email. */ public function column_submitter_name( $item ) { $name = ! empty( $item->submitter_name ) ? esc_html( $item->submitter_name ) : '' . esc_html__( 'Unknown', 'forge-trueconversion' ) . ''; $email = ! empty( $item->submitter_email ) ? '
' . esc_html( $item->submitter_email ) . '' : ''; return $name . $email; } /** * Message Column * * @param object $item The current entry. * @return string Truncated message. */ public function column_message_excerpt( $item ) { if ( empty( $item->message_excerpt ) ) { return '' . esc_html__( 'No message', 'forge-trueconversion' ) . ''; } // Display-only truncation: first ~100 chars, cut at a word boundary, with // '...' appended when longer. The full message stays in the database and // is shown in the View Details modal — only this rendered cell is shortened. $message = (string) $item->message_excerpt; $limit = 100; if ( mb_strlen( $message ) > $limit ) { // Take the first $limit chars, then cut back to the last space so a // word is never split. mb_strrpos finds the last space within the clip // (mb_strpos finds the first); mb_* keeps it multibyte-safe. $clip = mb_substr( $message, 0, $limit ); $space = mb_strrpos( $clip, ' ' ); if ( false !== $space && $space > 0 ) { $clip = mb_substr( $clip, 0, $space ); } $message = $clip . '...'; } return esc_html( $message ); } /** * Traffic Source Column * * @param object $item The current entry. * @return string Traffic source with badge. */ public function column_traffic_source( $item ) { $source = esc_html( $item->traffic_source ); $color = '#6b7280'; if ( false !== strpos( $source, 'Google' ) ) { $color = '#ea4335'; } elseif ( false !== strpos( $source, 'Meta' ) || false !== strpos( $source, 'Facebook' ) ) { $color = '#1877f2'; } elseif ( false !== strpos( $source, 'LinkedIn' ) ) { $color = '#0a66c2'; } elseif ( false !== strpos( $source, 'Microsoft' ) || false !== strpos( $source, 'Bing' ) ) { $color = '#00a4ef'; } elseif ( false !== strpos( $source, 'TikTok' ) ) { $color = '#000000'; } elseif ( false !== strpos( $source, 'Organic' ) ) { $color = '#16a34a'; } elseif ( 'Direct' === $source ) { $color = '#9ca3af'; } return sprintf( '%s', esc_attr( $color ), $source ); } /** * Conversion Column * * Shows a toggle button to mark/unmark an entry as a conversion. * Available on all plans (Free + Pro). * * @param object $item The current entry. * @return string HTML for the conversion toggle. */ public function column_is_conversion( $item ) { // Conversion marking is available on all plans. $is_conversion = ! empty( $item->is_conversion ); $button_class = $is_conversion ? 'tc-conversion-toggle tc-conversion-active' : 'tc-conversion-toggle'; $label = $is_conversion ? esc_html__( 'Conversion', 'forge-trueconversion' ) : esc_html__( 'Mark', 'forge-trueconversion' ); $google_indicator = ''; if ( $is_conversion && ! empty( $item->conversion_sent ) ) { $google_indicator = ' G'; } return sprintf( '%s', esc_attr( $button_class ), (int) $item->id, $is_conversion ? 1 : 0, $label, $google_indicator ); } /** * Default Column Rendering * * @param object $item The current entry. * @param string $column_name The column slug. * @return string Column value. */ public function column_default( $item, $column_name ) { /** * Filter a column's rendered output. * * Lets an add-on render its own columns (added via the * ftcon_entries_table_columns filter) with custom markup instead * of the raw escaped field value. * * @param string|null $output Null by default — the default * rendering below is used unless * an add-on returns a string. * @param object $item The current entry. * @param string $column_name The column slug. */ $custom = apply_filters( "ftcon_entries_table_column_{$column_name}", null, $item ); if ( null !== $custom ) { return $custom; } return esc_html( isset( $item->$column_name ) ? $item->$column_name : '' ); } /** * Message when no entries found. */ public function no_items() { esc_html_e( 'No form submissions tracked yet. Once a visitor submits a form on your site, entries will appear here.', 'forge-trueconversion' ); } /** * Prepare Items */ public function prepare_items() { $per_page = 20; $orderby = isset( $_REQUEST['orderby'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) : 'submitted_at'; // phpcs:ignore WordPress.Security.NonceVerification $order = isset( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : 'DESC'; // phpcs:ignore WordPress.Security.NonceVerification $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification $current_page = $this->get_pagenum(); $result = FTCON_Entry_Manager::get_entries( array( 'per_page' => $per_page, 'page' => $current_page, 'search' => $search, 'orderby' => $orderby, 'order' => $order, ) ); $this->items = $result['entries']; $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns(), ); $this->set_pagination_args( array( 'total_items' => $result['total'], 'per_page' => $per_page, 'total_pages' => ceil( $result['total'] / $per_page ), ) ); } } /** * Class FTCON_Admin_Dashboard * * Registers the admin menu and handles dashboard page rendering, * CSV exports, entry deletion, and bulk actions. */ class FTCON_Admin_Dashboard { /** * Initialize the dashboard. */ public static function init() { add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) ); add_action( 'admin_init', array( __CLASS__, 'handle_actions' ) ); } /** * Register Admin Menu Pages */ public static function register_menu() { add_menu_page( __( 'TrueConversion', 'forge-trueconversion' ), __( 'TrueConversion', 'forge-trueconversion' ), 'manage_options', 'forge-trueconversion', array( __CLASS__, 'render_page' ), 'dashicons-chart-area', 80 ); add_submenu_page( 'forge-trueconversion', __( 'Form Entries', 'forge-trueconversion' ), __( 'Entries', 'forge-trueconversion' ), 'manage_options', 'forge-trueconversion', array( __CLASS__, 'render_page' ) ); } /** * Handle Admin Actions */ public static function handle_actions() { // CSV Export. if ( isset( $_GET['page'] ) && 'forge-trueconversion' === $_GET['page'] && isset( $_GET['action'] ) && 'export_csv' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized access.', 'forge-trueconversion' ) ); } check_admin_referer( 'ftcon_export_csv' ); FTCON_Entry_Manager::export_csv(); exit; } // Single entry delete. if ( isset( $_GET['page'] ) && 'forge-trueconversion' === $_GET['page'] && isset( $_GET['action'] ) && 'delete' === $_GET['action'] && isset( $_GET['entry_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized access.', 'forge-trueconversion' ) ); } check_admin_referer( 'ftcon_delete_entry' ); $entry_id = absint( $_GET['entry_id'] ); FTCON_Entry_Manager::delete_entry( $entry_id ); wp_safe_redirect( admin_url( 'admin.php?page=forge-trueconversion&deleted=1' ) ); exit; } // Bulk actions (delete, mark/unmark conversion). if ( isset( $_POST['page'] ) && 'forge-trueconversion' === $_POST['page'] && isset( $_POST['action'] ) && isset( $_POST['entry_ids'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Unauthorized access.', 'forge-trueconversion' ) ); } $action = sanitize_text_field( wp_unslash( $_POST['action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification if ( 'delete' === $action ) { check_admin_referer( 'bulk-entries' ); $ids = array_map( 'absint', (array) $_POST['entry_ids'] ); $count = FTCON_Entry_Manager::delete_entries( $ids ); wp_safe_redirect( admin_url( 'admin.php?page=forge-trueconversion&deleted=' . $count ) ); exit; } if ( 'mark_conversion' === $action || 'unmark_conversion' === $action ) { check_admin_referer( 'bulk-entries' ); // Conversion marking is available on all plans. $ids = array_map( 'absint', (array) $_POST['entry_ids'] ); $mark = 'mark_conversion' === $action ? 1 : 0; foreach ( $ids as $id ) { if ( $id ) { FTCON_Entry_Manager::update_field( $id, 'is_conversion', $mark ); // Upload to Google Ads if marking. if ( $mark && FTCON_Google_Ads::is_connected() ) { $entry = FTCON_Entry_Manager::get_entry( $id ); if ( $entry && 'gclid' === $entry->click_id_type && ! empty( $entry->click_id ) ) { FTCON_Google_Ads::upload_conversion( $entry ); } } } } $count = count( $ids ); wp_safe_redirect( admin_url( 'admin.php?page=forge-trueconversion&conversions_updated=' . $count ) ); exit; } } } /** * Render the Dashboard Page */ public static function render_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'forge-trueconversion' ) ); } $table = new FTCON_Entries_Table(); $table->prepare_items(); // Get stats. $conversion_stats = FTCON_Entry_Manager::get_conversion_stats(); $source_stats = FTCON_Entry_Manager::get_source_stats(); ?>


'; /* translators: %d: number of entries deleted */ echo esc_html( sprintf( _n( '%d entry deleted.', '%d entries deleted.', $count, 'forge-trueconversion' ), $count ) ); echo '

'; } if ( isset( $_GET['conversions_updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $count = absint( $_GET['conversions_updated'] ); // phpcs:ignore WordPress.Security.NonceVerification echo '

'; /* translators: %d: number of entries updated */ echo esc_html( sprintf( _n( '%d entry updated.', '%d entries updated.', $count, 'forge-trueconversion' ), $count ) ); echo '

'; } ?> 0 ) : ?>
%
search_box( __( 'Search Entries', 'forge-trueconversion' ), 'tc-search' ); $table->display(); ?>
items as $item ) { $all_entries_json[ $item->id ] = array( 'id' => (int) $item->id, 'submitted_at' => esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $item->submitted_at ) ) ), 'form_plugin' => esc_html( $item->form_plugin ), 'form_name' => esc_html( $item->form_name ), 'submitter_name' => esc_html( $item->submitter_name ), 'submitter_email' => esc_html( $item->submitter_email ), 'message_excerpt' => esc_html( $item->message_excerpt ), 'form_fields_summary' => esc_html( isset( $item->form_fields_summary ) ? $item->form_fields_summary : '' ), 'traffic_source' => esc_html( $item->traffic_source ), 'utm_source' => esc_html( $item->utm_source ), 'utm_medium' => esc_html( $item->utm_medium ), 'utm_campaign' => esc_html( $item->utm_campaign ), 'utm_term' => esc_html( $item->utm_term ), 'utm_content' => esc_html( $item->utm_content ), 'click_id' => esc_html( $item->click_id ), 'click_id_type' => esc_html( $item->click_id_type ), 'page_url' => esc_url( $item->page_url ), 'landing_page' => esc_url( $item->landing_page ), 'referrer' => esc_url( $item->referrer ), 'is_conversion' => (int) $item->is_conversion, 'conversion_sent' => (int) $item->conversion_sent, 'conversion_sent_at' => esc_html( $item->conversion_sent_at ? $item->conversion_sent_at : '' ), 'ai_classification' => esc_html( $item->ai_classification ), 'ai_confidence' => $item->ai_confidence, 'ai_reasoning' => esc_html( $item->ai_reasoning ), ); } wp_add_inline_script( 'ftcon-admin-dashboard', 'var ftcEntries = ' . wp_json_encode( $all_entries_json ) . ';', 'before' ); } } // Initialize the dashboard. FTCON_Admin_Dashboard::init();