ArcWOC/admin/class-hesabix-v2-orders-list-table.php
morrning 8aedcf2500 Release 4.9.0: invoice profit in WP + marketplace license banner
Show Hesabix invoice profit on ArcWOC orders list and order meta box,
refresh after sync, and surface WooCommerce marketplace license upsell
(hidden when active). Also includes 4.8.2 bank/cash take≤100 fixes.
2026-08-04 12:56:06 +00:00

465 lines
13 KiB
PHP
Executable file

<?php
/**
* فهرست سفارش‌ها برای پنل حسابیکس.
*
* @package Hesabix_V2
* @subpackage Hesabix_V2/admin
*/
if (!defined('WPINC')) {
die;
}
if (!class_exists('WP_List_Table')) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* @extends WP_List_Table
*/
class Hesabix_V2_Orders_List_Table extends WP_List_Table
{
/**
* @return void
*/
public function __construct()
{
parent::__construct(
array(
'singular' => 'order',
'plural' => 'orders',
'ajax' => false,
)
);
}
/**
* @return array<string,string>
*/
protected function get_bulk_actions()
{
return array();
}
/**
* @return void
*/
public function no_items()
{
esc_html_e('سفارشی در این فیلتر یافت نشد.', 'hesabix-v2');
}
/**
* @return array<string,string>
*/
public function get_columns()
{
return array(
'cb' => '<input type="checkbox" />',
'order' => __('سفارش', 'hesabix-v2'),
'date' => __('تاریخ', 'hesabix-v2'),
'status' => __('وضعیت', 'hesabix-v2'),
'customer' => __('خریدار', 'hesabix-v2'),
'total' => __('مبلغ', 'hesabix-v2'),
'profit' => __('سود', 'hesabix-v2'),
'hesabix' => __('حسابیکس', 'hesabix-v2'),
'pause' => __('همگام خودکار', 'hesabix-v2'),
'actions' => __('عملیات', 'hesabix-v2'),
);
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_cb($item)
{
return sprintf('<input type="checkbox" name="order_ids[]" value="%d" />', (int) $item->get_id());
}
/**
* فقط سفارش فروشگاه (نه برگشت وجه / refund).
*
* @param mixed $item
* @return bool
*/
private static function is_listable_shop_order($item)
{
if (!$item instanceof WC_Order) {
return false;
}
// WC_Order_Refund / Admin Overrides\OrderRefund هم از WC_Order ارث می‌برند.
if (is_a($item, 'WC_Order_Refund')) {
return false;
}
$type = method_exists($item, 'get_type') ? (string) $item->get_type() : '';
if ($type === 'shop_order_refund') {
return false;
}
return true;
}
/**
* @param mixed $item
* @return string
*/
private static function safe_edit_order_url($item)
{
if ($item instanceof WC_Order && method_exists($item, 'get_edit_order_url')) {
$url = $item->get_edit_order_url();
if (is_string($url) && $url !== '') {
return $url;
}
}
$id = ($item instanceof WC_Order) ? (int) $item->get_id() : 0;
if ($id > 0) {
return admin_url('post.php?post=' . $id . '&action=edit');
}
return '';
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_order($item)
{
$edit = self::safe_edit_order_url($item);
$num = $item->get_order_number();
if ($edit === '') {
return '<strong>#' . esc_html((string) $num) . '</strong>';
}
return sprintf('<a href="%s"><strong>#%s</strong></a>', esc_url($edit), esc_html((string) $num));
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_date($item)
{
$d = $item->get_date_created();
return $d ? esc_html($d->date_i18n(get_option('date_format') . ' ' . get_option('time_format'))) : '—';
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_status($item)
{
return esc_html(wc_get_order_status_name($item->get_status()));
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_customer($item)
{
$name = trim($item->get_formatted_billing_full_name());
if ($name === '') {
$name = __('بدون نام', 'hesabix-v2');
}
$email = $item->get_billing_email();
$phone = $item->get_billing_phone();
$bits = array(esc_html($name));
if ($email !== '') {
$bits[] = '<span class="description">' . esc_html($email) . '</span>';
}
if ($phone !== '') {
$bits[] = '<span class="description">' . esc_html($phone) . '</span>';
}
return implode('<br />', $bits);
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_total($item)
{
return wp_kses_post($item->get_formatted_order_total());
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_profit($item)
{
if (!class_exists('Hesabix_V2_Invoice_Profit_Service')) {
return '—';
}
return Hesabix_V2_Invoice_Profit_Service::render_list_cell($item);
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_hesabix($item)
{
$row = Hesabix_V2_Invoice_Service::get_sync_status((int) $item->get_id());
if (!$row || empty($row['hesabix_id'])) {
return '<span class="hesabix-v2-badge hesabix-v2-badge-muted">' . esc_html__('ارسال نشده', 'hesabix-v2') . '</span>';
}
$st = isset($row['sync_status']) ? (string) $row['sync_status'] : 'synced';
$hid = (int) $row['hesabix_id'];
$label = __('شناسه فاکتور:', 'hesabix-v2') . ' ' . $hid;
if ($st === 'error') {
$err = isset($row['error_message']) ? (string) $row['error_message'] : '';
$short = $err !== '' ? mb_substr($err, 0, 120) : '';
return '<span class="hesabix-v2-badge hesabix-v2-badge-error">' . esc_html__('خطا', 'hesabix-v2') . '</span><br />'
. '<small>' . esc_html($label) . '</small>'
. ($short !== '' ? '<br /><span class="description">' . esc_html($short) . '</span>' : '');
}
if ($st === 'pending') {
return '<span class="hesabix-v2-badge hesabix-v2-badge-pending">' . esc_html__('در انتظار', 'hesabix-v2') . '</span><br /><small>' . esc_html($label) . '</small>';
}
return '<span class="hesabix-v2-badge hesabix-v2-badge-ok">' . esc_html__('ارسال شده', 'hesabix-v2') . '</span><br /><small>' . esc_html($label) . '</small>';
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_pause($item)
{
$oid = (int) $item->get_id();
$paused = Hesabix_V2_Order_Sync_Meta::is_pause_auto_sync($oid);
$paused_attr = $paused ? '1' : '0';
if ($paused) {
$txt = '<span class="hesabix-v2-badge hesabix-v2-badge-warn">' . esc_html__('متوقف', 'hesabix-v2') . '</span>';
} else {
$txt = '<span class="hesabix-v2-badge hesabix-v2-badge-muted">' . esc_html__('فعال', 'hesabix-v2') . '</span>';
}
$btn_label = $paused ? __('از سرگیری خودکار', 'hesabix-v2') : __('توقف خودکار', 'hesabix-v2');
$btn = sprintf(
'<button type="button" class="button button-small hesabix-v2-pause-toggle" data-order-id="%d" data-paused="%s">%s</button>',
$oid,
esc_attr($paused_attr),
esc_html($btn_label)
);
return $txt . '<br />' . $btn;
}
/**
* @param WC_Order $item
* @return string
*/
protected function column_actions($item)
{
$syn = __('ارسال / به‌روزرسانی', 'hesabix-v2');
$uns = __('لغو ارسال', 'hesabix-v2');
$oid = (int) $item->get_id();
return sprintf(
'<button type="button" class="button button-small hesabix-v2-order-sync" data-order-id="%d">%s</button> '
. '<button type="button" class="button button-small hesabix-v2-order-unsync" data-order-id="%d">%s</button>',
$oid,
esc_html($syn),
$oid,
esc_html($uns)
);
}
/**
* @param WC_Order $item
* @param string $column_name
* @return string
*/
protected function column_default($item, $column_name)
{
return '';
}
/**
* @param string $which
* @return void
*/
protected function extra_tablenav($which)
{
if ($which !== 'top') {
return;
}
$current = isset($_GET['hesabix_filter']) ? sanitize_key(wp_unslash((string) $_GET['hesabix_filter'])) : 'all';
$opts = array(
'all' => __('همهٔ سفارش‌ها', 'hesabix-v2'),
'synced' => __('ارسال‌شده به حسابیکس', 'hesabix-v2'),
'not_synced' => __('ارسال‌نشده', 'hesabix-v2'),
'error' => __('خطای همگام‌سازی', 'hesabix-v2'),
'pending' => __('در انتظار (نگاشت)', 'hesabix-v2'),
);
echo '<div class="alignleft actions hesabix-v2-order-filters">';
echo '<label for="hesabix_filter" class="screen-reader-text">' . esc_html__('فیلتر حسابیکس', 'hesabix-v2') . '</label>';
echo '<select name="hesabix_filter" id="hesabix_filter">';
foreach ($opts as $val => $lab) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr($val),
selected($current, $val, false),
esc_html($lab)
);
}
echo '</select>';
submit_button(__('فیلتر', 'hesabix-v2'), 'secondary', 'filter_action', false);
echo '</div>';
}
/**
* Default wc_get_orders() uses statuses from wc_get_order_statuses() or equivalent; checkout-draft is excluded from that set (e.g. HPOS OrdersTableQuery::sanitize_status). Include it explicitly for CPT + HPOS.
*
* @return array<int,string>
*/
private static function order_statuses_for_list_query()
{
$statuses = array_keys( wc_get_order_statuses() );
if (!in_array('wc-checkout-draft', $statuses, true)) {
$statuses[] = 'wc-checkout-draft';
}
return $statuses;
}
/**
* آرگومان پایهٔ فهرست: فقط shop_order تا refund داخل لیست نیاید (HPOS).
*
* @param array<string,mixed> $extra
* @return array<string,mixed>
*/
private static function shop_order_query_args(array $extra = array())
{
$base = array(
'type' => 'shop_order',
'limit' => 20,
'paginate' => true,
'orderby' => 'date',
'order' => 'DESC',
'status' => self::order_statuses_for_list_query(),
);
return array_merge($base, $extra);
}
/**
* @param mixed $orders
* @return array<int,WC_Order>
*/
private static function filter_shop_orders($orders)
{
if (!is_array($orders)) {
return array();
}
$out = array();
foreach ($orders as $order) {
if (self::is_listable_shop_order($order)) {
$out[] = $order;
}
}
return $out;
}
/**
* @return void
*/
public function prepare_items()
{
$this->items = array();
$columns = $this->get_columns();
$this->_column_headers = array($columns, array(), $this->get_sortable_columns());
$per_page = 20;
$current_page = max(1, (int) $this->get_pagenum());
$filter = isset($_GET['hesabix_filter']) ? sanitize_key(wp_unslash((string) $_GET['hesabix_filter'])) : 'all';
$business_id = (int) get_option('hesabix_v2_business_id');
global $wpdb;
$map_table = $wpdb->prefix . 'hesabix_v2';
if (in_array($filter, array('synced', 'error', 'pending'), true)) {
$status_map = array(
'synced' => 'synced',
'error' => 'error',
'pending' => 'pending',
);
$st = $status_map[ $filter ];
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from trusted prefix
$total_items = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$map_table} WHERE entity_type = %s AND business_id = %d AND sync_status = %s",
'order',
$business_id,
$st
)
);
$offset = ($current_page - 1) * $per_page;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$wc_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT wc_id FROM {$map_table} WHERE entity_type = %s AND business_id = %d AND sync_status = %s ORDER BY COALESCE(last_sync_at, created_at) DESC, wc_id DESC LIMIT %d OFFSET %d",
'order',
$business_id,
$st,
$per_page,
$offset
)
);
$this->items = self::filter_shop_orders(
array_map('wc_get_order', array_map('intval', $wc_ids))
);
} elseif ($filter === 'not_synced') {
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$mapped = $wpdb->get_col(
$wpdb->prepare(
"SELECT wc_id FROM {$map_table} WHERE entity_type = %s AND business_id = %d",
'order',
$business_id
)
);
$mapped = array_map('intval', $mapped);
$args = self::shop_order_query_args(
array(
'limit' => $per_page,
'page' => $current_page,
)
);
if (!empty($mapped)) {
$args['exclude'] = $mapped;
}
$query = wc_get_orders($args);
if (is_object($query) && isset($query->orders)) {
$this->items = self::filter_shop_orders($query->orders);
$total_items = (int) $query->total;
} else {
$this->items = self::filter_shop_orders(is_array($query) ? $query : array());
$total_items = count($this->items);
}
} else {
$query = wc_get_orders(
self::shop_order_query_args(
array(
'limit' => $per_page,
'page' => $current_page,
)
)
);
if (is_object($query) && isset($query->orders)) {
$this->items = self::filter_shop_orders($query->orders);
$total_items = (int) $query->total;
} else {
$this->items = self::filter_shop_orders(is_array($query) ? $query : array());
$total_items = count($this->items);
}
}
$this->set_pagination_args(
array(
'total_items' => $total_items,
'per_page' => $per_page,
'total_pages' => max(1, (int) ceil($total_items / $per_page)),
)
);
}
}