repository = $repository; } public static function labels() { $labels = array( 'login_otp' => __( 'Login code', 'anar-toolkit' ), 'password_reset' => __( 'Password recovery', 'anar-toolkit' ), 'paid_customer' => __( 'Payment confirmation', 'anar-toolkit' ), 'paid_admin' => __( 'Paid order to administrators', 'anar-toolkit' ), 'new_admin' => __( 'New order to administrators', 'anar-toolkit' ), 'failed_admin' => __( 'Payment failure alert', 'anar-toolkit' ), 'shipment_customer' => __( 'Shipment tracking', 'anar-toolkit' ), 'status_processing' => __( 'Order processing', 'anar-toolkit' ), 'status_completed' => __( 'Order completed', 'anar-toolkit' ), 'status_on-hold' => __( 'Order on hold', 'anar-toolkit' ), 'status_cancelled' => __( 'Order cancelled', 'anar-toolkit' ), 'status_refunded' => __( 'Order refunded', 'anar-toolkit' ), 'recovery_1' => __( 'Recovery · first reminder', 'anar-toolkit' ), 'recovery_2' => __( 'Recovery · second reminder', 'anar-toolkit' ), 'recovery_3' => __( 'Recovery · third reminder', 'anar-toolkit' ), 'recovery_request' => __( 'Requested recovery code', 'anar-toolkit' ), ); foreach ( array_keys( ( new \Anar\Login\Marketing\Settings() )->defaults()['rules'] ) as $event ) { $labels[ 'mkt_' . $event ] = \Anar\Login\Marketing\Engine::label( $event ); } return $labels; } public static function states() { return array( 'waiting' => __( 'Waiting for stock', 'anar-toolkit' ), 'queued' => __( 'Queued', 'anar-toolkit' ), 'notified' => __( 'Notified', 'anar-toolkit' ), 'rewarded' => __( 'Reward issued', 'anar-toolkit' ), 'finished' => __( 'Completed', 'anar-toolkit' ), 'started' => __( 'Started', 'anar-toolkit' ), 'paused' => __( 'Paused', 'anar-toolkit' ), 'draft' => __( 'Draft', 'anar-toolkit' ), 'active' => __( 'Open', 'anar-toolkit' ), 'converted' => __( 'Purchased', 'anar-toolkit' ), 'suppressed' => __( 'Stopped', 'anar-toolkit' ), 'expired' => __( 'Expired', 'anar-toolkit' ), 'pending' => __( 'Queued', 'anar-toolkit' ), 'running' => __( 'Sending', 'anar-toolkit' ), 'accepted' => __( 'Accepted by provider', 'anar-toolkit' ), 'failed' => __( 'Failed', 'anar-toolkit' ), 'uncertain' => __( 'Needs investigation', 'anar-toolkit' ), 'cancelled' => __( 'Cancelled', 'anar-toolkit' ) ); } public static function badge( $state ) { $states = self::states(); return '' . esc_html( $states[ $state ] ?? $state ) . ''; } public function data( $days = 30, $currency = '' ) { global $wpdb; $days = in_array( (int) $days, array( 7, 30, 90 ), true ) ? (int) $days : 30; $since = ( new \DateTimeImmutable( 'today', wp_timezone() ) )->modify( '-' . ( $days - 1 ) . ' days' )->getTimestamp(); $carts = $this->repository->table( 'carts' ); $messages = $this->repository->table( 'messages' ); $currencies = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT currency FROM %i ORDER BY currency LIMIT 30', $carts ) ); $default = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'IRR'; $currency = in_array( $currency, $currencies, true ) || $currency === $default ? $currency : $default; $counts = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) saved, SUM(CASE WHEN status = 'active' AND updated_at < %d THEN 1 ELSE 0 END) abandoned, SUM(CASE WHEN clicked_at >= %d THEN 1 ELSE 0 END) visits FROM %i WHERE created_at >= %d AND currency = %s", time() - 30 * MINUTE_IN_SECONDS, $since, $carts, $since, $currency ) ); $revenue = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) recovered, COALESCE(SUM(amount),0) revenue FROM (SELECT recovered_order, MAX(recovered_total) amount FROM %i WHERE status = 'converted' AND recovered_order > 0 AND converted_at >= %d AND currency = %s GROUP BY recovered_order) recovered_orders", $carts, $since, $currency ) ); $queue = $wpdb->get_results( $wpdb->prepare( 'SELECT status, COUNT(*) count FROM %i WHERE created_at >= %d GROUP BY status', $messages, $since ), OBJECT_K ); $chart = array(); $today = new \DateTimeImmutable( 'today', wp_timezone() ); for ( $index = min( 14, $days ) - 1; $index >= 0; --$index ) { $start = $today->modify( '-' . $index . ' days' ); $end = $start->modify( '+1 day' ); $row = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) saved, SUM(CASE WHEN status = 'converted' AND recovered_order > 0 THEN 1 ELSE 0 END) recovered FROM %i WHERE created_at >= %d AND created_at < %d AND currency = %s", $carts, $start->getTimestamp(), $end->getTimestamp(), $currency ) ); $chart[] = array( 'date' => wp_date( 'm/d', $start->getTimestamp() ), 'saved' => (int) $row->saved, 'recovered' => (int) $row->recovered ); } return compact( 'days', 'since', 'currency', 'currencies', 'counts', 'revenue', 'queue', 'chart' ); } public function render() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only, administrator-only report filters. $days = isset( $_GET['period'] ) ? absint( $_GET['period'] ) : 30; $currency = isset( $_GET['currency'] ) && is_string( $_GET['currency'] ) ? sanitize_text_field( wp_unslash( $_GET['currency'] ) ) : ''; $page = max( 1, min( 10000, isset( $_GET['report_page'] ) ? absint( $_GET['report_page'] ) : 1 ) ); $filter = isset( $_GET['state'] ) && is_string( $_GET['state'] ) ? sanitize_key( wp_unslash( $_GET['state'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended $tab = \Anar\Login\Infrastructure\Features::current_tab(); $days = in_array( $days, array( 7, 30, 90 ), true ) ? $days : 30; $data = 'overview' === $tab ? $this->data( $days, $currency ) : array( 'days' => $days, 'since' => time() - $days * DAY_IN_SECONDS, 'currency' => $currency ?: ( function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'IRR' ) ); $labels = self::labels(); $states = self::states(); $accepted = (int) ( $data['queue']['accepted']->count ?? 0 ); $pending = (int) ( $data['queue']['pending']->count ?? 0 ); $failed = (int) ( $data['queue']['failed']->count ?? 0 ) + (int) ( $data['queue']['uncertain']->count ?? 0 ); ?>

ANAR TOOLKIT v


revenue, 0 ), $data['currency'], 'money', 'hero' ), array( __( 'Recovered purchases', 'anar-toolkit' ), number_format_i18n( (int) $data['revenue']->recovered ), __( 'Paid after verified recovery', 'anar-toolkit' ), 'cart', '' ), array( __( 'Open abandoned carts', 'anar-toolkit' ), number_format_i18n( (int) $data['counts']->abandoned ), __( 'Inactive for at least 30 minutes', 'anar-toolkit' ), 'clock', '' ), array( __( 'SMS requests accepted', 'anar-toolkit' ), number_format_i18n( $accepted ), __( 'Provider acceptance, not delivery', 'anar-toolkit' ), 'email-alt', '' ), ); foreach ( $cards as $card ) : ?>

