db = RT_WA_Analytics_DB::instance(); $this->settings = RT_WA_Analytics_Settings::instance(); } public function render_dashboard_content() { $date_from = $this->date_from; $date_to = $this->date_to; $filters = array( 'date_from' => $date_from, 'date_to' => $date_to, ); $signal_kpis = $this->db->get_whatsapp_kpis( $filters ); $total_clicks = (int) $signal_kpis['total_signals']; $unique_visitors = (int) $signal_kpis['unique_visitors']; $total_sessions = (int) $this->db->get_total_sessions( $date_from, $date_to ); $campaigns_count = (int) $this->db->get_top_campaigns_count( $date_from, $date_to ); $range_days = max( 1, (int) round( ( strtotime( $date_to . ' 23:59:59' ) - strtotime( $date_from . ' 00:00:00' ) ) / DAY_IN_SECONDS ) ); $prev_to = gmdate( 'Y-m-d', strtotime( $date_from . ' -1 day' ) ); $prev_from = gmdate( 'Y-m-d', strtotime( $prev_to . ' -' . $range_days . ' days' ) ); $prev_filters = array( 'date_from' => $prev_from, 'date_to' => $prev_to, ); $prev_kpis = $this->db->get_whatsapp_kpis( $prev_filters ); $prev_clicks = (int) $prev_kpis['total_signals']; $prev_visitors = (int) $prev_kpis['unique_visitors']; $prev_sessions = (int) $this->db->get_total_sessions( $prev_from, $prev_to ); $delta_clicks = $this->percent_delta( $total_clicks, $prev_clicks ); $delta_visitors = $this->percent_delta( $unique_visitors, $prev_visitors ); $delta_sessions = $this->percent_delta( $total_sessions, $prev_sessions ); $wa_chart = $this->db->get_wa_clicks_chart( $date_from, $date_to ); $sessions_chart = $this->db->get_sessions_chart( $date_from, $date_to ); $chart_dates = array_values( array_unique( array_merge( array_column( $wa_chart, 'date' ), array_column( $sessions_chart, 'date' ) ) ) ); sort( $chart_dates ); $wa_by_date = array_column( $wa_chart, 'count', 'date' ); $sessions_by_date = array_column( $sessions_chart, 'count', 'date' ); $chart_wa_values = array(); $chart_sessions_values = array(); foreach ( $chart_dates as $chart_date ) { $chart_wa_values[] = isset( $wa_by_date[ $chart_date ] ) ? (int) $wa_by_date[ $chart_date ] : 0; $chart_sessions_values[] = isset( $sessions_by_date[ $chart_date ] ) ? (int) $sessions_by_date[ $chart_date ] : 0; } $top_pages = $this->db->get_dashboard_top_pages( $date_from, $date_to, 5 ); $page_urls = array(); foreach ( $top_pages as $row ) { if ( $row->page ) { $page_urls[] = $row->page; } } $page_titles = $this->resolve_page_titles( $page_urls ); $campaigns = $this->db->get_dashboard_campaigns( $date_from, $date_to, 5 ); $recent_events = $this->db->get_recent_events( 10, $date_from, $date_to, 0 ); $recent_urls = array(); foreach ( $recent_events as $event ) { if ( $event->page_url ) { $recent_urls[] = $event->page_url; } } $recent_titles = $this->resolve_page_titles( $recent_urls ); include RT_WA_ANALYTICS_PATH . 'modules/reports/views/dashboard.php'; } public function render_whatsapp_clicks_content() { $filters = array( 'date_from' => $this->date_from, 'date_to' => $this->date_to, ); $source_filter = ''; $medium_filter = ''; $campaign_filter = ''; $page_filter = ''; $visitor_filter = ''; if ( isset( $_GET['source'] ) && $_GET['source'] !== '' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $source_filter = sanitize_text_field( wp_unslash( $_GET['source'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin render context; capability-gated by add_menu_page. $filters['source'] = $source_filter; } if ( isset( $_GET['utm_medium'] ) && $_GET['utm_medium'] !== '' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $medium_filter = sanitize_text_field( wp_unslash( $_GET['utm_medium'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin render context; capability-gated by add_menu_page. $filters['utm_medium'] = $medium_filter; } if ( isset( $_GET['campaign'] ) && $_GET['campaign'] !== '' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $campaign_filter = sanitize_text_field( wp_unslash( $_GET['campaign'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin render context; capability-gated by add_menu_page. $filters['campaign'] = $campaign_filter; } if ( isset( $_GET['landing'] ) && $_GET['landing'] !== '' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page_filter = sanitize_text_field( wp_unslash( $_GET['landing'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin render context; capability-gated by add_menu_page. $filters['page'] = $page_filter; } if ( isset( $_GET['visitor'] ) && $_GET['visitor'] !== '' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $visitor_filter = sanitize_text_field( wp_unslash( $_GET['visitor'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin render context; capability-gated by add_menu_page. $filters['visitor'] = $visitor_filter; } $page = isset( $_GET['paged'] ) ? max( 1, absint( wp_unslash( $_GET['paged'] ) ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $per_page = isset( $_GET['per_page'] ) && in_array( absint( wp_unslash( $_GET['per_page'] ) ), array( 20, 50, 100 ), true ) ? absint( wp_unslash( $_GET['per_page'] ) ) : 20; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $offset = ( $page - 1 ) * $per_page; $clicks = $this->db->get_whatsapp_clicks( $per_page, $offset, $filters ); $total = $this->db->get_whatsapp_clicks_count( $filters ); $total_pages = max( 1, (int) ceil( $total / $per_page ) ); $kpis = $this->db->get_whatsapp_kpis( $filters ); $signals_by_source = $this->db->get_whatsapp_signals_by_source( $filters, 8 ); $signals_by_page = $this->db->get_whatsapp_signals_by_page( $filters, 8 ); $sources = $this->db->get_whatsapp_clicks_distinct( 'utm_source' ); $visitor_counts = array(); $uuids = array(); foreach ( $clicks as $event ) { if ( $event->visitor_uuid ) { $uuids[] = $event->visitor_uuid; } } if ( ! empty( $uuids ) ) { $visitor_counts = $this->db->get_whatsapp_visitor_counts( $filters, $uuids ); } $page_urls = array(); foreach ( $signals_by_page as $row ) { $page_urls[] = $row->page; } foreach ( $clicks as $event ) { if ( $event->page_url ) { $page_urls[] = $event->page_url; } } $page_titles = $this->resolve_page_titles( $page_urls ); $source_labels = array(); $source_values = array(); foreach ( $signals_by_source as $row ) { $source_labels[] = $row->source; $source_values[] = (int) $row->total; } $page_labels = array(); $page_values = array(); $page_chart_urls = array(); foreach ( $signals_by_page as $row ) { $title = isset( $page_titles[ $row->page ] ) ? $page_titles[ $row->page ] : ''; $page_labels[] = $title !== '' ? $title : $row->page; $page_values[] = (int) $row->total; $page_chart_urls[] = $this->absolute_url( $row->page ); } $has_active_filters = $this->has_explicit_range || $source_filter !== '' || $medium_filter !== '' || $campaign_filter !== '' || $page_filter !== '' || $visitor_filter !== ''; $event_detail = null; $event_detail_id = isset( $_GET['event_id'] ) ? absint( wp_unslash( $_GET['event_id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $event_detail_id > 0 ) { $event_detail = $this->db->get_event( $event_detail_id ); if ( $event_detail && $event_detail->event_type !== 'whatsapp_click' ) { $event_detail = null; } } $visitor_detail = null; if ( $visitor_filter !== '' ) { $visitor_row = $this->db->get_visitor( $visitor_filter ); if ( $visitor_row ) { $visitor_signals = $this->db->get_visitor_wa_clicks( $visitor_filter ); $page_visited = array(); $source_counts = array(); foreach ( $visitor_signals as $signal ) { if ( $signal->page_url ) { $page_visited[ $signal->page_url ] = true; } $utm = rt_wa_parse_meta( $signal->utm ); $src = isset( $utm['utm_source'] ) && $utm['utm_source'] !== '' ? $utm['utm_source'] : __( 'Direct', 'zaaed-click2chat' ); $source_counts[ $src ] = isset( $source_counts[ $src ] ) ? $source_counts[ $src ] + 1 : 1; } arsort( $source_counts, SORT_NUMERIC ); $main_source = $source_counts ? array_key_first( $source_counts ) : __( 'Direct', 'zaaed-click2chat' ); $visitor_detail = array( 'uuid' => $visitor_row->uuid, 'first_seen' => $visitor_row->first_seen, 'last_seen' => $visitor_row->last_seen, 'total_signals' => count( $visitor_signals ), 'pages_visited' => count( $page_visited ), 'main_source' => $main_source, 'signals' => $visitor_signals, ); } } $date_from_val = $this->date_from; $date_to_val = $this->date_to; include RT_WA_ANALYTICS_PATH . 'modules/reports/views/whatsapp-clicks.php'; } public function render_analytics_content() { $date_from = $this->date_from; $date_to = $this->date_to; $total_visitors = $this->db->get_total_visitors( $date_from, $date_to ); $total_sessions = $this->db->get_total_sessions( $date_from, $date_to ); $total_wa_clicks = $this->db->get_total_wa_clicks( $date_from, $date_to ); $conversion_rate = $this->db->get_conversion_rate( $date_from, $date_to ); $sources_report = $this->db->get_sources_report( $date_from, $date_to ); $campaign_report = $this->db->get_campaign_report( $date_from, $date_to ); $landing_pages_report = $this->db->get_landing_pages_report( $date_from, $date_to ); $device_report = $this->db->get_device_report( $date_from, $date_to ); $browser_report = $this->db->get_browser_report( $date_from, $date_to ); $os_report = $this->db->get_os_report( $date_from, $date_to ); $hourly_report = $this->db->get_hourly_report( $date_from, $date_to ); $weekly_report = $this->db->get_weekly_report( $date_from, $date_to ); $monthly_report = $this->db->get_monthly_report( $date_from, $date_to ); $funnel_data = $this->db->get_funnel_data( $date_from, $date_to ); $visitors_chart = $this->db->get_visitors_chart( $date_from, $date_to ); $sessions_chart = $this->db->get_sessions_chart( $date_from, $date_to ); $wa_clicks_chart = $this->db->get_wa_clicks_chart( $date_from, $date_to ); $traffic_chart_dates = array_values( array_unique( array_merge( array_column( $visitors_chart, 'date' ), array_column( $wa_clicks_chart, 'date' ) ) ) ); sort( $traffic_chart_dates ); $visitors_by_date = array_column( $visitors_chart, 'count', 'date' ); $wa_clicks_by_date = array_column( $wa_clicks_chart, 'count', 'date' ); $traffic_chart_values = array(); $wa_chart_values = array(); foreach ( $traffic_chart_dates as $chart_date ) { $traffic_chart_values[] = isset( $visitors_by_date[ $chart_date ] ) ? (int) $visitors_by_date[ $chart_date ] : 0; $wa_chart_values[] = isset( $wa_clicks_by_date[ $chart_date ] ) ? (int) $wa_clicks_by_date[ $chart_date ] : 0; } include RT_WA_ANALYTICS_PATH . 'modules/reports/views/analytics.php'; } public function render_utm_links_content() { $settings = $this->settings; $utm_links = $this->db->get_utm_links( 200 ); include RT_WA_ANALYTICS_PATH . 'modules/reports/views/utm-links.php'; } /** * Percentage change between a current and a previous value. * * @param int|float $current * @param int|float $previous * @return float */ private function percent_delta( $current, $previous ) { $current = (float) $current; $previous = (float) $previous; if ( $previous <= 0 ) { return $current > 0 ? 100.0 : 0.0; } return round( ( ( $current - $previous ) / $previous ) * 100, 1 ); } /** * Presentation parts for a KPI delta badge (up/down/flat). * * @param float $delta * @return array{class:string, icon:string, label:string} */ private function delta_badge_parts( $delta ) { $delta = (float) $delta; if ( abs( $delta ) < 0.05 ) { $delta = 0; } $class = $delta > 0 ? 'up' : ( $delta < 0 ? 'down' : 'flat' ); $icon = $delta > 0 ? 'arrow-up-alt' : ( $delta < 0 ? 'arrow-down-alt' : 'minus' ); $value = round( $delta ) == $delta ? (string) (int) $delta : number_format( $delta, 1 ); $label = $delta > 0 ? '+' . $value . '%' : $value . '%'; return array( 'class' => $class, 'icon' => $icon, 'label' => $label, ); } /** * Resolve a list of page URLs/paths to WordPress post/page titles using one query. * * @param array $urls * @return array path => title */ private function resolve_page_titles( $urls ) { $titles = array(); $urls = array_values( array_unique( array_filter( array_map( 'trim', (array) $urls ) ) ) ); if ( empty( $urls ) ) { return $titles; } $slugs = array(); $by_slug = array(); foreach ( $urls as $url ) { $path = $this->path_from_url( $url ); if ( $path === '/' || $path === '' ) { $titles[ $path ] = get_bloginfo( 'name' ); continue; } $slug = basename( $path ); if ( $slug !== '' ) { $slugs[ $slug ] = true; $by_slug[ $slug ] = $path; } } if ( ! empty( $slugs ) ) { global $wpdb; $in_placeholders = implode( ',', array_fill( 0, count( $slugs ), '%s' ) ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- IN-list placeholders generated via array_fill(); values bound by prepare(). $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- volatile analytics custom table; caching intentionally omitted $wpdb->prepare( "SELECT post_name, post_title FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_name IN ($in_placeholders)", array_keys( $slugs ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare foreach ( $rows as $row ) { if ( isset( $by_slug[ $row->post_name ] ) ) { $titles[ $by_slug[ $row->post_name ] ] = $row->post_title; } } } return $titles; } /** * Normalize a URL/path to a path key ('' for home). */ private function path_from_url( $url ) { $parsed = wp_parse_url( (string) $url ); $path = isset( $parsed['path'] ) ? $parsed['path'] : ''; return rtrim( $path, '/' ); } /** * Make a page path absolute for links/charts. */ private function absolute_url( $url ) { if ( preg_match( '~^https?://~i', (string) $url ) ) { return $url; } return home_url( $url ); } /** * Humanized relative time ("2 min ago", "Yesterday", ...). */ private function format_signal_time( $mysql_datetime ) { if ( empty( $mysql_datetime ) ) { return ''; } $ts = strtotime( (string) $mysql_datetime ); if ( ! $ts ) { return ''; } $now = time() + (int) ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ); $diff = $now - $ts; if ( $diff < 60 ) { return __( 'just now', 'zaaed-click2chat' ); } if ( $diff < 3600 ) { // translators: %d: number of minutes. return sprintf( _n( '%d min ago', '%d min ago', (int) floor( $diff / 60 ), 'zaaed-click2chat' ), (int) floor( $diff / 60 ) ); } if ( $diff < 86400 && gmdate( 'Ymd', $now ) === gmdate( 'Ymd', $ts ) ) { // translators: %d: number of hours. return sprintf( _n( '%d hr ago', '%d hr ago', (int) floor( $diff / 3600 ), 'zaaed-click2chat' ), (int) floor( $diff / 3600 ) ); } if ( gmdate( 'Ymd', strtotime( '-1 day', $now ) ) === gmdate( 'Ymd', $ts ) ) { return __( 'Yesterday', 'zaaed-click2chat' ); } return date_i18n( get_option( 'date_format' ), $ts ); } /** * CSS modifier token for a traffic-source badge. */ private function source_badge_class( $source ) { $source = strtolower( trim( (string) $source ) ); if ( $source === '' || $source === 'direct' ) { return 'direct'; } $social = array( 'facebook', 'instagram', 'tiktok', 'twitter', 'x.com', 'linkedin', 'youtube', 'pinterest', 'snapchat', 'reddit', 'telegram', 'whatsapp' ); if ( in_array( $source, $social, true ) ) { return 'social'; } $search = array( 'google', 'bing', 'yahoo', 'duckduckgo', 'ecosia', 'baidu', 'yandex', 'ask' ); if ( in_array( $source, $search, true ) ) { return 'search'; } $email = array( 'email', 'mail', 'newsletter', 'crm' ); if ( in_array( $source, $email, true ) ) { return 'email'; } return 'other'; } /** * Classify a WhatsApp click into a readable traffic-source bucket. * * @param array $utm_data Parsed UTM data (may be empty). * @param string $referrer Raw referrer URL (may be empty). * @return string */ private function classify_traffic_source( $utm_data, $referrer ) { $source = isset( $utm_data['utm_source'] ) ? strtolower( trim( $utm_data['utm_source'] ) ) : ''; if ( $source === '' ) { $host = ''; if ( $referrer ) { $parsed = wp_parse_url( $referrer ); $host = isset( $parsed['host'] ) ? strtolower( trim( $parsed['host'] ) ) : ''; } if ( $host === '' ) { return __( 'Direct', 'zaaed-click2chat' ); } if ( preg_match( '/(^|\.)(google|bing|yahoo|duckduckgo|ecosia|baidu|yandex|ask)\./i', $host ) ) { return __( 'Search Engine', 'zaaed-click2chat' ); } if ( preg_match( '/(^|\.)(facebook|instagram|twitter|x\.com|linkedin|youtube|tiktok|pinterest|snapchat|reddit|whatsapp|telegram|wa\.me)\./i', $host ) ) { return __( 'Social', 'zaaed-click2chat' ); } return __( 'Referral', 'zaaed-click2chat' ); } if ( preg_match( '/(^|\.)(google|bing|yahoo|duckduckgo|ecosia|baidu|yandex|ask)\./i', $source ) ) { return __( 'Search Engine', 'zaaed-click2chat' ); } if ( preg_match( '/(^|\.)(facebook|instagram|twitter|linkedin|youtube|tiktok|pinterest|snapchat|reddit|whatsapp|telegram)\./i', $source ) ) { return __( 'Social', 'zaaed-click2chat' ); } return __( 'Referral', 'zaaed-click2chat' ); } }