' . esc_html( I18n::t( 'Store data is loading…' ) ) . '
'Today', 'storesuit-orders' => 'Sales', 'storesuit-products' => 'Products', 'storesuit-customers' => 'Customers', 'storesuit-marketing' => 'Marketing', 'storesuit-reports' => 'Reports', 'storesuit-settings' => 'Settings & Help', ) as $slug => $source_label ) { $label = I18n::t( $source_label ); $cap = ( in_array( $slug, array( 'storesuit-products', 'storesuit-customers', 'storesuit-marketing', 'storesuit-reports', 'storesuit-settings' ), true ) ? 'manage_woocommerce' : 'storesuit_orders' ); $hook = add_submenu_page( 'storesuit', $label, $label, $cap, $slug, array(__CLASS__, 'render') ); // The menu's first entry is "Today" and Home is the same page: WordPress names the browser tab after the entry, so Home names its own. if ( 'storesuit' === $slug && $hook ) { add_action( 'load-' . $hook, array(__CLASS__, 'home_title') ); } } // The carts page is "Live carts" in the free build (PLAN-0.63 Q14); StoreRuler Pro names it after its abandoned carts. $hidden = array( 'storesuit-guide' => 'Quick actions', 'storesuit-carts' => 'Live carts', 'storesuit-stock-alerts' => 'Waiting customers', 'storesuit-mail-log' => 'Email history', 'storesuit-redirects' => 'Redirects and 404s', 'storesuit-modules' => 'All features', 'storesuit-audit' => 'Activity log', ); foreach ( $hidden as $slug => $source_label ) { $label = I18n::t( $source_label ); $cap = ( in_array( $slug, array( 'storesuit-audit', 'storesuit-carts', 'storesuit-stock-alerts', 'storesuit-mail-log', 'storesuit-redirects' ), true ) ? 'manage_woocommerce' : 'storesuit_orders' ); $hook = add_submenu_page( '', $label, $label, $cap, $slug, array(__CLASS__, 'render') ); if ( $hook ) { add_action( 'load-' . $hook, static function () use($label) { $GLOBALS['title'] = $label; } ); } } // Keep old bookmarks working, but do not present the same order desk twice. $order_desk = I18n::t( 'Order desk' ); $hook = add_submenu_page( '', $order_desk, $order_desk, 'storesuit_orders', 'storesuit-operations', array(__CLASS__, 'render') ); if ( $hook ) { add_action( 'load-' . $hook, static function () use($order_desk) { $GLOBALS['title'] = $order_desk; } ); } self::$navigation = array(); self::$navigation = Navigation::groups( self::$navigation ); // StoreRuler Pro's page: the last entry of the submenu while it is offered (ProPage). ProPage::menu(); } /** load-{page}: the browser tab of Home reads "Home"; the day's work list (view=today) keeps the menu's "Today". */ public static function home_title() : void { if ( Design::home_page() ) { $GLOBALS['title'] = I18n::t( 'Home' ); } } public static function assets() : void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Only picks the scripts and styles of the admin page being shown; nothing is changed. $page = ( isset( $_GET['page'] ) && is_string( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '' ); // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( strpos( $page, 'storesuit' ) !== 0 ) { return; } // StoreRuler Pro's page is plain HTML over the shell: its stylesheet only, none of the store screens' scripts. if ( $page === ProPage::SLUG ) { ProPage::assets(); return; } wp_enqueue_style( 'storesuit', plugins_url( 'assets/admin.css', STORESUIT_FILE ), array(), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/admin.css' ) ); wp_enqueue_style( 'storesuit-dashboard', plugins_url( 'assets/dashboard.css', STORESUIT_FILE ), array('storesuit'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/dashboard.css' ) ); wp_enqueue_style( 'storesuit-counter', plugins_url( 'assets/counter.css', STORESUIT_FILE ), array('storesuit-dashboard', 'storesuit-mega'), (string) filemtime( STORESUIT_DIR . 'assets/counter.css' ) ); wp_enqueue_style( 'storesuit-order-desk', plugins_url( 'assets/order-desk.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/order-desk.css' ) ); wp_enqueue_style( 'storesuit-ledger-worklists', plugins_url( 'assets/ledger-worklists.css', STORESUIT_FILE ), array('storesuit-order-desk', 'storesuit-dashboard', 'storesuit-mega'), (string) filemtime( STORESUIT_DIR . 'assets/ledger-worklists.css' ) ); I18n::enqueue_script(); wp_enqueue_script( 'storesuit-charts', plugins_url( 'assets/charts.js', STORESUIT_FILE ), array('storesuit-i18n'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/charts.js' ), true ); if ( $page === 'storesuit' ) { wp_enqueue_script( 'storesuit-home', plugins_url( 'assets/home.js', STORESUIT_FILE ), array('storesuit-charts', 'storesuit-dashboard'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/home.js' ), true ); } wp_enqueue_script( 'storesuit-saved-views', plugins_url( 'assets/saved-views.js', STORESUIT_FILE ), array('storesuit-i18n'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/saved-views.js' ), true ); if ( $page === 'storesuit-reports' ) { wp_enqueue_script( 'storesuit-reports-home', plugins_url( 'assets/reports-home.js', STORESUIT_FILE ), array('storesuit-charts'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/reports-home.js' ), true ); wp_enqueue_style( 'storesuit-reports-home', plugins_url( 'assets/reports-home.css', STORESUIT_FILE ), array('storesuit-dashboard'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/reports-home.css' ) ); } if ( $page === 'storesuit-products' ) { wp_enqueue_script( 'storesuit-products-workspace', plugins_url( 'assets/products-workspace.js', STORESUIT_FILE ), array('storesuit-i18n'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/products-workspace.js' ), true ); wp_enqueue_style( 'storesuit-products-workspace', plugins_url( 'assets/products-workspace.css', STORESUIT_FILE ), array('storesuit'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/products-workspace.css' ) ); } // The product window's "Storefront page" tab: video, unit price, pre-order and the discount rule's warnings (ProductExtras). if ( $page === 'storesuit-products' ) { wp_enqueue_script( 'storesuit-product-extras', plugins_url( 'assets/product-extras.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/product-extras.js' ), true ); wp_enqueue_style( 'storesuit-product-extras', plugins_url( 'assets/product-extras.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/product-extras.css' ) ); } wp_enqueue_script( 'storesuit-workflows', plugins_url( 'assets/workflows.js', STORESUIT_FILE ), array_merge( array('storesuit-i18n', 'storesuit-saved-views'), ( $page === 'storesuit-products' ? array('storesuit-products-workspace') : array() ) ), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/workflows.js' ), true ); wp_enqueue_script( 'storesuit-merchant', plugins_url( 'assets/merchant.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/merchant.js' ), true ); if ( $page === 'storesuit-products' ) { wp_enqueue_media(); wp_enqueue_script( 'storesuit-product-add', plugins_url( 'assets/product-add.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/product-add.js' ), true ); wp_enqueue_style( 'storesuit-product-add', plugins_url( 'assets/product-add.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/product-add.css' ) ); } if ( in_array( $page, array('storesuit-orders', 'storesuit-operations'), true ) ) { wp_enqueue_script( 'storesuit-order-views', plugins_url( 'assets/order-views.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/order-views.js' ), true ); wp_enqueue_script( 'storesuit-order-desk', plugins_url( 'assets/order-desk.js', STORESUIT_FILE ), array('storesuit-i18n', 'storesuit-order-views'), (string) filemtime( STORESUIT_DIR . 'assets/order-desk.js' ), true ); } wp_enqueue_script( 'storesuit-counter', plugins_url( 'assets/counter.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/counter.js' ), true ); wp_enqueue_script( 'storesuit-dashboard', plugins_url( 'assets/dashboard.js', STORESUIT_FILE ), array('storesuit-counter'), STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/dashboard.js' ), true ); if ( $page === 'storesuit-stock-alerts' ) { wp_enqueue_script( 'storesuit-stock-alerts-admin', plugins_url( 'assets/stock-alerts-admin.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/stock-alerts-admin.js' ), true ); } if ( $page === 'storesuit-mail-log' ) { wp_enqueue_script( 'storesuit-mail-log', plugins_url( 'assets/mail-log.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/mail-log.js' ), true ); } if ( $page === 'storesuit-redirects' ) { wp_enqueue_script( 'storesuit-redirects', plugins_url( 'assets/redirects.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/redirects.js' ), true ); } if ( $page === 'storesuit' ) { wp_enqueue_script( 'storesuit-app-install', plugins_url( 'assets/app-install.js', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/app-install.js' ), true ); } // Sample data is offered to those who may manage the store only (never to a cashier); Store Staff only with the settings // permission, which its actions ask (Roles::staff_actions()). $demo = $page === 'storesuit-guide' && current_user_can( 'manage_woocommerce' ); if ( $demo ) { wp_enqueue_style( 'storesuit-demo-data', plugins_url( 'assets/demo-data.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/demo-data.css' ) ); wp_enqueue_script( 'storesuit-demo-data', plugins_url( 'assets/demo-data.js', STORESUIT_FILE ), array('storesuit-i18n', 'storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/demo-data.js' ), true ); } $deps = array_merge( array('storesuit-workflows', 'storesuit-dashboard', 'storesuit-merchant'), ( in_array( $page, array('storesuit-orders', 'storesuit-operations'), true ) ? array('storesuit-order-desk') : array() ), ( $page === 'storesuit-stock-alerts' ? array('storesuit-stock-alerts-admin') : (( $page === 'storesuit-mail-log' ? array('storesuit-mail-log') : array() )) ) ); if ( $page === 'storesuit-settings' ) { wp_enqueue_script( 'storesuit-email-setup', plugins_url( 'assets/email-setup.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/email-setup.js' ), true ); wp_enqueue_style( 'storesuit-email-setup', plugins_url( 'assets/email-setup.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/email-setup.css' ) ); $deps[] = 'storesuit-email-setup'; // Settings → License (LicensePanel): the plan, its dates and every license task in one tab. wp_enqueue_script( 'storesuit-license-panel', plugins_url( 'assets/license-panel.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/license-panel.js' ), true ); wp_enqueue_style( 'storesuit-license-panel', plugins_url( 'assets/license-panel.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/license-panel.css' ) ); $deps[] = 'storesuit-license-panel'; } // Marketing → SEO health (SeoHealth): the score and the top issues, read only. if ( $page === 'storesuit-marketing' && current_user_can( 'manage_woocommerce' ) ) { wp_enqueue_script( 'storesuit-seo-health', plugins_url( 'assets/seo-health.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/seo-health.js' ), true ); wp_enqueue_style( 'storesuit-seo-health', plugins_url( 'assets/seo-health.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/seo-health.css' ) ); $deps[] = 'storesuit-seo-health'; } // The product window's Content tab: a description and photo alt text with AI for one product (ProductAi). if ( $page === 'storesuit-products' ) { wp_enqueue_script( 'storesuit-product-ai', plugins_url( 'assets/product-ai.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/product-ai.js' ), true ); $deps[] = 'storesuit-product-ai'; } wp_enqueue_script( 'storesuit', plugins_url( 'assets/admin.js', STORESUIT_FILE ), $deps, STORESUIT_VERSION . '.' . filemtime( STORESUIT_DIR . 'assets/admin.js' ), true ); // Complete customer directory; explicit dependency prevents footer ordering races. if ( $page === 'storesuit-customers' ) { wp_enqueue_script( 'storesuit-customer-directory', plugins_url( 'assets/customer-directory.js', STORESUIT_FILE ), array('storesuit-i18n'), (string) filemtime( STORESUIT_DIR . 'assets/customer-directory.js' ), true ); wp_enqueue_style( 'storesuit-customer-directory', plugins_url( 'assets/customer-directory.css', STORESUIT_FILE ), array('storesuit'), (string) filemtime( STORESUIT_DIR . 'assets/customer-directory.css' ) ); wp_scripts()->registered['storesuit']->deps[] = 'storesuit-customer-directory'; } wp_localize_script( 'storesuit', 'StoreSuitNavigation', self::$navigation ); wp_localize_script( 'storesuit', 'StoreSuitJourneys', Navigation::journeys() ); $online_payment_ready = OrderMessages::online_payment_ready(); $countries = array_map( static fn( $name ) => html_entity_decode( $name, ENT_QUOTES, 'UTF-8' ), WC()->countries->get_countries() ); $config = array( 'url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'storesuit' ), 'page' => $page, 'currency' => get_woocommerce_currency(), 'price_decimals' => wc_get_price_decimals(), 'today' => current_time( 'Y-m-d' ), 'country' => WC()->countries->get_base_country(), 'countries' => $countries, 'carriers' => Shipping::public_carriers(), 'carrier_default' => Shipping::default_carrier(), 'pick_list_url' => admin_url( 'admin-post.php?action=storesuit_pick_list' ), 'pick_list_nonce' => wp_create_nonce( 'storesuit_pick_list' ), 'packing_slip_url' => admin_url( 'admin-post.php?action=storesuit_packing_slips' ), 'packing_slip_nonce' => wp_create_nonce( 'storesuit_packing_slips' ), 'online_payment_ready' => $online_payment_ready, 'customer_email_ready' => OrderMessages::email_ready(), 'payment_settings' => admin_url( 'admin.php?page=wc-settings&tab=checkout' ), 'customer_email_settings' => admin_url( 'admin.php?page=wc-settings&tab=email§ion=wc_email_customer_note' ), 'fulfil' => current_user_can( 'storesuit_fulfil' ), 'finance' => current_user_can( 'storesuit_finance' ), 'collect' => current_user_can( 'storesuit_finance' ) || current_user_can( Roles::COLLECT ), 'approve' => current_user_can( 'storesuit_approve' ), 'refund' => current_user_can( Roles::REFUND ), 'manage' => current_user_can( 'manage_woocommerce' ), 'weight_unit' => get_option( 'woocommerce_weight_unit', 'kg' ), 'dimension_unit' => get_option( 'woocommerce_dimension_unit', 'cm' ), ); // Home is drawn without the order queue while the order index is ready (admin.js load()). if ( $page === 'storesuit' ) { $config['index_ready'] = get_option( 'storesuit_order_index_ready' ) === '1'; } $config = self::kit_config( $config, $page ); wp_localize_script( 'storesuit', 'StoreSuitConfig', $config ); } /** * The shared kit's settings (persona fixes B01), after the others: the store's decimal mark for typed amounts (KIT-1), the * store's clock (KIT-3), the states of each country on the screens with an address form (KIT-2) and, on the settings page, * what the store's details hold (KIT-5). */ public static function kit_config( array $config, string $page ) : array { $config['price_decimal_sep'] = wc_get_price_decimal_separator(); $config['tz_offset'] = (int) round( wp_timezone()->getOffset( new \DateTimeImmutable('now') ) / 60 ); $config['hour'] = (int) current_time( 'G' ); $config['email_setup_manage'] = current_user_can( 'manage_options' ); if ( in_array( $page, array('storesuit-customers', 'storesuit-orders', 'storesuit-operations'), true ) ) { $config['states'] = self::states(); } if ( $page === 'storesuit-settings' && current_user_can( 'manage_woocommerce' ) ) { $config['store_details'] = self::store_details(); // The seller details the checkout's contracts are written from (CheckoutLegal): their own panel, apart from the // store details, which name no tax or invoice data (owner decision OD-11). $config['legal_seller'] = CheckoutLegal::seller_form(); } return $config; } /** WooCommerce's states of each country that has them, as [code, name] pairs in WooCommerce's order (JSON keeps it). */ public static function states() : array { $out = array(); foreach ( (array) WC()->countries->get_states() as $country => $states ) { if ( !is_array( $states ) || !$states ) { continue; } foreach ( $states as $code => $name ) { $out[$country][] = array((string) $code, html_entity_decode( (string) $name, ENT_QUOTES, 'UTF-8' )); } } return $out; } /** * The store's details as they are kept, for the settings page's read-only summary: the WooCommerce store address, the bank * transfer accounts (and whether bank transfer is offered), and in StoreRuler Pro the till receipt header and the shipping * label sender. Each comes with the screen that changes it; nothing here is saved by StoreRuler. */ public static function store_details() : array { $countries = WC()->countries; $country = $countries->get_base_country(); $state = $countries->get_base_state(); $names = $countries->get_countries(); $states = $countries->get_states( $country ); $place = trim( implode( ' ', array_filter( array($countries->get_base_postcode(), $countries->get_base_city()) ) ) ); $region = ( is_array( $states ) && isset( $states[$state] ) ? $states[$state] : $state ); $street = array_filter( array($countries->get_base_address(), $countries->get_base_address_2(), $place), static fn( $line ) => trim( (string) $line ) !== '' ); $address = ( $street ? array_merge( $street, array(implode( ', ', array_filter( array($region, $names[$country] ?? $country) ) )) ) : array() ); $bacs = get_option( 'woocommerce_bacs_settings', array() ); $accounts = array(); foreach ( (array) get_option( 'woocommerce_bacs_accounts', array() ) as $account ) { if ( !is_array( $account ) ) { continue; } $bank = trim( (string) ($account['bank_name'] ?? '') ); $number = trim( (string) (( ($account['iban'] ?? '') !== '' ? $account['iban'] : $account['account_number'] ?? '' )) ); if ( $bank !== '' || $number !== '' ) { $accounts[] = array( 'bank' => $bank, 'iban' => $number, ); } } $details = array( 'address' => array( 'lines' => array_values( array_map( static fn( $line ) => html_entity_decode( (string) $line, ENT_QUOTES, 'UTF-8' ), $address ) ), 'url' => admin_url( 'admin.php?page=wc-settings&tab=general' ), ), 'bank' => array( 'enabled' => is_array( $bacs ) && ($bacs['enabled'] ?? 'no') === 'yes', 'accounts' => $accounts, 'url' => admin_url( 'admin.php?page=wc-settings&tab=checkout§ion=bacs' ), ), ); return $details; } /** plugin_action_links_{basename}: "Open StoreRuler" on the Plugins screen, for those who may open it (P3-10). */ public static function action_links( $links ) { if ( !is_array( $links ) || !current_user_can( 'storesuit_orders' ) ) { return $links; } array_unshift( $links, '' . esc_html( I18n::t( 'Open StoreRuler' ) ) . '' ); return $links; } public static function render() : void { if ( !current_user_can( 'storesuit_orders' ) ) { return; } echo '
' . esc_html( I18n::t( 'Store data is loading…' ) ) . '