__( 'Products', 'nota-store-analytics' ), 'render' => array( __CLASS__, 'render_missing_costs' ), 'position' => 10, ) ); } /** * The URL of one of the plugin's admin pages. * * @param string $slug Page slug. * @param array $args Extra query arguments. */ public static function page_url( string $slug, array $args = array() ): string { return add_query_arg( array_merge( array( 'page' => $slug ), $args ), admin_url( 'admin.php' ) ); } /** * Builds the menu. */ public static function menu(): void { /** * Fires when report screens should be registered with NOTASTAN_Reports::register(). */ do_action( 'notastan_register_reports' ); add_menu_page( __( 'Store Analytics', 'nota-store-analytics' ), __( 'Store Analytics', 'nota-store-analytics' ), self::CAPABILITY, 'notastan-dashboard', array( __CLASS__, 'render_dashboard' ), 'dashicons-chart-area', 56 ); add_submenu_page( 'notastan-dashboard', __( 'Dashboard', 'nota-store-analytics' ), __( 'Dashboard', 'nota-store-analytics' ), self::CAPABILITY, 'notastan-dashboard', array( __CLASS__, 'render_dashboard' ) ); foreach ( NOTASTAN_Reports::all() as $id => $report ) { add_submenu_page( 'notastan-dashboard', $report['title'], esc_html( $report['title'] ), self::CAPABILITY, 'notastan-report-' . $id, static function () use ( $id ) { NOTASTAN_Admin::render_report( $id ); } ); } add_submenu_page( 'notastan-dashboard', __( 'Store Analytics Settings', 'nota-store-analytics' ), __( 'Settings', 'nota-store-analytics' ), self::CAPABILITY, 'notastan-settings', array( 'NOTASTAN_Admin_Settings', 'render' ) ); // The product profit screen is reached by link only. hide_product_page() takes it out of the menu. add_submenu_page( 'notastan-dashboard', __( 'Product profit', 'nota-store-analytics' ), __( 'Product profit', 'nota-store-analytics' ), self::CAPABILITY, 'notastan-product', array( __CLASS__, 'render_product' ) ); } /** * Takes the product profit screen out of the visible menu. * * Runs on admin_head, not admin_menu: WordPress checks page access against the menu * structure after admin_menu, and refuses a page that is no longer in it. */ public static function hide_product_page(): void { remove_submenu_page( 'notastan-dashboard', 'notastan-product' ); } /** * The product id the current request asks for. */ private static function requested_product_id(): int { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing value. return isset( $_GET['product_id'] ) ? absint( wp_unslash( $_GET['product_id'] ) ) : 0; } /** * Renders the profit screen of one product. */ public static function render_product(): void { self::require_capability(); $product_id = self::requested_product_id(); $product = $product_id ? wc_get_product( $product_id ) : false; $product_name = NOTASTAN_REST::product_name( $product_id, $product ); $edit_url = $product ? (string) get_edit_post_link( $product_id, 'raw' ) : ''; $base_currency = NOTASTAN_Currency::base_currency(); include __DIR__ . '/views/product.php'; } /** * Stops users who may not see the plugin's screens. */ public static function require_capability(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You are not allowed to view this page.', 'nota-store-analytics' ) ); } } /** * Renders the dashboard. */ public static function render_dashboard(): void { self::require_capability(); $base_currency = NOTASTAN_Currency::base_currency(); $setup_url = NOTASTAN_Admin_Wizard::is_done() ? '' : self::page_url( 'notastan-setup' ); include __DIR__ . '/views/dashboard.php'; } /** * Renders the missing costs screen (the body of the Products report). */ public static function render_missing_costs(): void { self::require_capability(); include __DIR__ . '/views/missing-costs.php'; } /** * Renders a registered report inside the standard page wrapper. * * @param string $id Report id. */ public static function render_report( string $id ): void { self::require_capability(); $report = NOTASTAN_Reports::get( $id ); if ( ! $report ) { return; } echo '

' . esc_html( $report['title'] ) . '

'; if ( is_callable( $report['render'] ) ) { call_user_func( $report['render'] ); } echo '
'; } /** * The slug of the current admin page. */ private static function current_page(): string { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing value. return isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; } /** * The version string for an asset: the plugin version plus the file's modification time, * so a changed file is never served from the browser cache. * * @param string $file File name inside core/assets. */ public static function asset_version( string $file ): string { $path = NOTASTAN_DIR . 'core/assets/' . $file; return file_exists( $path ) ? NOTASTAN_VERSION . '.' . filemtime( $path ) : NOTASTAN_VERSION; } /** * Loads styles and scripts on the plugin's screens. */ public static function assets(): void { $page = self::current_page(); if ( 0 !== strpos( $page, 'notastan-' ) ) { return; } wp_enqueue_style( 'notastan-admin', plugins_url( 'core/assets/admin.css', NOTASTAN_FILE ), array(), self::asset_version( 'admin.css' ) ); $scripts = array( 'notastan-dashboard' => 'dashboard.js', 'notastan-product' => 'product.js', 'notastan-report-products' => 'missing-costs.js', ); /** * Filters which of the core's scripts a screen loads. An empty string loads none. * * @param string $script File name inside core/assets, or ''. * @param string $page Page slug. */ $script = (string) apply_filters( 'notastan_screen_script', $scripts[ $page ] ?? '', $page ); /** * Filters whether a screen without a core script still loads the shared helpers. * * @param bool $load Whether to load them. * @param string $page Page slug. */ if ( '' === $script && ! apply_filters( 'notastan_load_common_script', false, $page ) ) { return; } wp_enqueue_script( 'notastan-common', plugins_url( 'core/assets/common.js', NOTASTAN_FILE ), array( 'wp-api-fetch' ), self::asset_version( 'common.js' ), true ); wp_localize_script( 'notastan-common', 'notastanData', self::script_data() ); if ( '' !== $script ) { wp_enqueue_script( 'notastan-screen', plugins_url( 'core/assets/' . $script, NOTASTAN_FILE ), array( 'notastan-common' ), self::asset_version( $script ), true ); } /** * Fires after the shared script is enqueued on one of the plugin's screens. * * @param string $page Page slug. */ do_action( 'notastan_enqueue_scripts', $page ); } /** * Data and strings handed to the screen scripts. */ public static function script_data(): array { $data = array( 'namespace' => NOTASTAN_REST::REST_NAMESPACE, 'productId' => self::requested_product_id(), 'dashboardUrl' => self::page_url( 'notastan-dashboard' ), 'currency' => NOTASTAN_Currency::base_currency(), 'locale' => str_replace( '_', '-', get_user_locale() ), 'today' => wp_date( 'Y-m-d' ), 'firstDate' => NOTASTAN_Query::first_date(), 'missingCostsUrl' => self::page_url( 'notastan-report-products' ), 'ratesUrl' => self::page_url( 'notastan-settings', array( 'tab' => 'currencies' ) ), 'i18n' => array( 'netRevenue' => __( 'Net revenue', 'nota-store-analytics' ), 'netProfit' => __( 'Net profit', 'nota-store-analytics' ), 'profit' => __( 'Profit', 'nota-store-analytics' ), 'period' => __( 'Period', 'nota-store-analytics' ), 'dataTable' => __( 'Show these figures as a table', 'nota-store-analytics' ), 'noData' => __( 'No data for this period.', 'nota-store-analytics' ), 'allListed' => __( 'Every product is listed in the first table.', 'nota-store-analytics' ), 'loadError' => __( 'The report could not be loaded. Reload the page to try again.', 'nota-store-analytics' ), /* translators: %s: number of orders. */ 'missingRate' => __( 'Orders not counted because their exchange rate is unknown: %s.', 'nota-store-analytics' ), 'enterRates' => __( 'Enter exchange rates', 'nota-store-analytics' ), /* translators: %s: number of order lines. */ 'missingCost' => __( 'Order lines without a product cost: %s. Profit is overstated.', 'nota-store-analytics' ), /* translators: %s: percentage. */ 'coverage' => __( '%s of revenue rests on real costs.', 'nota-store-analytics' ), 'showMissing' => __( 'Show products without a cost', 'nota-store-analytics' ), /* translators: %s: number of orders. */ 'estimatedCost' => __( 'Orders using an estimated cost, because a product had no cost of its own when they were placed: %s.', 'nota-store-analytics' ), /* translators: 1: processed orders, 2: total orders. */ 'backfillRunning' => __( 'Order history is being processed: %1$s of %2$s orders.', 'nota-store-analytics' ), 'save' => __( 'Save', 'nota-store-analytics' ), 'saved' => __( 'Saved', 'nota-store-analytics' ), 'saveError' => __( 'Could not save. Enter the cost as a number.', 'nota-store-analytics' ), 'allDone' => __( 'Every product has a cost.', 'nota-store-analytics' ), 'costFor' => __( 'Cost', 'nota-store-analytics' ), 'previous' => __( 'Previous', 'nota-store-analytics' ), 'next' => __( 'Next', 'nota-store-analytics' ), /* translators: 1: current page, 2: number of pages. */ 'pageOf' => __( 'Page %1$s of %2$s', 'nota-store-analytics' ), 'variable' => __( 'Default for all variations', 'nota-store-analytics' ), /* translators: %s: product name. */ 'selectProduct' => __( 'Select %s', 'nota-store-analytics' ), /* translators: %s: number of selected products. */ 'applySelected' => __( 'Apply to %s selected', 'nota-store-analytics' ), 'applyNone' => __( 'Apply to selected', 'nota-store-analytics' ), /* translators: %s: number of products. */ 'bulkSaved' => __( 'Cost saved for %s products.', 'nota-store-analytics' ), 'noneInCategory' => __( 'No product in this category is missing a cost.', 'nota-store-analytics' ), /* translators: 1: number of products shown per list, 2: number of products with sales. */ 'showingProducts' => __( 'Showing the top and bottom %1$s of the %2$s products sold in this period.', 'nota-store-analytics' ), ), ); /** * Filters the data handed to the screen scripts. * * @param array $data Script data. */ return (array) apply_filters( 'notastan_script_data', $data ); } }