*/
class PizzaPilot_Kitchen {
/**
* The ID of this plugin.
*
* @since 1.1.0
* @access private
* @var string $plugin_name The ID of this plugin.
*/
private $plugin_name;
/**
* The version of this plugin.
*
* @since 1.1.0
* @access private
* @var string $version The current version of this plugin.
*/
private $version;
/**
* Initialize the class and set its properties.
*
* @since 1.1.0
* @param string $plugin_name The name of this plugin.
* @param string $version The version of this plugin.
*/
public function __construct( $plugin_name, $version ) {
$this->plugin_name = $plugin_name;
$this->version = $version;
}
/**
* Register the kitchen orders submenu page under PizzaPilot.
*
* @since 1.1.0
* @return void
*/
public function add_kitchen_menu() {
add_submenu_page(
'pizzapilot-settings',
__( 'Kitchen Orders', 'pizzapilot' ),
__( 'Kitchen Orders', 'pizzapilot' ),
'edit_shop_orders',
'pizzapilot-kitchen',
array( $this, 'render_kitchen_page' )
);
}
/**
* Enqueue styles for the kitchen page.
*
* @since 1.1.0
* @param string $hook_suffix The current admin page hook suffix.
* @return void
*/
public function enqueue_kitchen_styles( $hook_suffix ) {
if ( 'pizzapilot_page_pizzapilot-kitchen' !== $hook_suffix ) {
return;
}
wp_enqueue_style(
$this->plugin_name . '-kitchen',
plugin_dir_url( __FILE__ ) . 'css/pizzapilot-kitchen.css',
array(),
$this->version,
'all'
);
}
/**
* Add contextual help tab to the Kitchen Orders page.
*
* Provides guidance on using the kitchen order view via the
* WordPress Help dropdown at the top of the screen.
*
* @since 1.1.0
* @return void
*/
public function add_kitchen_help_tab() {
$screen = get_current_screen();
if ( ! $screen || 'pizzapilot_page_pizzapilot-kitchen' !== $screen->id ) {
return;
}
$screen->add_help_tab(
array(
'id' => 'pizzapilot-help-kitchen',
'title' => __( 'Kitchen Orders', 'pizzapilot' ),
'content' =>
'
' . __( 'This page shows today\'s orders grouped by delivery time slot.', 'pizzapilot' ) . '
' .
'' .
'- ' . __( 'Each card shows the customer name, delivery type, and ordered items.', 'pizzapilot' ) . '
' .
'- ' . __( 'Click "Mark Completed" to mark an order as done. This also sets the WooCommerce order status to completed and removes it from this view.', 'pizzapilot' ) . '
' .
'- ' . __( 'Use the Refresh button to reload orders — the page does not auto-update.', 'pizzapilot' ) . '
' .
'
',
)
);
}
/**
* Render the kitchen orders admin page.
*
* @since 1.1.0
* @return void
*/
public function render_kitchen_page() {
$today = current_time( 'Y-m-d' );
$grouped_orders = $this->get_orders_grouped_by_slot( $today );
echo '';
echo '
' . esc_html__( 'Kitchen Orders', 'pizzapilot' ) . '
';
$this->render_pro_upsell_banner();
echo '
';
echo esc_html__( 'Orders for today, grouped by time slot.', 'pizzapilot' ) . ' ';
echo '';
echo esc_html__( 'Refresh', 'pizzapilot' );
echo '';
echo '
';
if ( empty( $grouped_orders ) ) {
echo '
';
echo '
' . esc_html__( 'No orders for today.', 'pizzapilot' ) . '
';
echo '
';
} else {
foreach ( $grouped_orders as $group ) {
$this->render_slot_group( $group['label'], $group['orders'] );
}
}
echo '
';
}
/**
* Render a group of orders under a time slot heading.
*
* @since 1.1.0
* @param string $slot_label The time slot label (e.g., "2:00 PM - 2:30 PM").
* @param array $orders Array of WC_Order objects in this slot.
* @return void
*/
private function render_slot_group( $slot_label, $orders ) {
$order_count = count( $orders );
// Derive the slot date from the first order's delivery-time meta so the
// heading reflects the actual stored slot, not "today".
$slot_date = '';
if ( ! empty( $orders ) ) {
$first_order = reset( $orders );
$delivery_time = $first_order->get_meta( '_wc_other/pizzapilot/delivery-time', true );
if ( empty( $delivery_time ) ) {
$delivery_time = $first_order->get_meta( '_pizzapilot_delivery_time', true );
}
if ( ! empty( $delivery_time ) && is_numeric( $delivery_time ) ) {
$slot_date = wp_date( 'D jS M', (int) $delivery_time );
}
}
echo '';
echo '
';
if ( '' !== $slot_date ) {
echo '' . esc_html( $slot_date ) . ' ';
}
echo esc_html( $slot_label );
echo ' ';
echo '(' . esc_html(
sprintf(
/* translators: %d: number of orders */
_n( '%d order', '%d orders', $order_count, 'pizzapilot' ),
$order_count
)
) . ')';
echo '';
echo '
';
echo '
';
foreach ( $orders as $order ) {
$this->render_order_card( $order );
}
echo '
';
echo '
';
}
/**
* Render a single order card.
*
* @since 1.1.0
* @param WC_Order $order The WooCommerce order object.
* @return void
*/
private function render_order_card( $order ) {
$order_id = $order->get_id();
$customer_name = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
$completed = 'yes' === $order->get_meta( '_pizzapilot_kitchen_completed', true );
$delivery_type = $order->get_meta( '_wc_other/pizzapilot/delivery-type', true );
if ( empty( $delivery_type ) ) {
$delivery_type = $order->get_meta( '_pizzapilot_delivery_type', true );
}
$card_class = 'pizzapilot-order-card';
if ( $completed ) {
$card_class .= ' pizzapilot-order-completed';
}
echo '';
echo '';
echo '
';
foreach ( $order->get_items() as $item ) {
echo '- ';
echo esc_html( $item->get_quantity() . 'x ' . $item->get_name() );
echo '
';
}
echo '
';
echo '';
echo '
';
}
/**
* Render the completion toggle form for an order.
*
* @since 1.1.0
* @param int $order_id The order ID.
* @param bool $completed Whether the order is marked as completed.
* @return void
*/
private function render_completion_toggle( $order_id, $completed ) {
$action_url = admin_url( 'admin-post.php' );
echo '';
}
/**
* Handle the mark completed/incomplete form submission.
*
* @since 1.1.0
* @return void
*/
public function handle_mark_completed() {
if ( ! isset( $_POST['pizzapilot_kitchen_nonce'], $_POST['order_id'] ) ) {
wp_die( esc_html__( 'Invalid request.', 'pizzapilot' ) );
}
$order_id = absint( $_POST['order_id'] );
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pizzapilot_kitchen_nonce'] ) ), 'pizzapilot_kitchen_toggle_' . $order_id ) ) {
wp_die( esc_html__( 'Security check failed.', 'pizzapilot' ) );
}
if ( ! current_user_can( 'edit_shop_orders' ) ) {
wp_die( esc_html__( 'You do not have permission to perform this action.', 'pizzapilot' ) );
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
wp_die( esc_html__( 'Order not found.', 'pizzapilot' ) );
}
$new_status = isset( $_POST['completed'] ) ? sanitize_text_field( wp_unslash( $_POST['completed'] ) ) : 'no';
$new_status = 'yes' === $new_status ? 'yes' : 'no';
$order->update_meta_data( '_pizzapilot_kitchen_completed', $new_status );
// Sync with WooCommerce order status so completed orders drop out of
// the kitchen view (which only queries processing / on-hold orders).
if ( 'yes' === $new_status ) {
$order->set_status( 'completed', __( 'Marked completed from PizzaPilot Kitchen.', 'pizzapilot' ) );
} elseif ( 'no' === $new_status && 'completed' === $order->get_status() ) {
$order->set_status( 'processing', __( 'Returned to processing from PizzaPilot Kitchen.', 'pizzapilot' ) );
}
$order->save();
/**
* Fires when an order's kitchen completion status is toggled.
*
* @since 1.1.0
* @param int $order_id The order ID.
* @param string $new_status The new completion status ('yes' or 'no').
*/
do_action( 'pizzapilot_kitchen_order_toggled', $order_id, $new_status );
wp_safe_redirect( admin_url( 'admin.php?page=pizzapilot-kitchen' ) );
exit;
}
/**
* Render the Pro upsell banner on the kitchen page.
*
* Shows a dismissible notice encouraging upgrade to Pro for advanced
* kitchen features. Tracks dismissal in user meta so it stays hidden.
* Only shown when Pro is not active.
*
* @since 1.1.0
* @return void
*/
private function render_pro_upsell_banner() {
if ( Pizzapilot_Helpers::pizzapilot_is_pro_active( 'Pizzapilot_Pro' ) ) {
return;
}
$user_id = get_current_user_id();
$dismissed = get_user_meta( $user_id, 'pizzapilot_kitchen_pro_dismissed', true );
if ( $dismissed ) {
return;
}
$upgrade_url = admin_url( 'admin.php?page=pizzapilot-upgrade' );
// No 'page' argument here, deliberately. wp-admin/admin.php renders the
// plugin page for a 'page' request and exits before it reaches the
// do_action( "admin_action_{$action}" ) dispatch further down the file,
// so including it meant handle_dismiss_kitchen_pro() never ran and the
// banner could never be dismissed. The handler redirects back to the
// kitchen page itself, so the argument was never needed.
$dismiss_url = wp_nonce_url(
add_query_arg(
array(
'action' => 'pizzapilot_dismiss_kitchen_pro',
),
admin_url( 'admin.php' )
),
'pizzapilot_dismiss_kitchen_pro'
);
echo '';
}
/**
* Handle dismissal of the kitchen Pro upsell banner.
*
* Saves a user meta flag so the banner stays hidden for that user.
* Verifies nonce and redirects back to the kitchen page.
*
* @since 1.1.0
* @return void
*/
public function handle_dismiss_kitchen_pro() {
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'pizzapilot_dismiss_kitchen_pro' ) ) {
wp_die( esc_html__( 'Security check failed.', 'pizzapilot' ) );
}
update_user_meta( get_current_user_id(), 'pizzapilot_kitchen_pro_dismissed', '1' );
wp_safe_redirect( admin_url( 'admin.php?page=pizzapilot-kitchen' ) );
exit;
}
/**
* Get orders for a given date, grouped by time slot.
*
* Queries WooCommerce orders that have PizzaPilot delivery time meta
* for the specified date, and groups them by their time slot.
*
* @since 1.1.0
* @param string $date Date in Y-m-d format.
* @return array Associative array of slot_label => array of WC_Order objects.
*/
public function get_orders_grouped_by_slot( $date ) {
$timezone = wp_timezone();
$date_obj = new DateTime( $date, $timezone );
$day_start = (int) $date_obj->setTime( 0, 0, 0 )->format( 'U' );
$day_end = (int) $date_obj->setTime( 23, 59, 59 )->format( 'U' );
// Query orders with PizzaPilot delivery time within today's range.
// A meta query is required here: WooCommerce stores the slot timestamp
// as order meta, and the kitchen view is admin-only and scoped to a
// single day, so the result set is small.
//
// The slot timestamp lives under one of two keys: block checkout stores
// it as '_wc_other/pizzapilot/delivery-time', classic checkout as
// '_pizzapilot_delivery_time'. Hence the OR.
//
// Deliberately no 'orderby' => 'meta_value_num' / 'meta_key' here.
// Setting them joins on that single meta key, which discards every
// order that carries only the other one - so a store on classic
// checkout saw an empty kitchen page. Ordering is not lost: the slot
// groups built below are sorted by timestamp with uasort(), and orders
// inside a group all share the same slot time anyway.
$query_args = array(
'limit' => -1,
'status' => array( 'wc-processing', 'wc-on-hold' ),
);
if ( \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
$query_args['meta_query'] = self::delivery_time_meta_query( $day_start, $day_end ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
} else {
// The legacy (posts) order store does not accept 'meta_query': it
// logs a _doing_it_wrong notice and drops it, which returned every
// processing/on-hold order from any date. Pass the range as a
// custom query var instead; filter_legacy_orders_query() turns it
// back into a meta_query on the underlying WP_Query.
$query_args['pizzapilot_delivery_time_range'] = array( $day_start, $day_end );
add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', array( __CLASS__, 'filter_legacy_orders_query' ), 10, 2 );
}
$orders = wc_get_orders( $query_args );
remove_filter( 'woocommerce_order_data_store_cpt_get_orders_query', array( __CLASS__, 'filter_legacy_orders_query' ), 10 );
$groups = array();
foreach ( $orders as $order ) {
$delivery_time = $order->get_meta( '_wc_other/pizzapilot/delivery-time', true );
if ( empty( $delivery_time ) ) {
$delivery_time = $order->get_meta( '_pizzapilot_delivery_time', true );
}
if ( empty( $delivery_time ) || ! is_numeric( $delivery_time ) ) {
continue;
}
$delivery_time = (int) $delivery_time;
// Backstop in case a query filter or order store ignored the range.
if ( $delivery_time < $day_start || $delivery_time > $day_end ) {
continue;
}
$slot_datetime = new DateTime( '@' . $delivery_time );
$slot_datetime->setTimezone( $timezone );
$end_datetime = clone $slot_datetime;
$end_datetime->modify( '+30 minutes' );
$slot_label = $slot_datetime->format( 'g:i A' ) . ' - ' . $end_datetime->format( 'g:i A' );
// Key groups by the full slot start (date + time) so identical
// times on different dates do not collapse into one group.
$group_key = $slot_datetime->format( 'YmdHi' );
if ( ! isset( $groups[ $group_key ] ) ) {
$groups[ $group_key ] = array(
'timestamp' => $delivery_time,
'label' => $slot_label,
'orders' => array(),
);
}
$groups[ $group_key ]['orders'][] = $order;
}
// Sort groups by the full slot timestamp (date + time) ASC.
uasort(
$groups,
function ( $a, $b ) {
return $a['timestamp'] <=> $b['timestamp'];
}
);
return $groups;
}
/**
* Build the meta query matching either PizzaPilot slot-time key within a range.
*
* @since 1.2.2
* @param int $day_start Range start (Unix timestamp, inclusive).
* @param int $day_end Range end (Unix timestamp, inclusive).
* @return array Meta query array.
*/
private static function delivery_time_meta_query( $day_start, $day_end ) {
return array(
'relation' => 'OR',
array(
'key' => '_wc_other/pizzapilot/delivery-time',
'value' => array( $day_start, $day_end ),
'compare' => 'BETWEEN',
'type' => 'NUMERIC',
),
array(
'key' => '_pizzapilot_delivery_time',
'value' => array( $day_start, $day_end ),
'compare' => 'BETWEEN',
'type' => 'NUMERIC',
),
);
}
/**
* Apply the slot-time range to a legacy (posts) order store query.
*
* Hooked to woocommerce_order_data_store_cpt_get_orders_query only for
* the duration of the kitchen query in get_orders_grouped_by_slot().
*
* @since 1.2.2
* @param array $query WP_Query arguments built by WooCommerce.
* @param array $query_vars Arguments passed to wc_get_orders().
* @return array WP_Query arguments.
*/
public static function filter_legacy_orders_query( $query, $query_vars ) {
if ( empty( $query_vars['pizzapilot_delivery_time_range'] ) ) {
return $query;
}
list( $day_start, $day_end ) = $query_vars['pizzapilot_delivery_time_range'];
$query['meta_query'] = isset( $query['meta_query'] ) ? (array) $query['meta_query'] : array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$query['meta_query'][] = self::delivery_time_meta_query( (int) $day_start, (int) $day_end );
return $query;
}
}