getActiveTab();
}
/**
* Prepare the items for the table to process.
*
* @return void
*/
public function prepare_items() {
$columns = $this->get_columns();
$hidden = $this->get_hidden_columns();
$sortable = $this->get_sortable_columns();
$this->_column_headers = array( $columns, $hidden, $sortable );
$collection = $this->table_data();
if ( is_wp_error( $collection ) ) {
$this->items = [];
return;
}
$this->items = $collection->data;
// `table_data()` drops non-actionable orders, so the server count
// over-reports. Show the filtered count, but pass `total_pages`
// explicitly so WP_List_Table doesn't recompute it from the smaller
// count and hide pages.
$per_page = $this->get_items_per_page( 'shipping_labels', 20 );
$server_count = (int) ( $collection->pagination->count ?? 0 );
$this->set_pagination_args(
[
'total_items' => count( $this->items ),
'total_pages' => max( 1, (int) ceil( $server_count / max( 1, $per_page ) ) ),
'per_page' => $per_page,
]
);
}
/**
* Tab links, without count badges.
*
* @return array
*/
protected function get_views() {
$tabs = [
'all' => __( 'All', 'surecart' ),
'pending_labels' => __( 'Pending', 'surecart' ),
];
$active = $this->getActiveTab();
$base = admin_url( 'admin.php?page=sc-shipping-labels' );
$tab_links = [];
foreach ( $tabs as $tab => $label ) {
$link = esc_url( add_query_arg( 'tab', $tab, $base ) );
$current = $tab === $active ? ' class="current" aria-current="page"' : '';
$tab_links[ $tab ] = "" . esc_html( $label ) . '';
}
return $tab_links;
}
/**
* Define the columns to use in the listing table.
*
* @return array
*/
public function get_columns() {
return [
'cb' => '',
'order' => __( 'Order', 'surecart' ),
'status' => __( 'Status', 'surecart' ),
'customer' => __( 'Customer', 'surecart' ),
'items' => __( 'Items', 'surecart' ),
'ship_to' => __( 'Ship to', 'surecart' ),
'created' => __( 'Date', 'surecart' ),
'mode' => '',
];
}
/**
* Define the sortable columns.
*
* @return array
*/
public function get_sortable_columns() {
return [
'created' => [ 'created_at', false ],
];
}
/**
* Checkbox column. Value is the order id on the All tab, the shipment
* id on the Pending tab.
*
* @param \SureCart\Models\Order|\SureCart\Models\Shipment $item The row model.
*
* @return void
*/
public function column_cb( $item ) {
$value = $this->isPendingTab() ? ( $item->id ?? '' ) : ( $this->resolveOrder( $item )->id ?? '' );
// Unique per-row screen reader label so rows are distinguishable.
if ( $this->isPendingTab() ) {
$label = sprintf(
// translators: %s: shipment id.
__( 'Select shipment %s', 'surecart' ),
$value
);
} else {
$order = $this->resolveOrder( $item );
$number = $order ? ( $order->number ?? $order->id ) : $value;
$label = sprintf(
// translators: %s: order number.
__( 'Select order %s', 'surecart' ),
$number
);
}
?>
get_items_per_page( 'shipping_labels', 20 );
// Sort goes in where() — Model::paginate() discards a `sort` argument.
$conditions = [];
$orderby = $this->get_orderby();
$order = $this->get_order();
$map = $this->get_sort_map();
if ( $orderby && isset( $map[ $orderby ] ) ) {
$conditions['sort'] = $map[ $orderby ] . ':' . $order;
}
if ( $this->isPendingTab() ) {
// Array `status` serializes as a repeating param the platform reads
// as an IN-set. No `query` — the shipments endpoint has no search.
return Shipment::where(
array_merge(
[ 'status' => [ 'draft', 'quoted' ] ],
$conditions
)
)->with(
[
'fulfillment',
'fulfillment.fulfillment_items',
'fulfillment.order',
'shipment.to_contact',
'contact.address',
]
)->paginate(
[
'page' => $this->get_pagenum(),
'per_page' => $per_page,
]
);
}
$collection = Order::where(
array_merge(
[
'status' => [ 'paid' ],
'fulfillment_status' => [ 'unfulfilled', 'partially_fulfilled', 'fulfilled' ],
'shipment_status' => [ 'unshipped', 'partially_shipped' ],
'query' => $this->get_search_query(),
],
$conditions
)
)->with(
[
'checkout',
'checkout.customer',
'checkout.shipping_address',
'checkout.line_items',
'line_item.price',
'price.product',
'fulfillments',
'fulfillment.shipments',
]
)->paginate(
[
'page' => $this->get_pagenum(),
'per_page' => $per_page,
]
);
if ( is_wp_error( $collection ) ) {
return $collection;
}
// A `fulfilled` order with no pending-label shipment has no shippable
// work left, so hide it to match the modern DataView. Per-page filter
// only — pagination totals stay server-based. Rebuilt through the
// constructor because `$collection->data = …` would fatal via __set.
$rows = array_values(
array_filter(
$collection->data ?? [],
function ( $order ) {
if ( ! is_object( $order ) ) {
return false;
}
return ! ( 'fulfilled' === ( $order->fulfillment_status ?? '' ) && ! $this->hasPendingLabelShipment( $order ) );
}
)
);
return new Collection(
[
'data' => $rows,
'pagination' => $collection->pagination,
]
);
}
/**
* Resolve the row's order — the row itself on the All tab,
* `fulfillment.order` on the Pending tab.
*
* @param \SureCart\Models\Order|\SureCart\Models\Shipment $item The row model.
*
* @return object|null
*/
private function resolveOrder( $item ): ?object {
if ( ! $this->isPendingTab() ) {
return is_object( $item ) ? $item : null;
}
if ( is_object( $item->fulfillment ?? null ) && is_object( $item->fulfillment->order ?? null ) ) {
return $item->fulfillment->order;
}
return null;
}
/**
* Order column — `#number` link to the order edit screen.
*
* @param \SureCart\Models\Order|\SureCart\Models\Shipment $item The row model.
*
* @return string
*/
public function column_order( $item ) {
$order = $this->resolveOrder( $item );
if ( ! $order ) {
return '-';
}
$number = $order->number ?? $order->id;
ob_start();
?>
#
*/
protected function getFulfillmentTags(): array {
return [
'unfulfilled' => [
'type' => 'warning',
'label' => __( 'Unfulfilled', 'surecart' ),
],
'partially_fulfilled' => [
'type' => 'warning',
'label' => __( 'Partially Fulfilled', 'surecart' ),
],
'fulfilled' => [
'type' => 'success',
'label' => __( 'Fulfilled', 'surecart' ),
],
];
}
/**
* Whether an order has a shipment still awaiting a label — no
* `label_url` and a non-terminal status. Mirrors
* `isPendingLabelShipment` in packages/admin/shipping-labels/utils.js.
*
* @param object $order The resolved order.
*
* @return bool
*/
protected function hasPendingLabelShipment( object $order ): bool {
$fulfillments = $order->fulfillments->data ?? $order->fulfillments ?? [];
foreach ( $fulfillments as $fulfillment ) {
if ( ! is_object( $fulfillment ) ) {
continue;
}
$shipments = $fulfillment->shipments->data ?? $fulfillment->shipments ?? [];
foreach ( $shipments as $shipment ) {
if ( ! is_object( $shipment ) ) {
continue;
}
if ( ! empty( $shipment->label_url ) ) {
continue;
}
if ( ! in_array( $shipment->status ?? '', self::DONE_SHIPMENT_STATUSES, true ) ) {
return true;
}
}
}
return false;
}
/**
* Status column. Collapses the modern DataView's per-shipment tags into
* one per-order status on the All tab.
*
* @param \SureCart\Models\Order|\SureCart\Models\Shipment $item The row model.
*
* @return string
*/
public function column_status( $item ) {
if ( $this->isPendingTab() ) {
return '