chart( $data['chart'] ); ?>

table( 'carts', $page, $filter, $data ); ?>

table( 'messages', $page, $filter, $data ); ?>
repository->table( $kind ); $states = self::states(); $state = isset( $states[ $state ] ) ? $state : ''; $where = $wpdb->prepare( 'created_at >= %d', $data['since'] ); if ( $state ) { $where .= $wpdb->prepare( ' AND status = %s', $state ); } if ( 'carts' === $kind ) { $where .= $wpdb->prepare( ' AND currency = %s', $data['currency'] ); } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where is assembled exclusively from prepared clauses above. $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE $where", $table ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE $where ORDER BY id DESC LIMIT 20 OFFSET %d", $table, ( $page - 1 ) * 20 ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $labels = self::labels(); ?>
repository->contact( $row->contact_id ) : null; ?>
id : ( $labels[ $row->event ] ?? $row->event ) ); ?> coupon_id && class_exists( 'WC_Coupon' ) ) : ?> coupon_id ) )->get_code() ); ?>phone ?? '' ) : $row->recipient ) ); ?>status ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- badge() escapes all values. ?> total, 0 ) . ' ' . $row->currency ); ?> order_id && function_exists( 'wc_get_order' ) && wc_get_order( $row->order_id ) ) : ?> #order_id ); ?> provider ?: '—' ); ?>error_code ); ?>updated_at ) ); ?> object_id > 0 && 'failed' === $row->status && $row->attempts < 3 && 'login_otp' !== $row->event && 'password_reset' !== $row->event ) : ?>
—

__( 'Previous', 'anar-toolkit' ), $page + 1 => __( 'Next', 'anar-toolkit' ) ) as $target => $label ) : if ( $target < 1 || ( $target - 1 ) * 20 >= $total ) { continue; } ?>