self::PAGE ), $args ), admin_url( 'admin.php' ) ); } /** * Submenu under WooCommerce. */ public static function menu() { // Needs attention: failed confirmations and refunds past the deadline. $count = Submission::count_failed() + Refunds::count_overdue(); $title = __( 'Withdrawals', 'otkazpro' ); $menu = $title; if ( $count ) { $menu .= ' ' . (int) $count . ''; } add_submenu_page( 'woocommerce', $title, $menu, self::CAP, self::PAGE, array( __CLASS__, 'render' ) ); } /** * Page router. */ public static function render() { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'otkazpro' ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $view = isset( $_GET['view'] ) ? absint( $_GET['view'] ) : 0; if ( $view ) { self::render_detail( $view ); return; } $table = new Admin_List_Table(); $table->prepare_items(); ?>


search_box( __( 'Search', 'otkazpro' ), 'otkazpro-search' ); $table->display(); ?>

' . esc_html__( 'Withdrawal not found.', 'otkazpro' ) . '

'; return; } $order = Verifier::get_order( $s->order_id ); $rows = array( __( 'Statement number', 'otkazpro' ) => esc_html( $s->number ), __( 'Submitted on', 'otkazpro' ) => esc_html( Util::format_gmt( $s->created_gmt, 'd.m.Y H:i:s' ) . ' (' . wp_timezone_string() . ') / ' . $s->created_gmt . ' UTC' ), __( 'Order', 'otkazpro' ) => $order ? '#' . esc_html( $s->order_number ) . '' : esc_html( '#' . $s->order_number ), __( 'Name', 'otkazpro' ) => esc_html( $s->name ), __( 'Email', 'otkazpro' ) => '' . esc_html( $s->email ) . '', __( 'Deadline', 'otkazpro' ) => self::deadline_badge( $s ) . ( $s->deadline_gmt ? ' ' . esc_html( Util::format_gmt( $s->deadline_gmt, 'date' ) ) : '' ), __( 'Products', 'otkazpro' ) => self::items_html( $s ), __( 'Refund', 'otkazpro' ) => self::refund_html( $s, $order ), __( 'Text of the statement', 'otkazpro' ) => '
' . esc_html( $s->statement ) . '
', __( 'Reason (optional)', 'otkazpro' ) => '' !== $s->reason ? nl2br( esc_html( $s->reason ) ) : '—', __( 'Source', 'otkazpro' ) => esc_html( self::source_label( $s->source ) ), __( 'Language', 'otkazpro' ) => esc_html( $s->language ? $s->language : '—' ), __( 'IP address', 'otkazpro' ) => esc_html( $s->ip_address ), __( 'User agent', 'otkazpro' ) => '' . esc_html( $s->user_agent ) . '', __( 'Customer confirmation', 'otkazpro' ) => self::email_badge( $s ) . ( $s->email_sent_gmt ? ' ' . esc_html( Util::format_gmt( $s->email_sent_gmt, 'datetime' ) ) : '' ) . ' ' . self::resend_button( $s ), __( 'Store notification', 'otkazpro' ) => esc_html( self::status_label( $s->admin_email_status ) ), ); ?>

number ) ); ?>

←

$html ) : ?>

processed_gmt ) { $out = Refunds::badge( $s ) . ' ' . esc_html( Util::format_gmt( $s->processed_gmt, 'datetime' ) ); } else { /* translators: %s: date */ $out = Refunds::badge( $s ) . ' ' . esc_html( sprintf( __( 'Refund due by %s', 'otkazpro' ), wp_date( get_option( 'date_format' ), $s->refund_due() ) ) ); } $actions = ( $s->processed_gmt ? '' : Refunds::prepare_button( $s, $order ) . ' ' ) . Refunds::processed_button( $s ); return $out . '

' . $actions . '

