log_repository = new MessageLogRepository(); $this->campaign_repository = new CampaignRepository(); $this->plugin_log_repository = new PluginLogRepository(); $this->webhook_log_repository = new WebhookLogRepository(); add_action( 'admin_post_inteltele_process_campaign', array( $this, 'handle_process_campaign' ) ); add_action( 'admin_post_inteltele_sync_statuses', array( $this, 'handle_sync_statuses' ) ); add_action( 'admin_post_inteltele_process_fallbacks', array( $this, 'handle_process_fallbacks' ) ); add_action( 'admin_post_inteltele_export_reports', array( $this, 'handle_export_reports' ) ); add_action( 'admin_post_inteltele_report_columns', array( $this, 'handle_report_columns' ) ); } /** * Process next campaign batch from admin action. */ public function handle_process_campaign(): void { if ( ! current_user_can( 'inteltele_manage' ) ) { wp_die( esc_html__( 'You do not have permission to manage IntelTele campaigns.', 'inteltele' ) ); } if ( ! $this->is_post_request() ) { wp_die( esc_html__( 'This action requires a POST request.', 'inteltele' ), '', array( 'response' => 405 ) ); } $campaign_id = isset( $_POST['campaign_id'] ) && is_scalar( $_POST['campaign_id'] ) ? absint( wp_unslash( $_POST['campaign_id'] ) ) : 0; if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'inteltele_process_campaign_' . $campaign_id ) ) { wp_die( esc_html__( 'Security check failed. Please reload the page and try again.', 'inteltele' ) ); } if ( $campaign_id > 0 ) { $processor = new CampaignProcessor(); $processor->process_batch( $campaign_id ); $this->set_action_notice( 'success', __( 'Campaign batch processing was started. Check the bulk report for progress.', 'inteltele' ) ); PluginLogger::log( 'info', 'campaign_batch_requested', __( 'Campaign batch was processed from the reports page.', 'inteltele' ), array( 'campaign_id' => $campaign_id ), 'ReportsPage' ); } wp_safe_redirect( $this->get_page_url( array( 'page' => 'inteltele-reports', 'tab' => 'bulk' ) ) ); exit; } /** * Sync delivery statuses. */ public function handle_sync_statuses(): void { $this->verify_report_action( 'inteltele_sync_statuses' ); $sync = new StatusSync(); $result = $sync->sync(); $this->set_action_notice( 'success', sprintf( /* translators: 1: SMS count, 2: Viber count. */ __( 'Delivery status sync completed. Updated SMS: %1$d, Viber: %2$d.', 'inteltele' ), (int) ( $result['sms'] ?? 0 ), (int) ( $result['viber'] ?? 0 ) ) ); PluginLogger::log( 'info', 'manual_status_sync_requested', __( 'Manual delivery status sync was requested.', 'inteltele' ), $result, 'ReportsPage' ); wp_safe_redirect( $this->get_page_url( array( 'page' => 'inteltele-reports' ) ) ); exit; } /** * Process Viber fallbacks. */ public function handle_process_fallbacks(): void { $this->verify_report_action( 'inteltele_process_fallbacks' ); $processor = new ViberFallbackProcessor(); $sent = $processor->process(); $this->set_action_notice( 'success', sprintf( /* translators: %d: sent fallback count. */ __( 'Viber SMS fallback processing completed. SMS sent: %d.', 'inteltele' ), $sent ) ); PluginLogger::log( 'info', 'manual_fallback_processing_requested', __( 'Manual Viber fallback processing was requested.', 'inteltele' ), array( 'sent' => $sent ), 'ReportsPage' ); wp_safe_redirect( $this->get_page_url( array( 'page' => 'inteltele-reports' ) ) ); exit; } /** * Export report logs to CSV. */ public function handle_export_reports(): void { $this->require_permission(); if ( ! isset( $_GET['_wpnonce'] ) || ! is_string( $_GET['_wpnonce'] ) ) { wp_die( esc_html__( 'Security check failed. Please reload the page and try again.', 'inteltele' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'inteltele_export_reports' ); $this->read_query( 'inteltele_export_reports', '_wpnonce' ); $tab = $this->get_current_tab( 'single', array( 'single', 'bulk', 'replies', 'plugin_logs', 'webhooks' ) ); if ( 'plugin_logs' === $tab ) { $this->export_plugin_logs(); return; } if ( 'webhooks' === $tab ) { $this->export_webhook_logs(); return; } $args = $this->get_report_query_args( $tab ); $args['limit'] = 5000; $args['offset'] = 0; $logs = $this->log_repository->get_logs( $args ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=inteltele-report-' . gmdate( 'Ymd-His' ) . '.csv' ); $output = fopen( 'php://output', 'w' ); if ( false === $output ) { exit; } fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); fputcsv( $output, array( 'id', 'order_id', 'campaign_id', 'send_type', 'recipient_phone', 'recipient_name', 'recipient_role', 'channel', 'scheme', 'message', 'fallback_message', 'created_at', 'updated_at', 'status_updated_at', 'api_message_id', 'api_status', 'api_status_code', 'error_code', 'status', 'error_reason', 'api_response', 'viber_ttl', 'sms_fallback_sent', 'sms_fallback_sent_at', ) ); foreach ( $logs as $log ) { fputcsv( $output, array( $log->id, $log->order_id, $log->campaign_id, $log->send_type, $log->recipient_phone, $log->recipient_name, $log->recipient_role, $log->channel, $log->scheme, $log->message, isset( $log->fallback_message ) ? $log->fallback_message : '', $log->created_at, $log->updated_at, $log->status_updated_at, $log->api_message_id, $log->api_status, $log->api_status_code, $log->error_code, $log->status, $log->error_reason, $log->api_response, $log->viber_ttl, $log->sms_fallback_sent, $log->sms_fallback_sent_at, ) ); } exit; } /** * Verify report admin action. * * @param string $action Action. */ private function verify_report_action( string $action ): void { $this->require_permission(); if ( ! $this->is_post_request() ) { wp_die( esc_html__( 'This action requires a POST request.', 'inteltele' ), '', array( 'response' => 405 ) ); } if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) ) { wp_die( esc_html__( 'Security check failed. Please reload the page and try again.', 'inteltele' ), '', array( 'response' => 403 ) ); } check_admin_referer( $action ); } public function handle_report_columns(): void { $this->require_permission(); if ( ! $this->is_post_request() ) { wp_die( esc_html__( 'This action requires a POST request.', 'inteltele' ), '', array( 'response' => 405 ) ); } if ( ! isset( $_POST['inteltele_columns_nonce'] ) || ! is_string( $_POST['inteltele_columns_nonce'] ) ) { wp_die( esc_html__( 'Security check failed. Please reload the page and try again.', 'inteltele' ), '', array( 'response' => 403 ) ); } check_admin_referer( 'inteltele_report_columns', 'inteltele_columns_nonce' ); $tab = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : 'single'; $available = $this->get_available_report_columns( $tab ); if ( empty( $available ) ) { wp_die( esc_html__( 'Invalid report.', 'inteltele' ), '', array( 'response' => 400 ) ); } $campaign_id = isset( $_POST['campaign_id'] ) && is_scalar( $_POST['campaign_id'] ) ? absint( wp_unslash( $_POST['campaign_id'] ) ) : 0; $suffix = 'bulk' === $tab && $campaign_id > 0 ? 'bulk_details' : $tab; $meta_key = 'inteltele_report_columns_' . $suffix; if ( isset( $_POST['columns_reset'] ) ) { delete_user_meta( get_current_user_id(), $meta_key ); } else { $selected = isset( $_POST['columns'] ) && is_array( $_POST['columns'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['columns'] ) ) : array(); $selected = array_values( array_intersect( $selected, array_keys( $available ) ) ); update_user_meta( get_current_user_id(), $meta_key, empty( $selected ) ? array_keys( $available ) : $selected ); } $args = array( 'page' => 'inteltele-reports', 'tab' => $tab, 'campaign_id' => $campaign_id ); foreach ( array( 's', 'date_from', 'date_to', 'role', 'level', 'event', 'status' ) as $key ) { if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) { $args[ $key ] = sanitize_text_field( wp_unslash( $_POST[ $key ] ) ); } } wp_safe_redirect( $this->get_page_url( $args ) ); exit; } /** * Render page. */ public function render(): void { $this->prepare_page(); $tabs = array( 'single' => __( 'Single', 'inteltele' ), 'bulk' => __( 'Bulk', 'inteltele' ), 'replies' => __( 'Replies', 'inteltele' ), 'webhooks' => __( 'Webhooks', 'inteltele' ), 'plugin_logs' => __( 'Plugin logs', 'inteltele' ), ); $tab = $this->get_current_tab( 'single', array_keys( $tabs ) ); $this->open_page( __( 'Reports', 'inteltele' ) ); $this->render_action_notice(); $this->render_tabs( $tabs, $tab, 'inteltele-reports' ); $this->render_step_notice(); $this->render_report_actions(); $this->render_filters( 'bulk' === $tab, $tab ); $this->render_column_panel( $tab ); $this->render_table( $tab ); $this->close_page(); } /** * Render report action buttons. */ private function render_report_actions(): void { echo '
'; $this->render_action_form( 'inteltele_sync_statuses', __( 'Sync delivery statuses', 'inteltele' ) ); $this->render_action_form( 'inteltele_process_fallbacks', __( 'Process Viber SMS fallbacks', 'inteltele' ) ); echo '
'; } private function render_action_form( string $action, string $label, int $campaign_id = 0 ): void { echo '
'; echo ''; if ( $campaign_id > 0 ) { echo ''; } wp_nonce_field( $campaign_id > 0 ? $action . '_' . $campaign_id : $action ); echo ''; echo '
'; } /** * Store one-time admin notice for redirected report actions. * * @param string $type Notice type. * @param string $message Notice message. */ private function set_action_notice( string $type, string $message ): void { set_transient( 'inteltele_report_notice_' . get_current_user_id(), array( 'type' => $type, 'message' => $message, ), MINUTE_IN_SECONDS ); } /** * Render one-time admin notice after redirected report actions. */ private function render_action_notice(): void { $key = 'inteltele_report_notice_' . get_current_user_id(); $notice = get_transient( $key ); if ( ! is_array( $notice ) || empty( $notice['message'] ) ) { return; } delete_transient( $key ); $type = isset( $notice['type'] ) ? sanitize_html_class( (string) $notice['type'] ) : 'info'; echo '

' . esc_html( (string) $notice['message'] ) . '

'; } /** * Render report filters. * * @param bool $show_role_filter Whether to show role filter. * @param string $tab Current tab. */ private function render_filters( bool $show_role_filter, string $tab ): void { $search = isset( $this->query['s'] ) ? sanitize_text_field( $this->query['s'] ) : ''; $date_from = isset( $this->query['date_from'] ) ? sanitize_text_field( $this->query['date_from'] ) : ''; $date_to = isset( $this->query['date_to'] ) ? sanitize_text_field( $this->query['date_to'] ) : ''; $selected_role = isset( $this->query['role'] ) ? sanitize_text_field( $this->query['role'] ) : ''; $selected_level = isset( $this->query['level'] ) ? sanitize_key( $this->query['level'] ) : ''; $selected_event = isset( $this->query['event'] ) ? sanitize_key( $this->query['event'] ) : ''; $selected_status = isset( $this->query['status'] ) ? sanitize_key( $this->query['status'] ) : ''; echo '
'; $this->render_view_nonce(); echo ''; echo ''; if ( 'bulk' === $tab && $this->get_current_campaign_id() > 0 ) { echo ''; } if ( 'bulk' !== $tab || $this->get_current_campaign_id() > 0 ) { echo ' '; } echo ' '; echo ' '; if ( 'plugin_logs' === $tab ) { echo ' '; echo ' '; } elseif ( 'webhooks' === $tab ) { echo ' '; } elseif ( $show_role_filter && $this->get_current_campaign_id() > 0 ) { echo ' '; } $this->render_column_hidden_inputs( $tab ); echo ' '; echo '' . esc_html__( 'Export CSV', 'inteltele' ) . ''; echo '
'; } /** * Get export URL for current filters. * * @param string $tab Current tab. */ private function get_export_url( string $tab ): string { $args = array( 'action' => 'inteltele_export_reports', 'tab' => $tab, ); foreach ( array( 's', 'date_from', 'date_to', 'role', 'campaign_id', 'level', 'event', 'status' ) as $key ) { if ( isset( $this->query[ $key ] ) && '' !== $this->query[ $key ] ) { $args[ $key ] = sanitize_text_field( $this->query[ $key ] ); } } if ( 'bulk' !== $tab || $this->get_current_campaign_id() > 0 ) { foreach ( $this->get_visible_column_keys( $tab ) as $column ) { $args['columns'][] = $column; } } return wp_nonce_url( add_query_arg( $args, admin_url( 'admin-post.php' ) ), 'inteltele_export_reports' ); } /** * Render placeholder report table. * * @param string $tab Current tab. */ private function render_table( string $tab ): void { if ( 'single' === $tab ) { $this->render_log_table( 'manual' ); return; } if ( 'replies' === $tab ) { $this->render_log_table( 'order_status' ); return; } if ( 'bulk' === $tab ) { if ( $this->get_current_campaign_id() > 0 ) { $this->render_bulk_details(); } else { $this->render_campaign_table(); } return; } if ( 'plugin_logs' === $tab ) { $this->render_plugin_log_table(); return; } if ( 'webhooks' === $tab ) { $this->render_webhook_log_table(); return; } $columns = array( __( 'No.', 'inteltele' ), __( 'Phone', 'inteltele' ), ); if ( in_array( $tab, array( 'bulk', 'replies' ), true ) ) { $columns[] = __( 'Name', 'inteltele' ); } $columns[] = __( 'Channel', 'inteltele' ); if ( in_array( $tab, array( 'bulk', 'replies' ), true ) ) { $columns[] = __( 'Role', 'inteltele' ); } $columns[] = __( 'Date', 'inteltele' ); $columns[] = __( 'Text', 'inteltele' ); $columns[] = __( 'Status', 'inteltele' ); echo ''; echo ''; foreach ( $columns as $column ) { echo ''; } echo ''; echo ''; echo '
' . esc_html( $column ) . '
' . esc_html__( 'No messages found.', 'inteltele' ) . '
'; } /** * Render separate report column visibility panel. * * @param string $tab Current tab. */ private function render_column_panel( string $tab ): void { if ( 'bulk' === $tab && $this->get_current_campaign_id() <= 0 ) { return; } $columns = $this->get_available_report_columns( $tab ); if ( empty( $columns ) ) { return; } $visible = $this->get_visible_column_keys( $tab ); $args = $this->get_current_report_base_args( $tab ); echo '
'; echo ''; wp_nonce_field( 'inteltele_report_columns', 'inteltele_columns_nonce', false ); foreach ( $args as $key => $value ) { if ( is_array( $value ) ) { foreach ( $value as $item ) { echo ''; } } else { echo ''; } } echo '
'; echo '' . esc_html__( 'Table columns', 'inteltele' ) . ''; echo '
'; echo '
'; foreach ( $columns as $key => $label ) { echo ''; } echo '
'; echo '

'; echo '

'; echo '
'; echo '
'; echo '
'; } /** * Preserve selected columns while submitting filters. * * @param string $tab Current tab. */ private function render_column_hidden_inputs( string $tab ): void { if ( 'bulk' === $tab && $this->get_current_campaign_id() <= 0 ) { return; } foreach ( $this->get_visible_column_keys( $tab ) as $column ) { echo ''; } } /** * Get current report args without column selection. * * @param string $tab Current tab. * @return array */ private function get_current_report_base_args( string $tab ): array { $args = array( 'page' => 'inteltele-reports', 'tab' => $tab, ); foreach ( array( 's', 'date_from', 'date_to', 'role', 'campaign_id', 'level', 'event', 'status' ) as $key ) { if ( isset( $this->query[ $key ] ) && '' !== $this->query[ $key ] ) { $args[ $key ] = sanitize_text_field( $this->query[ $key ] ); } } return $args; } /** * Get available report columns for a tab. * * @param string $tab Current tab. * @return array */ private function get_available_report_columns( string $tab ): array { if ( in_array( $tab, array( 'single', 'bulk', 'replies' ), true ) ) { return array( 'no' => __( 'No.', 'inteltele' ), 'order_id' => __( 'Order ID', 'inteltele' ), 'phone' => __( 'Phone', 'inteltele' ), 'name' => __( 'Name', 'inteltele' ), 'channel' => __( 'Channel', 'inteltele' ), 'role' => __( 'Role', 'inteltele' ), 'date' => __( 'Date', 'inteltele' ), 'text' => __( 'Text', 'inteltele' ), 'status' => __( 'Status', 'inteltele' ), 'api_status' => __( 'API status', 'inteltele' ), 'http' => __( 'HTTP', 'inteltele' ), 'error_code' => __( 'Error code', 'inteltele' ), 'status_updated' => __( 'Status updated', 'inteltele' ), 'fallback' => __( 'Fallback', 'inteltele' ), 'api_message_id' => __( 'API message ID', 'inteltele' ), 'error_reason' => __( 'Error reason', 'inteltele' ), ); } if ( 'plugin_logs' === $tab ) { return array( 'date' => __( 'Date', 'inteltele' ), 'level' => __( 'Level', 'inteltele' ), 'event' => __( 'Event', 'inteltele' ), 'source' => __( 'Source', 'inteltele' ), 'message' => __( 'Message', 'inteltele' ), 'context' => __( 'Context', 'inteltele' ), 'user' => __( 'User', 'inteltele' ), ); } if ( 'webhooks' === $tab ) { return array( 'date' => __( 'Date', 'inteltele' ), 'status' => __( 'Status', 'inteltele' ), 'ip' => __( 'IP', 'inteltele' ), 'content_type' => __( 'Content type', 'inteltele' ), 'payload' => __( 'Payload', 'inteltele' ), 'result' => __( 'Result', 'inteltele' ), ); } return array(); } /** * Get visible column keys from request. * * @param string $tab Current tab. * @return string[] */ private function get_visible_column_keys( string $tab ): array { $available = $this->get_available_report_columns( $tab ); if ( empty( $available ) ) { return array(); } $meta_key = $this->get_column_preference_key( $tab ); if ( ! isset( $this->query['columns'] ) ) { $saved = get_user_meta( get_current_user_id(), $meta_key, true ); if ( is_array( $saved ) ) { $saved = array_values( array_intersect( array_map( 'sanitize_key', $saved ), array_keys( $available ) ) ); if ( ! empty( $saved ) ) { return $saved; } } return array_keys( $available ); } $selected = array_map( 'sanitize_key', (array) $this->query['columns'] ); $selected = array_values( array_intersect( $selected, array_keys( $available ) ) ); return empty( $selected ) ? array_keys( $available ) : $selected; } /** * Get user meta key for report column preferences. * * @param string $tab Current tab. */ private function get_column_preference_key( string $tab ): string { $suffix = 'bulk' === $tab && $this->get_current_campaign_id() > 0 ? 'bulk_details' : $tab; return 'inteltele_report_columns_' . sanitize_key( $suffix ); } /** * Render selected table header cells. * * @param array $columns All columns. * @param string[] $visible Visible keys. */ private function render_selected_headers( array $columns, array $visible ): void { foreach ( $visible as $key ) { if ( isset( $columns[ $key ] ) ) { echo '' . esc_html( $columns[ $key ] ) . ''; } } } /** * Render bulk campaigns table. */ private function render_campaign_table(): void { $campaigns = $this->campaign_repository->get_recent( 50 ); echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ( empty( $campaigns ) ) { echo ''; } foreach ( $campaigns as $campaign ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
' . esc_html__( 'Campaign ID', 'inteltele' ) . '' . esc_html__( 'Scheme', 'inteltele' ) . '' . esc_html__( 'Date', 'inteltele' ) . '' . esc_html__( 'Text', 'inteltele' ) . '' . esc_html__( 'Progress', 'inteltele' ) . '' . esc_html__( 'Status', 'inteltele' ) . '' . esc_html__( 'Actions', 'inteltele' ) . '
' . esc_html__( 'No campaigns found.', 'inteltele' ) . '
' . esc_html( (string) $campaign->id ) . '' . esc_html( $this->format_channel( '', (string) $campaign->scheme ) ) . '' . esc_html( (string) $campaign->created_at ) . '' . esc_html( wp_trim_words( (string) $campaign->message, 12, '...' ) ) . '' . esc_html( sprintf( '%d / %d', (int) $campaign->processed_count, (int) $campaign->recipients_total ) ) . '' . esc_html( $this->format_campaign_status( (string) $campaign->status ) ) . ''; echo '' . esc_html__( 'Details', 'inteltele' ) . ' '; if ( (int) $campaign->processed_count < (int) $campaign->recipients_total ) { $this->render_action_form( 'inteltele_process_campaign', __( 'Process next batch', 'inteltele' ), (int) $campaign->id ); } echo '
'; } /** * Render selected bulk campaign recipient details. */ private function render_bulk_details(): void { $campaign_id = $this->get_current_campaign_id(); $campaign = $this->campaign_repository->get( $campaign_id ); echo '

' . esc_html__( 'Back to campaigns', 'inteltele' ) . '

'; if ( $campaign ) { echo '

' . esc_html( sprintf( /* translators: %d: campaign ID. */ __( 'Campaign #%d recipients', 'inteltele' ), $campaign_id ) ) . '

'; echo '

' . esc_html( sprintf( '%d / %d', (int) $campaign->processed_count, (int) $campaign->recipients_total ) ) . '

'; } $this->render_log_table( 'bulk' ); } /** * Render manual message logs. */ private function render_log_table( string $send_type ): void { $args = $this->get_report_query_args_by_type( $send_type ); $logs = $this->log_repository->get_logs( $args ); $total = $this->log_repository->count_logs( $args ); $tab = $this->send_type_to_tab( $send_type ); $columns = $this->get_available_report_columns( $tab ); $visible = $this->get_visible_column_keys( $tab ); echo ''; echo ''; $this->render_selected_headers( $columns, $visible ); echo ''; if ( empty( $logs ) ) { echo ''; } foreach ( $logs as $index => $log ) { $row = array( 'no' => esc_html( (string) ( $index + 1 ) ), 'order_id' => esc_html( empty( $log->order_id ) ? '' : (string) $log->order_id ), 'phone' => esc_html( (string) $log->recipient_phone ), 'name' => esc_html( (string) $log->recipient_name ), 'channel' => esc_html( $this->format_channel( (string) $log->channel, (string) $log->scheme ) ), 'role' => esc_html( $this->format_role( (string) $log->recipient_role ) ), 'date' => esc_html( (string) $log->created_at ), 'text' => esc_html( wp_trim_words( (string) $log->message, 12, '...' ) ), 'status' => esc_html( $this->format_status( (string) $log->status ) ), 'api_status' => esc_html( $this->format_api_status( (string) $log->api_status ) ), 'http' => esc_html( empty( $log->api_status_code ) ? '' : (string) $log->api_status_code ), 'error_code' => '' . esc_html( (string) $log->error_code ) . '', 'status_updated' => esc_html( (string) $log->status_updated_at ), 'fallback' => esc_html( $this->format_fallback( $log ) ), 'api_message_id' => '' . esc_html( (string) $log->api_message_id ) . '', 'error_reason' => esc_html( (string) $log->error_reason ), ); echo ''; foreach ( $visible as $column ) { echo ''; } echo ''; } echo '
' . esc_html__( 'No messages found.', 'inteltele' ) . '
' . wp_kses_post( $row[ $column ] ?? '' ) . '
'; $this->render_pagination( $total, (int) $args['limit'] ); } /** * Render plugin diagnostics logs. */ private function render_plugin_log_table(): void { $args = $this->get_plugin_log_query_args(); $logs = $this->plugin_log_repository->get_logs( $args ); $total = $this->plugin_log_repository->count_logs( $args ); $columns = $this->get_available_report_columns( 'plugin_logs' ); $visible = $this->get_visible_column_keys( 'plugin_logs' ); echo ''; echo ''; $this->render_selected_headers( $columns, $visible ); echo ''; if ( empty( $logs ) ) { echo ''; } foreach ( $logs as $log ) { $row = array( 'date' => esc_html( (string) $log->created_at ), 'level' => esc_html( strtoupper( (string) $log->level ) ), 'event' => '' . esc_html( (string) $log->event ) . '', 'source' => esc_html( (string) $log->source ), 'message' => esc_html( (string) $log->message ), 'context' => '' . esc_html( wp_trim_words( (string) $log->context, 30, '...' ) ) . '', 'user' => esc_html( empty( $log->user_id ) ? '' : '#' . (string) $log->user_id ), ); echo ''; foreach ( $visible as $column ) { echo ''; } echo ''; } echo '
' . esc_html__( 'No plugin logs found.', 'inteltele' ) . '
' . wp_kses_post( $row[ $column ] ?? '' ) . '
'; $this->render_pagination( $total, (int) $args['limit'] ); } /** * Render webhook request logs. */ private function render_webhook_log_table(): void { $args = $this->get_webhook_log_query_args(); $logs = $this->webhook_log_repository->get_logs( $args ); $total = $this->webhook_log_repository->count_logs( $args ); $columns = $this->get_available_report_columns( 'webhooks' ); $visible = $this->get_visible_column_keys( 'webhooks' ); echo ''; echo ''; $this->render_selected_headers( $columns, $visible ); echo ''; if ( empty( $logs ) ) { echo ''; } foreach ( $logs as $log ) { $row = array( 'date' => esc_html( (string) $log->created_at ) . '
' . esc_html( (string) $log->method ) . ' ' . esc_html( (string) $log->route ) . '', 'status' => esc_html( $this->format_webhook_status( (string) $log->status ) ), 'ip' => '' . esc_html( (string) $log->remote_addr ) . '', 'content_type' => esc_html( (string) $log->content_type ), 'payload' => $this->render_payload_details( (string) $log->payload, (string) $log->raw_body ), 'result' => $this->render_json_details( (string) $log->result, '', __( 'View result', 'inteltele' ) ), ); echo ''; foreach ( $visible as $column ) { echo ''; } echo ''; } echo '
' . esc_html__( 'No webhook requests found.', 'inteltele' ) . '
' . wp_kses_post( $row[ $column ] ?? '' ) . '
'; $this->render_pagination( $total, (int) $args['limit'] ); } /** * Get query args by send type. * * @param string $send_type Send type. * @return array */ private function get_report_query_args_by_type( string $send_type ): array { $args = $this->get_report_query_args( $this->send_type_to_tab( $send_type ) ); $args['send_type'] = $send_type; return $args; } /** * Get query args for plugin logs. * * @return array */ private function get_plugin_log_query_args(): array { $page = isset( $this->query['paged'] ) ? max( 1, absint( $this->query['paged'] ) ) : 1; $limit = 50; return array( 'search' => isset( $this->query['s'] ) ? sanitize_text_field( $this->query['s'] ) : '', 'date_from' => isset( $this->query['date_from'] ) ? sanitize_text_field( $this->query['date_from'] ) : '', 'date_to' => isset( $this->query['date_to'] ) ? sanitize_text_field( $this->query['date_to'] ) : '', 'level' => isset( $this->query['level'] ) ? sanitize_key( $this->query['level'] ) : '', 'event' => isset( $this->query['event'] ) ? sanitize_key( $this->query['event'] ) : '', 'limit' => $limit, 'offset' => ( $page - 1 ) * $limit, ); } /** * Get query args for webhook logs. * * @return array */ private function get_webhook_log_query_args(): array { $page = isset( $this->query['paged'] ) ? max( 1, absint( $this->query['paged'] ) ) : 1; $limit = 50; return array( 'search' => isset( $this->query['s'] ) ? sanitize_text_field( $this->query['s'] ) : '', 'date_from' => isset( $this->query['date_from'] ) ? sanitize_text_field( $this->query['date_from'] ) : '', 'date_to' => isset( $this->query['date_to'] ) ? sanitize_text_field( $this->query['date_to'] ) : '', 'status' => isset( $this->query['status'] ) ? sanitize_key( $this->query['status'] ) : '', 'limit' => $limit, 'offset' => ( $page - 1 ) * $limit, ); } /** * Export plugin logs to CSV. */ private function export_plugin_logs(): void { $args = $this->get_plugin_log_query_args(); $args['limit'] = 5000; $args['offset'] = 0; $logs = $this->plugin_log_repository->get_logs( $args ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=inteltele-plugin-logs-' . gmdate( 'Ymd-His' ) . '.csv' ); $output = fopen( 'php://output', 'w' ); if ( false === $output ) { exit; } fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); fputcsv( $output, array( 'id', 'created_at', 'level', 'event', 'source', 'message', 'context', 'user_id' ) ); foreach ( $logs as $log ) { fputcsv( $output, array( $log->id, $log->created_at, $log->level, $log->event, $log->source, $log->message, $log->context, $log->user_id, ) ); } exit; } /** * Export webhook logs to CSV. */ private function export_webhook_logs(): void { $args = $this->get_webhook_log_query_args(); $args['limit'] = 5000; $args['offset'] = 0; $logs = $this->webhook_log_repository->get_logs( $args ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=inteltele-webhook-logs-' . gmdate( 'Ymd-His' ) . '.csv' ); $output = fopen( 'php://output', 'w' ); if ( false === $output ) { exit; } fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) ); fputcsv( $output, array( 'id', 'created_at', 'route', 'method', 'remote_addr', 'content_type', 'status', 'headers', 'query_params', 'payload', 'raw_body', 'result' ) ); foreach ( $logs as $log ) { fputcsv( $output, array( $log->id, $log->created_at, $log->route, $log->method, $log->remote_addr, $log->content_type, $log->status, $log->headers, $log->query_params, $log->payload, $log->raw_body, $log->result, ) ); } exit; } /** * Get query args for report tab. * * @param string $tab Tab. * @return array */ private function get_report_query_args( string $tab ): array { $page = isset( $this->query['paged'] ) ? max( 1, absint( $this->query['paged'] ) ) : 1; $limit = 50; $args = array( 'search' => isset( $this->query['s'] ) ? sanitize_text_field( $this->query['s'] ) : '', 'date_from' => isset( $this->query['date_from'] ) ? sanitize_text_field( $this->query['date_from'] ) : '', 'date_to' => isset( $this->query['date_to'] ) ? sanitize_text_field( $this->query['date_to'] ) : '', 'role' => isset( $this->query['role'] ) ? sanitize_key( $this->query['role'] ) : '', 'campaign_id' => isset( $this->query['campaign_id'] ) ? absint( $this->query['campaign_id'] ) : 0, 'limit' => $limit, 'offset' => ( $page - 1 ) * $limit, ); if ( 'single' === $tab ) { $args['send_type'] = 'manual'; $args['require_api_message_id'] = true; } elseif ( 'bulk' === $tab ) { $args['send_type'] = 'bulk'; $args['require_api_message_id'] = true; } elseif ( 'replies' === $tab ) { $args['send_type'] = 'order_status'; $args['require_api_message_id'] = true; } return $args; } /** * Get search input placeholder for a report tab. * * @param string $tab Current tab. */ private function get_search_placeholder( string $tab ): string { if ( 'plugin_logs' === $tab ) { return __( 'Message, source, context', 'inteltele' ); } if ( 'webhooks' === $tab ) { return __( 'Payload, result, IP, headers', 'inteltele' ); } return __( 'Phone or name', 'inteltele' ); } /** * Render collapsible JSON details. * * @param string $json JSON string. * @param string $fallback Fallback raw body. * @param string $label Details label. */ private function render_json_details( string $json, string $fallback, string $label ): string { $value = '' !== $json ? $json : $fallback; if ( '' === $value ) { return ''; } $pretty = $this->format_json_for_display( $value ); return '
' . esc_html( $label ) . '
' . esc_html( $pretty ) . '
'; } /** * Render webhook payload with raw body when it carries additional data. * * @param string $payload Payload JSON. * @param string $raw_body Raw request body. */ private function render_payload_details( string $payload, string $raw_body ): string { $output = ''; if ( '' !== $payload && '[]' !== $payload ) { $output .= $this->render_json_details( $payload, '', __( 'View payload', 'inteltele' ) ); } if ( '' !== $raw_body && $raw_body !== $payload ) { $output .= $this->render_json_details( $raw_body, '', __( 'View raw body', 'inteltele' ) ); } return $output; } /** * Pretty print JSON strings when possible. * * @param string $value Raw value. */ private function format_json_for_display( string $value ): string { $decoded = json_decode( $value, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { return $value; } $encoded = wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); return false === $encoded ? $value : $encoded; } /** * Format webhook status. * * @param string $status Status. */ private function format_webhook_status( string $status ): string { $map = array( 'processed' => __( 'Processed', 'inteltele' ), 'processed_with_errors' => __( 'Processed with errors', 'inteltele' ), 'received' => __( 'Received', 'inteltele' ), ); return $map[ $status ] ?? $status; } /** * Get current campaign ID from request. */ private function get_current_campaign_id(): int { return isset( $this->query['campaign_id'] ) ? absint( $this->query['campaign_id'] ) : 0; } /** * Convert send type to tab. * * @param string $send_type Send type. */ private function send_type_to_tab( string $send_type ): string { $map = array( 'manual' => 'single', 'bulk' => 'bulk', 'order_status' => 'replies', ); return $map[ $send_type ] ?? 'single'; } /** * Render simple pagination. * * @param int $total Total rows. * @param int $limit Page size. */ private function render_pagination( int $total, int $limit ): void { if ( $total <= $limit ) { return; } $current = isset( $this->query['paged'] ) ? max( 1, absint( $this->query['paged'] ) ) : 1; $pages = (int) ceil( $total / $limit ); $tab = $this->get_current_tab( 'single', array( 'single', 'bulk', 'replies', 'plugin_logs', 'webhooks' ) ); $base_args = $this->get_current_report_base_args( $tab ); $base_args['columns'] = $this->get_visible_column_keys( $tab ); echo '
'; echo '' . esc_html( sprintf( /* translators: %d: item count. */ __( '%d items', 'inteltele' ), $total ) ) . ' '; if ( $current > 1 ) { $prev_args = array_merge( $base_args, array( 'paged' => $current - 1 ) ); echo '' . esc_html__( 'Previous', 'inteltele' ) . ' '; } echo '' . esc_html( sprintf( /* translators: 1: current page, 2: total pages. */ __( '%1$d of %2$d', 'inteltele' ), $current, $pages ) ) . ' '; if ( $current < $pages ) { $next_args = array_merge( $base_args, array( 'paged' => $current + 1 ) ); echo '' . esc_html__( 'Next', 'inteltele' ) . ''; } echo '
'; } /** * Format role. * * @param string $role Role. */ private function format_role( string $role ): string { if ( 'admin' === $role ) { return __( 'Administrator', 'inteltele' ); } if ( 'customer' === $role ) { return __( 'Customer', 'inteltele' ); } return $role; } /** * Format channel for UI. * * @param string $channel Channel. * @param string $scheme Scheme. */ private function format_channel( string $channel, string $scheme ): string { if ( 'viber_sms' === $scheme ) { return __( 'Viber + SMS', 'inteltele' ); } if ( 'sms' === $channel ) { return __( 'SMS', 'inteltele' ); } if ( 'viber' === $channel ) { return __( 'Viber', 'inteltele' ); } return $channel; } /** * Format internal status. * * @param string $status Status. */ private function format_status( string $status ): string { $map = array( 'created' => __( 'Created', 'inteltele' ), 'sent' => __( 'Sent', 'inteltele' ), 'pending' => __( 'Sending', 'inteltele' ), 'delivered' => __( 'Delivered', 'inteltele' ), 'failed' => __( 'Failed', 'inteltele' ), ); return $map[ $status ] ?? $status; } /** * Format API status. * * @param string $status API status. */ private function format_api_status( string $status ): string { $map = array( 'pending' => __( 'Sending', 'inteltele' ), 'delivrd' => __( 'Delivered', 'inteltele' ), 'seen' => __( 'Seen', 'inteltele' ), 'click' => __( 'Clicked', 'inteltele' ), 'undeliv' => __( 'Not delivered', 'inteltele' ), 'expired' => __( 'Expired', 'inteltele' ), 'rejectd' => __( 'Rejected', 'inteltele' ), 'OK' => __( 'Sent to API', 'inteltele' ), ); return $map[ $status ] ?? $status; } /** * Format fallback data. * * @param object $log Log row. */ private function format_fallback( $log ): string { if ( 'viber_sms' !== (string) $log->scheme ) { return ''; } if ( 'viber' !== (string) $log->channel ) { return ''; } if ( (int) $log->sms_fallback_sent > 0 ) { return sprintf( /* translators: %s: fallback date. */ __( 'SMS fallback sent: %s', 'inteltele' ), (string) $log->sms_fallback_sent_at ); } return sprintf( /* translators: %d: TTL seconds. */ __( 'TTL: %d seconds', 'inteltele' ), (int) $log->viber_ttl ); } /** * Format campaign status. * * @param string $status Status. */ private function format_campaign_status( string $status ): string { $map = array( 'created' => __( 'Created', 'inteltele' ), 'processing' => __( 'Processing', 'inteltele' ), 'completed' => __( 'Completed', 'inteltele' ), 'completed_with_errors' => __( 'Completed with errors', 'inteltele' ), 'stopped' => __( 'Stopped', 'inteltele' ), ); return $map[ $status ] ?? $status; } }