→ detail {recipient, orderer, delivery, note, gift, items, amounts, tracking, aoo_url, …}
* GET summary → totals strip (cached like before — aggregate numbers only)
* 403 `orders_disabled` / `summary_disabled` = the shop hasn't turned on "ให้หลังบ้าน WordPress เห็นสินค้าและคำสั่งซื้อจากเว็บ"
* (`wordpress.share_summary` in aoo).
*
* Privacy: orders carry customer names / phones / addresses → fetched live on every view through AooShop_Api_Client::get()
* (no transient, no option, no log) and only kept in this request's memory. Read-only — status changes happen in AOO.
* Capability: AooShop_Admin::view_cap() (filter `aooshop_admin_capability`, default manage_options).
*
* @package AooShop
*/
defined( 'ABSPATH' ) || exit;
class AooShop_Admin_Orders {
const SUMMARY_ENDPOINT = 'summary';
const NONCE = 'aooshop_overview_refresh';
/** Status tabs in aoo's order (keys = aoo `tab` values). */
const TABS = array( 'all', 'awaiting_payment', 'to_ship', 'shipped', 'cancelled' );
/** @var AooShop_Settings */
private $settings;
/** @var AooShop_Api_Client */
private $api;
/** @var AooShop_Cache */
private $cache;
public function __construct( AooShop_Settings $settings, AooShop_Api_Client $api, AooShop_Cache $cache ) {
$this->settings = $settings;
$this->api = $api;
$this->cache = $cache;
}
private static function tab_labels() {
return array(
'all' => __( 'All', 'aoo-shop' ),
'awaiting_payment' => __( 'Awaiting payment', 'aoo-shop' ),
'to_ship' => __( 'To ship', 'aoo-shop' ),
'shipped' => __( 'Shipped', 'aoo-shop' ),
'cancelled' => __( 'Cancelled', 'aoo-shop' ),
);
}
// ── summary strip (aggregate numbers — cached like the old overview) ──
private function drop_cached_summary() {
$key = AooShop_Cache::data_key( self::SUMMARY_ENDPOINT, array() );
delete_transient( AooShop_Cache::PREFIX . $key );
delete_transient( AooShop_Cache::STALE_PREFIX . $key );
}
/**
* @param bool $force Skip the fresh copy ("โหลดใหม่").
* @return array|null Summary payload, or null when unavailable for any reason (the strip is just left out).
*/
private function summary( $force ) {
$res = $this->cache->get_data( self::SUMMARY_ENDPOINT, array(), $force );
if ( ! is_wp_error( $res ) && 'stale' === $res['source'] && ! get_transient( AooShop_Cache::DOWN_KEY ) ) {
// Stale copy after a real answer (403 / 503) — not an outage: never keep showing it.
$this->drop_cached_summary();
return null;
}
if ( is_wp_error( $res ) ) {
if ( 'aoo_summary_disabled' === $res->get_error_code() ) {
$this->drop_cached_summary();
}
return null;
}
$data = is_array( $res['payload'] ) ? $res['payload'] : array();
return isset( $data['today'], $data['last7'] ) ? $data : null;
}
private function render_summary( array $d ) {
$today = isset( $d['today'] ) && is_array( $d['today'] ) ? $d['today'] : array();
$last7 = isset( $d['last7'] ) && is_array( $d['last7'] ) ? $d['last7'] : array();
$stats = array(
array( __( 'Today', 'aoo-shop' ), AooShop_Admin::money( isset( $today['amount'] ) ? $today['amount'] : 0 ), sprintf( /* translators: %s: number of orders */ __( '%s orders', 'aoo-shop' ), number_format_i18n( isset( $today['orders'] ) ? (int) $today['orders'] : 0 ) ), '' ),
array( __( 'Last 7 days', 'aoo-shop' ), AooShop_Admin::money( isset( $last7['amount'] ) ? $last7['amount'] : 0 ), sprintf( /* translators: %s: number of orders */ __( '%s orders', 'aoo-shop' ), number_format_i18n( isset( $last7['orders'] ) ? (int) $last7['orders'] : 0 ) ), '' ),
array( __( 'Awaiting payment', 'aoo-shop' ), number_format_i18n( isset( $d['awaiting_payment'] ) ? (int) $d['awaiting_payment'] : 0 ), __( 'orders', 'aoo-shop' ), 'awaiting_payment' ),
array( __( 'To ship', 'aoo-shop' ), number_format_i18n( isset( $d['to_ship'] ) ? (int) $d['to_ship'] : 0 ), __( 'paid orders', 'aoo-shop' ), 'to_ship' ),
);
echo '
';
foreach ( $stats as $st ) {
$num = '
' . esc_html( $st[1] ) . '
';
if ( $st[3] ) {
$num = '
' . $num . '';
}
echo '
' . esc_html( $st[0] ) . '
' . wp_kses_post( $num )
. '
' . esc_html( $st[2] ) . '
';
}
echo '
';
}
// ── screen ────────────────────────────────────────────────────────
private function aoo_settings_url() {
return $this->settings->api_base() . '/settings/storefront/wordpress';
}
public function render() {
if ( ! current_user_can( AooShop_Admin::view_cap() ) ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only screen; filters only.
$number = isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : '';
// `status`, not `tab` — `?page=aoo-shop&tab=x` is the legacy screen URL that AooShop_Admin::load_overview() redirects.
$tab = isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : '';
$tab = in_array( $tab, self::TABS, true ) ? $tab : 'all';
$q = isset( $_GET['s'] ) ? substr( sanitize_text_field( wp_unslash( $_GET['s'] ) ), 0, 80 ) : '';
$page = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1;
$force = isset( $_GET['aoo_refresh'], $_GET['_wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), self::NONCE );
// phpcs:enable
echo '';
AooShop_Admin::header( __( 'Orders', 'aoo-shop' ) );
if ( ! $this->settings->is_configured() ) {
$this->notice_box(
__( 'Add your AOO API key first. Web orders will then appear here.', 'aoo-shop' ),
'',
array( array( AooShop_Admin::url( AooShop_Admin::SETTINGS ), __( 'Go to connection settings', 'aoo-shop' ), 'button-primary', false ) )
);
echo '
';
return;
}
if ( '' !== $number ) {
$this->render_detail( $number );
} else {
$this->render_list( $tab, $q, $page, $force );
}
echo '';
}
/**
* @param array $buttons [url, label, class, external]
*/
private function notice_box( $text, $desc, array $buttons = array(), $error = false ) {
echo '' . esc_html( $text ) . '
';
if ( '' !== $desc ) {
echo '
' . esc_html( $desc ) . '
';
}
if ( $buttons ) {
echo '
';
foreach ( $buttons as $b ) {
echo '' . esc_html( $b[1] ) . ' ';
}
echo '
';
}
echo '
';
}
/** Friendly box for an API error · returns true when something was printed. */
private function render_error( WP_Error $err ) {
$code = $err->get_error_code();
if ( 'aoo_orders_disabled' === $code ) {
$this->notice_box(
__( "Turn on 'Show web products and orders in WordPress admin' in AOO › Settings › Storefront › Connect WordPress.", 'aoo-shop' ),
__( 'When it is on, admins of this site can see the name, phone number and shipping address of web orders. The data is fetched live from AOO every time and is never stored in WordPress.', 'aoo-shop' ),
array(
array( $this->aoo_settings_url(), __( 'Open settings in AOO', 'aoo-shop' ), 'button-primary', true ),
array( AooShop_Admin::url( AooShop_Admin::SLUG ), __( 'Turned on — reload', 'aoo-shop' ), '', false ),
)
);
return;
}
if ( 'aoo_order_not_found' === $code ) {
$this->notice_box( __( 'This order was not found among web orders.', 'aoo-shop' ), '', array( array( AooShop_Admin::url( AooShop_Admin::SLUG ), __( 'Back to orders', 'aoo-shop' ), '', false ) ) );
return;
}
$this->notice_box(
/* translators: %s: error message */
sprintf( __( 'Could not load orders from AOO: %s', 'aoo-shop' ), $err->get_error_message() ),
'',
array( array( add_query_arg( array() ), __( 'Try again', 'aoo-shop' ), '', false ) ),
true
);
}
private function render_list( $tab, $q, $page, $force ) {
$res = $this->api->get( 'admin/orders', array( 'tab' => $tab, 'q' => $q, 'page' => $page > 1 ? $page : null ) );
if ( is_wp_error( $res ) ) {
$this->render_error( $res );
return;
}
$d = is_array( $res['data'] ) ? $res['data'] : array();
$items = isset( $d['items'] ) && is_array( $d['items'] ) ? $d['items'] : array();
$counts = isset( $d['counts'] ) && is_array( $d['counts'] ) ? $d['counts'] : array();
$total = isset( $d['total'] ) ? (int) $d['total'] : 0;
$per = isset( $d['per_page'] ) ? max( 1, (int) $d['per_page'] ) : 20;
$base = AooShop_Admin::url( AooShop_Admin::SLUG );
$sum = $this->summary( $force );
if ( $sum ) {
$this->render_summary( $sum );
}
// Status tabs (WP "subsubsub" like the posts / Woo orders screens).
echo '';
// Search.
echo '';
$this->pagination( $total, $per, $page, 'top' );
echo ''
. '| ' . esc_html__( 'Order', 'aoo-shop' ) . ' | '
. '' . esc_html__( 'Date', 'aoo-shop' ) . ' | '
. '' . esc_html__( 'Customer', 'aoo-shop' ) . ' | '
. '' . esc_html__( 'Province / district', 'aoo-shop' ) . ' | '
. '' . esc_html__( 'Qty', 'aoo-shop' ) . ' | '
. '' . esc_html__( 'Total', 'aoo-shop' ) . ' | '
. '' . esc_html__( 'Status', 'aoo-shop' ) . ' | '
. '
';
if ( ! $items ) {
$empty = '' !== $q ? __( 'No orders match your search.', 'aoo-shop' ) : __( 'No web orders in this view yet.', 'aoo-shop' );
echo '| ' . esc_html( $empty ) . ' |
';
}
foreach ( $items as $o ) {
if ( ! is_array( $o ) || empty( $o['number'] ) ) {
continue;
}
$detail = add_query_arg( array( 'order' => (string) $o['number'] ), $base );
$aoo = AooShop_Admin::http_url( isset( $o['aoo_url'] ) ? $o['aoo_url'] : '' );
echo '';
echo '| #' . esc_html( (string) $o['number'] ) . ''
. ' | ';
echo '' . esc_html( isset( $o['created_label'] ) ? (string) $o['created_label'] : '' ) . ' | ';
echo '' . esc_html( isset( $o['name'] ) && '' !== $o['name'] ? (string) $o['name'] : '–' )
. ( ! empty( $o['phone'] ) ? ' ' . esc_html( (string) $o['phone'] ) . ' ' : '' ) . ' | ';
echo '' . esc_html( isset( $o['area'] ) && '' !== $o['area'] ? (string) $o['area'] : '–' ) . ' | ';
echo '' . esc_html( number_format_i18n( isset( $o['qty'] ) ? (float) $o['qty'] : 0 ) ) . ' | ';
echo '' . esc_html( AooShop_Admin::money( isset( $o['total'] ) ? $o['total'] : 0 ) ) . ' | ';
echo '' . wp_kses_post( AooShop_Admin::status_pill( isset( $o['order_status'] ) ? $o['order_status'] : null ) )
. ' ' . wp_kses_post( AooShop_Admin::status_pill( isset( $o['payment_status'] ) ? $o['payment_status'] : null ) ) . ' | ';
echo '
';
}
echo '
';
$this->pagination( $total, $per, $page, 'bottom' );
$links = isset( $d['links']['orders'] ) ? AooShop_Admin::http_url( $d['links']['orders'] ) : '';
$fresh = wp_nonce_url( add_query_arg( array( 'aoo_refresh' => 1 ), add_query_arg( array() ) ), self::NONCE );
echo '' . esc_html__( 'Web orders only · Read-only — change status, confirm payment or ship in AOO · Customer data is fetched live from AOO and not stored in WordPress.', 'aoo-shop' ) . '
';
echo '';
if ( $links ) {
echo '' . esc_html__( 'Open all orders in AOO', 'aoo-shop' ) . ' ';
}
echo '' . esc_html__( 'Reload', 'aoo-shop' ) . '
';
}
private function pagination( $total, $per, $page, $which ) {
$pages = (int) ceil( $total / $per );
echo ''
. '' . esc_html( sprintf( /* translators: %s: number of orders */ _n( '%s item', '%s items', $total, 'aoo-shop' ), number_format_i18n( $total ) ) ) . '';
if ( $pages > 1 ) {
echo '';
}
echo '
';
}
// ── detail ────────────────────────────────────────────────────────
private function render_detail( $number ) {
$back = AooShop_Admin::url( AooShop_Admin::SLUG );
echo '← ' . esc_html__( 'All orders', 'aoo-shop' ) . '
';
$res = $this->api->get( 'admin/orders/' . $number );
if ( is_wp_error( $res ) ) {
$this->render_error( $res );
return;
}
$o = is_array( $res['data'] ) ? $res['data'] : array();
if ( empty( $o['number'] ) ) {
$this->notice_box( __( 'The order data from AOO is incomplete.', 'aoo-shop' ), '', array(), true );
return;
}
$aoo = AooShop_Admin::http_url( isset( $o['aoo_url'] ) ? $o['aoo_url'] : '' );
$str = static function ( $arr, $key ) {
return is_array( $arr ) && isset( $arr[ $key ] ) && is_scalar( $arr[ $key ] ) ? trim( (string) $arr[ $key ] ) : '';
};
echo '' . esc_html( sprintf( /* translators: %s: order number */ __( 'Order #%s', 'aoo-shop' ), (string) $o['number'] ) ) . '
'
. wp_kses_post( AooShop_Admin::status_pill( isset( $o['order_status'] ) ? $o['order_status'] : null ) )
. wp_kses_post( AooShop_Admin::status_pill( isset( $o['payment_status'] ) ? $o['payment_status'] : null ) )
. '
' . esc_html( $str( $o, 'created_label' ) ) . ''
. ( $aoo ? '
' . esc_html__( 'Open in AOO', 'aoo-shop' ) . '' : '' )
. '
';
echo '';
// Items.
$items = isset( $o['items'] ) && is_array( $o['items'] ) ? $o['items'] : array();
echo '
' . esc_html__( 'Items', 'aoo-shop' ) . '
| ' . esc_html__( 'Items', 'aoo-shop' ) . ' | ' . esc_html__( 'Quantity', 'aoo-shop' ) . ' | ' . esc_html__( 'Unit price', 'aoo-shop' ) . ' | ' . esc_html__( 'Amount', 'aoo-shop' ) . ' |
';
foreach ( $items as $it ) {
if ( ! is_array( $it ) ) {
continue;
}
$img = AooShop_Admin::http_url( isset( $it['image'] ) ? $it['image'] : '' );
$img = $img ? '
' : '';
$var = $str( $it, 'variation' );
echo '| ' . wp_kses_post( $img ) . ' | ' . esc_html( $str( $it, 'name' ) ) . ( '' !== $var ? ' ' . esc_html( $var ) . ' ' : '' ) . ' | '
. '' . esc_html( number_format_i18n( isset( $it['qty'] ) ? (float) $it['qty'] : 0 ) ) . ' | '
. '' . esc_html( AooShop_Admin::money( isset( $it['unit_price'] ) ? $it['unit_price'] : 0 ) ) . ' | '
. '' . esc_html( AooShop_Admin::money( isset( $it['total'] ) ? $it['total'] : 0 ) ) . ' |
';
}
echo '
';
$a = isset( $o['amounts'] ) && is_array( $o['amounts'] ) ? $o['amounts'] : array();
$rows = array( array( __( 'Subtotal', 'aoo-shop' ), isset( $a['items'] ) ? $a['items'] : 0, false ) );
if ( ! empty( $a['discount'] ) && (float) $a['discount'] > 0 ) {
$rows[] = array( __( 'Discount', 'aoo-shop' ), - (float) $a['discount'], false );
}
if ( ! empty( $a['shipping'] ) && (float) $a['shipping'] > 0 ) {
$rows[] = array( __( 'Shipping', 'aoo-shop' ), $a['shipping'], false );
}
if ( ! empty( $a['gift_card'] ) && (float) $a['gift_card'] > 0 ) {
$rows[] = array( __( 'Greeting card', 'aoo-shop' ), $a['gift_card'], false );
}
$rows[] = array( __( 'Grand total', 'aoo-shop' ), isset( $a['total'] ) ? $a['total'] : 0, true );
echo '
';
foreach ( $rows as $r ) {
echo '| ' . esc_html( $r[0] ) . ' | ' . esc_html( AooShop_Admin::money( $r[1] ) ) . ' |
';
}
echo '
';
// Shipping + delivery.
$rc = isset( $o['recipient'] ) && is_array( $o['recipient'] ) ? $o['recipient'] : array();
$addr = trim( implode( ' ', array_filter( array( $str( $rc, 'address' ), $str( $rc, 'district' ), $str( $rc, 'amphoe' ), $str( $rc, 'province' ), $str( $rc, 'postal_code' ) ), 'strlen' ) ) );
echo '
';
echo '
' . esc_html__( 'Shipping address', 'aoo-shop' ) . '
';
$kv = array(
array( __( 'Recipient', 'aoo-shop' ), $str( $rc, 'name' ) ),
array( __( 'Phone', 'aoo-shop' ), $str( $rc, 'phone' ) ),
array( __( 'Address', 'aoo-shop' ), $addr ),
);
$or = isset( $o['orderer'] ) && is_array( $o['orderer'] ) ? $o['orderer'] : null;
if ( $or ) {
$kv[] = array( __( 'Ordered by', 'aoo-shop' ), trim( $str( $or, 'name' ) . ' ' . $str( $or, 'phone' ) ) );
}
$dl = isset( $o['delivery'] ) && is_array( $o['delivery'] ) ? $o['delivery'] : array();
if ( '' !== $str( $dl, 'date_label' ) ) {
$kv[] = array( __( 'Delivery date', 'aoo-shop' ), $str( $dl, 'date_label' ) );
}
if ( '' !== $str( $dl, 'slot' ) ) {
$kv[] = array( __( 'Delivery slot', 'aoo-shop' ), $str( $dl, 'slot' ) );
}
if ( '' !== $str( $dl, 'zone' ) ) {
$kv[] = array( __( 'Delivery zone', 'aoo-shop' ), $str( $dl, 'zone' ) );
}
$tr = isset( $o['tracking'] ) && is_array( $o['tracking'] ) ? $o['tracking'] : null;
if ( $tr ) {
$kv[] = array( __( 'Tracking number', 'aoo-shop' ), trim( $str( $tr, 'carrier' ) . ' ' . $str( $tr, 'number' ) ) );
}
foreach ( $kv as $row ) {
echo '| ' . esc_html( $row[0] ) . ' | ' . ( '' !== $row[1] ? wp_kses_post( nl2br( esc_html( $row[1] ) ) ) : '–' ) . ' |
';
}
echo '
';
$note = $str( $o, 'note' );
$gift = isset( $o['gift'] ) && is_array( $o['gift'] ) ? $o['gift'] : null;
if ( '' !== $note || $gift ) {
echo '
' . esc_html__( 'Note', 'aoo-shop' ) . '
';
if ( '' !== $note ) {
echo '
' . nl2br( esc_html( $note ) ) . '
';
}
if ( $gift ) {
echo '
' . esc_html__( 'Greeting card', 'aoo-shop' ) . '';
foreach ( array( 'to' => __( 'To', 'aoo-shop' ), 'from' => __( 'From', 'aoo-shop' ), 'message' => __( 'Message', 'aoo-shop' ) ) as $k => $label ) {
if ( '' !== $str( $gift, $k ) ) {
echo '
' . esc_html( $label ) . ': ' . nl2br( esc_html( $str( $gift, $k ) ) );
}
}
echo '
';
}
echo '
';
}
echo '
';
echo '' . esc_html__( 'Read-only — change status, confirm payment or ship in AOO · Customer data is fetched live from AOO and not stored in WordPress.', 'aoo-shop' ) . '
';
}
}