modify( '-' . ($days - 1) . ' days' ); $previous = $start->modify( '-' . $days . ' days' ); $end = $today->format( 'Y-m-d' ); $from = $start->format( 'Y-m-d' ); $prior = $previous->format( 'Y-m-d' ); $prior_end = $start->modify( '-1 day' )->format( 'Y-m-d' ); $facts = DashboardFacts::sync(); if ( empty( $facts['ready'] ) ) { return array( 'building' => true, 'done' => $facts['done'], 'total' => $facts['total'], ); } $errors = array(); $zero = array( 'net' => 0.0, 'gross' => 0.0, 'refunds' => 0.0, 'orders' => 0, ); $sale = DashboardTotals::part( $errors, 'sales', static fn() => DashboardTotals::sales( $prior, $from, $end, $currency ), array( 'totals' => $zero, 'previous' => $zero, 'series' => array(), 'channels' => array(), 'excluded' => array(), ) ); $totals = $sale['totals']; $before = $sale['previous']; $series = $sale['series']; $channels = $sale['channels']; $top = DashboardTotals::part( $errors, 'top', static fn() => DashboardTotals::top( $from, $end, $currency ), array() ); $cash = array( 'incoming' => 0, 'outgoing' => 0, ); // Store sales another plugin's till records: the till and collection figures leave them out, and the card says so (admin audit A3). $other_till = DashboardTotals::part( $errors, 'other_till', static fn() => DashboardTotals::other_till( $from, $end, $currency ), null ); $balances = DashboardTotals::part( $errors, 'due', static fn() => DashboardTotals::due( $currency ), array( 'value' => null, 'count' => null, 'complete' => false, ) ); $due = $balances['value']; $due_complete = $balances['complete']; // Full recommendation rows belong to Stock. Home needs only the engine's cached count. $needs = array(); $need_count = 0; // Products to replenish: paid build. // The latest entries, each with what it was about and the screen that opens it (persona fixes REP-4). $recent = DashboardTotals::part( $errors, 'events', static fn() => Activity::recent( 8 ), array() ); $currencyRows = DashboardTotals::part( $errors, 'currencies', static fn() => DashboardTotals::rows( $wpdb->get_results( $wpdb->prepare( 'SELECT DISTINCT currency FROM %i ORDER BY currency', OrderIndex::table() ), ARRAY_A ) ), array() ); $currencies = array_values( array_unique( array_merge( array(get_woocommerce_currency(), $currency), array_values( array_filter( array_column( $currencyRows, 'currency' ), static fn( $c ) => isset( get_woocommerce_currencies()[$c] ) ) ) ) ) ); // Customer payment notices, the purchasing/count alerts and the Woo checkout payments card (it opens the finance screen): paid build. $queues = DashboardTotals::part( $errors, 'queues', static function () use($wpdb) { $q = array_map( 'intval', DashboardTotals::row( $wpdb->get_results( $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(OrderIndex::table()), SaleStatus::QUOTES ) ), ARRAY_A ) ) ); return $q; }, array( 'payment_notices' => 0, ) ); $alerts = array(); $online = null; // The part of the refunds that exchanges passed on to replacement orders: credit, not money back (persona fixes REP-1). $exchanged = DashboardTotals::part( $errors, 'sales', static fn() => DashboardFacts::exchanged( $prior, $from, $end, $currency ), array( 'current' => 0.0, 'previous' => 0.0, ) ); $totals['exchanged'] = $exchanged['current']; $before['exchanged'] = $exchanged['previous']; // Cash on delivery still to collect and the channels the store has orders in: the order index's totals and channel list // (persona fixes REP-1 and REP-3, from B02's totals=1 and channels=1), read without a page of orders (rows=0). $index = DashboardTotals::part( $errors, 'cod', static fn() => OrderIndex::query( array( 'rows' => 0, 'totals' => 1, 'channels' => 1, ) ), null ); $cod = ( is_array( $index ) ? array( 'count' => (int) ($index['totals']['cod']['count'] ?? 0), 'due' => (float) ($index['totals']['cod']['due'][$currency] ?? 0), 'complete' => !empty( $index['totals_complete'] ), ) : null ); $gateways = ( function_exists( 'WC' ) && WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : array() ); $cod_enabled = isset( $gateways['cod'] ) && 'yes' === $gateways['cod']->enabled; // The Woo checkout card counts online gateways only (OnlinePayments): with none switched on it is left out (REP-1). $online_enabled = false; // Only the currencies the select offers have their names here (REP-3: not WooCommerce's ~170). $currency_options = array_intersect_key( get_woocommerce_currencies(), array_flip( $currencies ) ); return array( 'from' => $from, 'to' => $end, 'days' => $days, 'previous_from' => $prior, 'previous_to' => $prior_end, 'currency' => $currency, 'currencies' => $currencies, 'currency_options' => $currency_options, 'cod' => $cod, 'cod_enabled' => $cod_enabled, 'online_enabled' => $online_enabled, 'channels_used' => ( is_array( $index ) ? array_values( (array) ($index['channels'] ?? array()) ) : null ), 'totals' => $totals, 'previous' => $before, 'series' => array_values( $series ), 'channels' => array_values( $channels ), 'top' => array_slice( $top, 0, 5 ), 'cash' => $cash, 'other_till' => $other_till, 'online' => $online, 'due' => ( $due_complete ? $due : null ), 'due_count' => $balances['count'], 'due_complete' => $due_complete, 'needs' => array_slice( $needs, 0, 5 ), 'need_count' => $need_count, 'queues' => $queues, 'alerts' => $alerts, 'errors' => $errors, 'excluded' => $sale['excluded'], 'events' => $recent, 'as_of' => current_time( 'Y-m-d H:i' ), ); } }