'; } /** * Items list markup. * * @param Submission $s Submission. * @return string */ public static function items_html( $s ) { $out = ''; } /** * Deadline badge. * * @param Submission $s Submission. * @return string */ public static function deadline_badge( $s ) { if ( true === $s->within_deadline ) { return '' . esc_html__( 'In time', 'otkazpro' ) . ''; } if ( false === $s->within_deadline ) { return '' . esc_html__( 'After the deadline', 'otkazpro' ) . ''; } return '' . esc_html__( 'Not started', 'otkazpro' ) . ''; } /** * Email status badge. * * @param Submission $s Submission. * @return string */ public static function email_badge( $s ) { $class = 'sent' === $s->email_status ? 'ok' : ( 'failed' === $s->email_status ? 'error' : '' ); $label = 'failed' === $s->email_status ? __( 'Email not sent', 'otkazpro' ) : self::status_label( $s->email_status ); return '' . esc_html( $label ) . ''; } /** * Status label. * * @param string $status Status. * @return string */ public static function status_label( $status ) { $labels = array( 'sent' => __( 'Sent', 'otkazpro' ), 'failed' => __( 'Failed', 'otkazpro' ), 'pending' => __( 'Pending', 'otkazpro' ), 'disabled' => __( 'Disabled', 'otkazpro' ), ); return isset( $labels[ $status ] ) ? $labels[ $status ] : $status; } /** * Source label. * * @param string $source Source. * @return string */ public static function source_label( $source ) { $labels = array( 'lookup' => __( 'Order number + email', 'otkazpro' ), 'link' => __( 'Link from email', 'otkazpro' ), 'account' => __( 'My account', 'otkazpro' ), ); return isset( $labels[ $source ] ) ? $labels[ $source ] : (string) $source; } /** * Resend form button. * * @param Submission $s Submission. * @return string */ public static function resend_button( $s ) { $url = wp_nonce_url( add_query_arg( array( 'action' => 'otkazpro_resend', 'id' => $s->id, ), admin_url( 'admin-post.php' ) ), 'py_otkaz_resend_' . $s->id ); return '' . esc_html( 'sent' === $s->email_status ? __( 'Send again', 'otkazpro' ) : __( 'Resend', 'otkazpro' ) ) . ''; } /** * Handle resend. */ public static function resend() { $id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; check_admin_referer( 'py_otkaz_resend_' . $id ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'otkazpro' ) ); } $s = Submission::get( $id ); $ok = $s ? Emails::send_customer( $s ) : false; if ( $s && $ok ) { $order = Verifier::get_order( $s->order_id ); if ( $order ) { /* translators: 1: statement number, 2: email */ $order->add_order_note( sprintf( __( 'Withdrawal confirmation %1$s resent to %2$s.', 'otkazpro' ), $s->number, $s->email ) ); } } $back = wp_get_referer(); wp_safe_redirect( add_query_arg( 'otkazpro_msg', $ok ? 'resent' : 'resend_failed', $back ? $back : self::url() ) ); exit; } /** * CSV export of the current filters (all columns). */ public static function export_csv() { check_admin_referer( 'otkazpro_export' ); if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'otkazpro' ) ); } $args = Admin_List_Table::filters_from_request(); $args['per_page'] = 0; $result = Submission::query( $args ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename=otkazi-' . gmdate( 'Y-m-d' ) . '.csv' ); $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- BOM for Excel. $header = array( __( 'Statement number', 'otkazpro' ), __( 'Submitted (local)', 'otkazpro' ), __( 'Submitted (UTC)', 'otkazpro' ), __( 'Order ID', 'otkazpro' ), __( 'Order number', 'otkazpro' ), __( 'Customer ID', 'otkazpro' ), __( 'Name', 'otkazpro' ), __( 'Email', 'otkazpro' ), __( 'Products', 'otkazpro' ), __( 'Total quantity', 'otkazpro' ), __( 'Excluded products', 'otkazpro' ), __( 'Deadline', 'otkazpro' ), __( 'Indicative deadline', 'otkazpro' ), __( 'Refund due by', 'otkazpro' ), __( 'Processed (UTC)', 'otkazpro' ), __( 'Text of the statement', 'otkazpro' ), __( 'Reason (optional)', 'otkazpro' ), __( 'Source', 'otkazpro' ), __( 'Language', 'otkazpro' ), __( 'IP address', 'otkazpro' ), __( 'User agent', 'otkazpro' ), __( 'Customer confirmation', 'otkazpro' ), __( 'Confirmation sent (UTC)', 'otkazpro' ), __( 'Send attempts', 'otkazpro' ), __( 'Store notification', 'otkazpro' ), ); fputcsv( $out, $header, ',', '"', '' ); foreach ( $result['items'] as $s ) { $items = array(); foreach ( $s->items as $item ) { $items[] = $item['name'] . ( ! empty( $item['sku'] ) ? ' [' . $item['sku'] . ']' : '' ) . ' x ' . (int) $item['qty'] . ( ! empty( $item['excluded'] ) ? ' (' . $item['excluded_reason'] . ')' : '' ); } if ( true === $s->within_deadline ) { $deadline = __( 'In time', 'otkazpro' ); } elseif ( false === $s->within_deadline ) { $deadline = __( 'After the deadline', 'otkazpro' ); } else { $deadline = __( 'Not started', 'otkazpro' ); } $row = array( $s->number, $s->created_local, $s->created_gmt, $s->order_id, $s->order_number, $s->customer_id, $s->name, $s->email, implode( '; ', $items ), $s->total_qty(), $s->has_excluded ? __( 'Yes', 'otkazpro' ) : __( 'No', 'otkazpro' ), $deadline, $s->deadline_gmt ? Util::format_gmt( $s->deadline_gmt, 'Y-m-d' ) : '', wp_date( 'Y-m-d', $s->refund_due() ), (string) $s->processed_gmt, $s->statement, $s->reason, self::source_label( $s->source ), $s->language, $s->ip_address, $s->user_agent, self::status_label( $s->email_status ), $s->email_sent_gmt, $s->email_attempts, self::status_label( $s->admin_email_status ), ); fputcsv( $out, array_map( array( Util::class, 'csv_cell' ), $row ), ',', '"', '' ); } fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } /** * Admin notices. */ public static function notices() { if ( get_transient( 'py_otkaz_activated' ) && current_user_can( self::CAP ) && ! get_option( Install::CONFLICT_OPTION ) ) { delete_transient( 'py_otkaz_activated' ); printf( '

%s %s

', esc_html__( 'OtkazPro is active. A “Withdrawal from contract” page was created – review the settings and add the link to your footer.', 'otkazpro' ), esc_url( Settings::url() ), esc_html__( 'Open settings', 'otkazpro' ) ); } $taken = get_transient( 'py_otkaz_took_over' ); if ( $taken && current_user_can( 'activate_plugins' ) ) { delete_transient( 'py_otkaz_took_over' ); echo '

' . esc_html__( 'OtkazPro took over the settings and withdrawal records of the previous edition (WPOtkaz) and deactivated it. You can now delete WPOtkaz from the Plugins screen – the data stays.', 'otkazpro' ) . '

'; } if ( get_transient( 'py_otkaz_settings_restored' ) && current_user_can( self::CAP ) ) { delete_transient( 'py_otkaz_settings_restored' ); echo '

' . esc_html__( 'OtkazPro restored its settings, which were removed when the previous edition was deleted.', 'otkazpro' ) . '

'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $msg = isset( $_GET['otkazpro_msg'] ) ? sanitize_key( wp_unslash( $_GET['otkazpro_msg'] ) ) : ''; if ( 'resent' === $msg ) { echo '

' . esc_html__( 'The confirmation was sent to the customer.', 'otkazpro' ) . '

'; } elseif ( 'cache_checked' === $msg ) { $level = Cache_Check::level( Cache_Check::result() ); $class = 'good' === $level ? 'notice-success' : ( 'critical' === $level ? 'notice-error' : 'notice-warning' ); echo '

' . wp_kses_post( Cache_Check::describe( Cache_Check::result() ) ) . '

'; } elseif ( 'page_ok' === $msg ) { echo '

' . esc_html__( 'The withdrawal form page is set up. Check it and add the link to your footer.', 'otkazpro' ) . '

'; } elseif ( 'page_failed' === $msg ) { echo '

' . esc_html__( 'The page could not be updated. Add [otkazpro] to the page manually and select it in the settings.', 'otkazpro' ) . '

'; } elseif ( 'resend_failed' === $msg ) { echo '

' . esc_html__( 'The email could not be sent. Check your SMTP settings and WooCommerce → Status → Logs (source “otkazpro”).', 'otkazpro' ) . '

'; } // Persistent warnings are limited to the OtkazPro screens (critical ones also to the Plugins screen). $on_ours = self::is_own_screen(); if ( ! $on_ours && ! self::is_screen( 'plugins' ) ) { return; } $cache_level = Cache_Check::level( Cache_Check::result() ); if ( 'cache_checked' !== $msg && current_user_can( self::CAP ) && ( 'critical' === $cache_level || ( 'recommended' === $cache_level && $on_ours ) ) ) { printf( '

%2$s %3$s

%5$s

', 'critical' === $cache_level ? 'notice-error' : 'notice-warning', esc_html__( 'OtkazPro:', 'otkazpro' ), wp_kses_post( Cache_Check::describe( Cache_Check::result() ) ), esc_url( Cache_Check::button_url() ), esc_html__( 'Check cache now', 'otkazpro' ) ); } $conflict = (int) get_option( Install::CONFLICT_OPTION ); if ( $conflict && current_user_can( self::CAP ) && current_user_can( 'edit_pages' ) ) { self::conflict_notice( $conflict ); return; } $page_id = (int) Settings::get( 'page_id' ); if ( current_user_can( self::CAP ) && ! Form::is_valid_page( $page_id ) ) { printf( '

%1$s %2$s

%4$s

', esc_html__( 'OtkazPro:', 'otkazpro' ), esc_html__( 'The withdrawal form page is missing, not published or no longer contains the form. Customers are sent to a fallback page so the function keeps working – select a page with [otkazpro] in the settings.', 'otkazpro' ), esc_url( Settings::url() ), esc_html__( 'Open settings', 'otkazpro' ) ); } } /** * Is the current admin screen one of the OtkazPro screens (register or its settings tab)? * * @return bool */ private static function is_own_screen() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; return self::is_screen( 'woocommerce_page_' . self::PAGE ) || ( self::is_screen( 'woocommerce_page_wc-settings' ) && Settings::TAB === $tab ); } /** * Is the current admin screen the given one? * * @param string $id Screen ID. * @return bool */ private static function is_screen( $id ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; return $screen && $id === $screen->id; } /** * "A page with this slug already exists" notice with two choices. * * @param int $page_id Existing page ID. */ private static function conflict_notice( $page_id ) { $post = get_post( $page_id ); if ( ! $post ) { delete_option( Install::CONFLICT_OPTION ); return; } $url = static function ( $choice ) use ( $page_id ) { return wp_nonce_url( add_query_arg( array( 'action' => 'otkazpro_page', 'choice' => $choice, 'page_id' => $page_id ), admin_url( 'admin-post.php' ) ), 'otkazpro_page' ); }; ?>

' . esc_html( get_the_title( $post ) ) . '', esc_html( get_post_status_object( $post->post_status ) ? get_post_status_object( $post->post_status )->label : $post->post_status ) ) ); ?>

Project Yordanov' ); } return $meta; } /** * Discreet credit in the admin footer, only on OtkazPro screens. * * @param string $text Footer text. * @return string */ public static function footer_credit( $text ) { if ( ! self::is_own_screen() ) { return $text; } return sprintf( /* translators: 1: plugin name, 2: version, 3: agency name with link */ esc_html__( '%1$s %2$s · developed by %3$s', 'otkazpro' ), 'OtkazPro', esc_html( PY_OTKAZ_VERSION ), 'Project Yordanov' ); } /** * Small admin CSS for badges. */ public static function admin_css() { wp_register_style( 'otkazpro-admin', false, array(), PY_OTKAZ_VERSION ); wp_enqueue_style( 'otkazpro-admin' ); wp_add_inline_style( 'otkazpro-admin', '.otkazpro-badge{display:inline-block;padding:2px 8px;border-radius:10px;background:#f0f0f1;color:#50575e;font-size:12px;line-height:1.6;white-space:nowrap}' . '.otkazpro-badge--ok{background:#edfaef;color:#00691c}' . '.otkazpro-badge--warn{background:#fcf9e8;color:#8a4b00}' . '.otkazpro-badge--error{background:#fcf0f1;color:#a00}' . '.otkazpro-admin-items{margin:0}' . '.otkazpro-admin-items li{margin:0 0 2px}' . '.otkazpro-actions{margin:6px 0 0}' . '.column-refund .button{margin:4px 4px 0 0}' . '.column-py_otkaz{width:90px}' ); } /** * Orders list column. * * @param array $columns Columns. * @return array */ public static function orders_column( $columns ) { $new = array(); foreach ( $columns as $key => $label ) { $new[ $key ] = $label; if ( 'order_status' === $key ) { $new['py_otkaz'] = __( 'Withdrawal', 'otkazpro' ); } } if ( ! isset( $new['py_otkaz'] ) ) { $new['py_otkaz'] = __( 'Withdrawal', 'otkazpro' ); } return $new; } /** * Orders list column content. * * @param string $column Column. * @param int|\WC_Order $order Order or post ID. */ public static function orders_column_content( $column, $order ) { if ( 'py_otkaz' !== $column ) { return; } $order = $order instanceof \WC_Order ? $order : Verifier::get_order( (int) $order ); if ( ! $order ) { return; } $history = Order_Data::history( $order ); if ( ! $history ) { return; } $numbers = wp_list_pluck( $history, 'number' ); printf( '%s', esc_url( self::url( array( 's' => $order->get_order_number() ) ) ), esc_attr( implode( ', ', $numbers ) ), esc_html( count( $history ) > 1 /* translators: %d: count */ ? sprintf( __( 'Withdrawal ×%d', 'otkazpro' ), count( $history ) ) : __( 'Withdrawal', 'otkazpro' ) ) ); } /** * Meta box on the order edit screen. * * @param string $screen_id Screen ID. * @param \WP_Post|object $object Post or order. */ public static function meta_box( $screen_id, $object = null ) { $screens = array( 'shop_order' ); if ( function_exists( 'wc_get_page_screen_id' ) ) { $screens[] = wc_get_page_screen_id( 'shop-order' ); } if ( ! in_array( $screen_id, $screens, true ) ) { return; } $order = $object instanceof \WP_Post ? Verifier::get_order( $object->ID ) : ( $object instanceof \WC_Order ? $object : null ); if ( ! $order || ! Order_Data::history( $order ) ) { return; } add_meta_box( 'otkazpro-order', __( 'Withdrawal from contract', 'otkazpro' ), array( __CLASS__, 'meta_box_content' ), $screen_id, 'side', 'high' ); } /** * Meta box content. * * @param \WP_Post|\WC_Order $object Post or order. */ public static function meta_box_content( $object ) { $order = $object instanceof \WP_Post ? Verifier::get_order( $object->ID ) : $object; if ( ! $order ) { return; } $deadline = Order_Data::deadline( $order ); foreach ( Submission::for_order( $order->get_id() ) as $s ) { echo '
'; echo '

' . esc_html( $s->number ) . ' · ' . esc_html( Util::format_gmt( $s->created_gmt, 'datetime' ) ) . '

'; echo wp_kses_post( self::items_html( $s ) ); echo '

' . wp_kses_post( self::deadline_badge( $s ) . ' ' . self::email_badge( $s ) ) . '

'; echo wp_kses_post( '
' . self::refund_html( $s, $order ) . '
' ); echo '
'; } if ( $deadline ) { /* translators: %s: date */ echo '

' . esc_html( sprintf( __( 'Indicative deadline: %s', 'otkazpro' ), wp_date( get_option( 'date_format' ), $deadline->getTimestamp() ) ) ) . '

'; } echo '

' . esc_html__( '“Prepare refund” fills in the WooCommerce refund form of this order; check the amount and confirm it. The plugin never refunds or restocks by itself.', 'otkazpro' ) . '

'; } }