__( '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 );
}
}