$id, 'title' => I18n::t( $title ), 'description' => I18n::t( $description ), 'url' => admin_url( 'admin.php?page=' . $url ), 'complete' => $complete, 'count' => $count, 'tone' => $tone, ); } private static function role() : array { $user = wp_get_current_user(); if ( in_array( 'storesuit_cashier', $user->roles, true ) || current_user_can( Roles::POS ) && !current_user_can( 'manage_woocommerce' ) ) { $message = I18n::t( 'Find a recent order. Management and financial settings stay out of your way.' ); // Selling at the till: paid build. return array( 'key' => 'cashier', 'name' => I18n::t( 'Cashier' ), 'message' => $message, ); } if ( in_array( 'storesuit_staff', $user->roles, true ) || current_user_can( 'manage_woocommerce' ) && !current_user_can( 'storesuit_finance' ) ) { return array( 'key' => 'staff', 'name' => I18n::t( 'Store Staff' ), 'message' => I18n::t( 'Prepare orders, help customers, and keep products and stock moving.' ), ); } return array( 'key' => 'owner', 'name' => I18n::t( 'Store Owner' ), 'message' => I18n::t( 'See what needs attention, finish setup, and keep every sales channel moving.' ), ); } private static function active_payments() : int { $count = 0; foreach ( WC()->payment_gateways()->payment_gateways() as $gateway ) { if ( $gateway->enabled === 'yes' ) { ++$count; } } return $count; } public static function read() : array { Store::require_cap( 'storesuit_orders' ); global $wpdb; // Home no longer reads the order queue first (admin.js load()): the index's upkeep that read did (orders waiting to be // read again, the time zone and phone passes, a lost refresh job) is done here, before the counts. if ( get_option( 'storesuit_order_index_ready' ) === '1' ) { OrderIndex::sync(); } $role = self::role(); $table = OrderIndex::table(); // A quote is no sale yet (0.62.9): one placeholder per quote status. $orders = ( get_option( 'storesuit_order_index_ready' ) === '1' ? (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE status NOT IN (' . implode( ',', array_fill( 0, count( SaleStatus::QUOTES ), '%s' ) ) . ')', array_merge( array($table), SaleStatus::QUOTES ) ) ) : 0 ); $queues = ( get_option( 'storesuit_order_index_ready' ) === '1' ? $wpdb->get_row( $wpdb->prepare( 'SELECT COALESCE(SUM(needs_payment),0) payment,COALESCE(SUM(needs_fulfil),0) fulfil,COALESCE(SUM(needs_return),0) returns_count FROM %i WHERE status NOT IN (' . implode( ',', array_fill( 0, count( SaleStatus::QUOTES ), '%s' ) ) . ')', array_merge( array($table), SaleStatus::QUOTES ) ), ARRAY_A ) : array( 'payment' => 0, 'fulfil' => 0, 'returns_count' => 0, ) ); $channels = array(); if ( get_option( 'storesuit_order_index_ready' ) === '1' ) { foreach ( $wpdb->get_results( $wpdb->prepare( 'SELECT channel,COUNT(*) total FROM %i WHERE status NOT IN (' . implode( ',', array_fill( 0, count( SaleStatus::QUOTES ), '%s' ) ) . ') GROUP BY channel', array_merge( array($table), SaleStatus::QUOTES ) ), ARRAY_A ) as $row ) { $channels[(string) $row['channel']] = (int) $row['total']; } } $products = (int) (wp_count_posts( 'product' )->publish ?? 0); $store_address = trim( implode( ' ', array_filter( array(get_option( 'woocommerce_store_address', '' ), get_option( 'woocommerce_store_city', '' ), get_option( 'woocommerce_store_postcode', '' )) ) ) ); $store_ready = $store_address !== '' && get_woocommerce_currency() !== '' && WC()->countries->get_base_country() !== ''; $setup = array(); if ( current_user_can( 'manage_woocommerce' ) ) { $setup[] = self::item( 'store', 'Add your store basics', 'Set the address, country, and currency used for sales and delivery.', 'wc-settings', $store_ready ); $product_step = 'Add the product with its price and shelf stock so shoppers can buy it.'; // Till and hand-recorded sales: paid build. $setup[] = self::item( 'products', 'Add your first product', $product_step, 'storesuit-products&new=1', DemoData::without( 'products', $products ) > 0 ); $setup[] = self::item( 'payments', 'Choose how customers pay', 'Enable at least one WooCommerce payment method. Cash and bank transfer are valid choices.', 'wc-settings&tab=checkout', self::active_payments() > 0 ); } $work = array(); if ( $role['key'] === 'cashier' ) { // The till: paid build. $work[] = self::item( 'find', 'Find an order', 'Search by order number, customer, phone, or email.', 'storesuit-orders', false, $orders ); } else { $work[] = self::item( 'fulfil', 'Prepare waiting orders', 'Open the orders that need picking, packing, or hand-off.', 'storesuit-orders&queue=fulfil', false, (int) $queues['fulfil'], ( (int) $queues['fulfil'] ? 'attention' : 'normal' ) ); // Returns: paid build. if ( current_user_can( 'storesuit_finance' ) ) { $work[] = self::item( 'payment', 'Check money still to collect', 'Open orders with a remaining balance; a customer message alone is not a verified payment.', 'storesuit-orders&queue=payment', false, (int) $queues['payment'], ( (int) $queues['payment'] ? 'attention' : 'normal' ) ); } } // Background jobs that stopped altogether (Denetim AYR-1): first on the list while it lasts. $stopped = ( current_user_can( 'manage_woocommerce' ) ? Health::cron_stopped() : null ); if ( $stopped !== null ) { $item = self::item( 'cron-stopped', 'Background jobs have stopped', 'Open Health to see why.', 'storesuit-settings#ss-health', false, 0, 'attention' ); $item['description'] = I18n::format( 'WordPress has run no background job for %s minutes: emails, stock updates and scheduled work are waiting. Open Health to see why.', (int) floor( $stopped / 60 ) ); array_unshift( $work, $item ); } $web_guide = 'Open WooCommerce orders in the same payment, preparation, and delivery flow.'; // Returns: paid build. $guides = array(self::item( 'web', 'Web store order', $web_guide, 'storesuit-orders&channel=web', ($channels['web'] ?? 0) > 0, (int) ($channels['web'] ?? 0) )); $stock_sale_report = null; // What the team did with refunds, cancellations, returns, stock, prices and discounts (owner, 28 Oct 2026): paid build, // for the people who manage the team. $team_activity = null; $done = count( array_filter( $setup, static fn( $step ) => $step['complete'] ) ); return array( 'role' => $role, 'first_run' => $orders === 0, 'setup' => array( 'steps' => $setup, 'done' => $done, 'total' => count( $setup ), 'complete' => ( $setup ? $done === count( $setup ) : true ), ), 'work' => $work, 'stock_sale_report' => $stock_sale_report, 'team_activity' => $team_activity, 'guides' => $guides, 'counts' => array( 'orders' => $orders, 'products' => $products, ), 'generated_at' => current_time( 'Y-m-d H:i' ), ); } /** Works out one person's preview and keeps it (Home, or the background job below). */ private static function sale_preview( int $user ) : array { $report = array(); return $report; } /** The background job (Store::later() from read()): as that person, so the rows pass the same permission checks. */ public static function preview_recount( $user = 0 ) : void { $user = absint( $user ); $before = get_current_user_id(); if ( !$user || !user_can( $user, 'manage_woocommerce' ) ) { return; } wp_set_current_user( $user ); try { self::sale_preview( $user ); } catch ( \Throwable $e ) { /* Home works it out itself next time. */ } finally { wp_set_current_user( $before ); } } }