'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 '
'; /* translators: %d: number of entries updated */ echo esc_html( sprintf( _n( '%d entry updated.', '%d entries updated.', $count, 'forge-trueconversion' ), $count ) ); echo '