init_components();
$this->init_hooks();
}
/**
* Initialise admin components.
*/
private function init_components() {
new BoxCart_Admin_Menus();
new BoxCart_Admin_Products();
new BoxCart_Admin_Categories();
new BoxCart_Admin_Orders();
new BoxCart_Admin_Locations();
new BoxCart_Admin_Settings();
new BoxCart_Admin_Dashboard_Widget();
new BoxCart_Setup();
// Pro extensions (CSV import/export, etc.) instantiate their admin
// controllers from /includes/premium/bootstrap.php, which is only
// present in the Pro distribution. Free has no admin extensions.
}
/**
* Initialise hooks.
*/
private function init_hooks() {
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_notices', array( $this, 'payment_setup_notice' ) );
add_action( 'admin_notices', array( $this, 'customer_role_notice' ) );
add_action( 'set_user_role', array( $this, 'clear_customer_role_check' ) );
add_action( 'deleted_user', array( $this, 'clear_customer_role_check' ) );
}
/**
* Point out customer accounts that can do more than a customer should
* (they also hold a role such as Editor or Administrator). Shown on
* BoxCart's screens, the Users screen and the dashboard, to people who
* can change roles.
*/
public function customer_role_notice() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || ! current_user_can( 'promote_users' ) ) {
return;
}
if ( false === strpos( (string) $screen->id, 'boxcart' ) && ! in_array( $screen->id, array( 'users', 'dashboard' ), true ) ) {
return;
}
$count = count( BoxCart_Helpers::get_overprivileged_customer_ids() );
if ( ! $count ) {
return;
}
printf(
'
',
esc_html__( 'BoxCart:', 'boxcart' ),
esc_html(
sprintf(
/* translators: %d: number of customer accounts */
_n(
'%d customer account also has a role with more access than a customer needs, such as Editor or Administrator. Please check it and set its role to Customer.',
'%d customer accounts also have a role with more access than a customer needs, such as Editor or Administrator. Please check them and set their role to Customer.',
$count,
'boxcart'
),
$count
)
),
esc_url( admin_url( 'users.php?role=boxcart_customer' ) ),
esc_html__( 'Review customer accounts', 'boxcart' )
);
}
/**
* Re-check customer roles straight after someone changes a role or
* deletes a user, so the notice disappears as soon as it's fixed.
*/
public function clear_customer_role_check() {
delete_transient( 'boxcart_overprivileged_customers' );
}
/**
* Warn the shop owner, on BoxCart's screens, when the payment settings
* don't fit together (for example card payment on without Stripe keys).
*/
public function payment_setup_notice() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || false === strpos( (string) $screen->id, 'boxcart' ) || ! current_user_can( 'manage_options' ) ) {
return;
}
foreach ( BoxCart_Payment::get_setup_problems() as $problem ) {
printf( '', esc_html__( 'BoxCart payments:', 'boxcart' ), esc_html( $problem ) );
}
}
/**
* Get asset version string with file modification time for cache busting.
*
* @param string $relative_path Asset path relative to plugin directory.
* @return string Version string.
*/
private function asset_version( $relative_path ) {
$file = BOXCART_PLUGIN_DIR . $relative_path;
if ( file_exists( $file ) ) {
return BOXCART_VERSION . '.' . filemtime( $file );
}
return BOXCART_VERSION;
}
/**
* Enqueue admin assets.
*
* @param string $hook Current admin page hook.
*/
public function enqueue_assets( $hook ) {
// Only load on BoxCart admin pages.
if ( false === strpos( $hook, 'boxcart' ) ) {
return;
}
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
// Enqueue admin CSS on all BoxCart pages.
$admin_css_path = 'assets/css/admin/boxcart-admin' . $suffix . '.css';
wp_enqueue_style(
'boxcart-admin',
BOXCART_PLUGIN_URL . $admin_css_path,
array(),
$this->asset_version( $admin_css_path )
);
// Always enqueue common utilities on BoxCart admin pages.
$common_js_path = 'assets/js/admin/admin-common' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-common',
BOXCART_PLUGIN_URL . $common_js_path,
array(),
$this->asset_version( $common_js_path ),
true
);
// Localize script data for all admin scripts.
wp_localize_script(
'boxcart-admin-common',
'boxcartAdmin',
$this->get_script_data( $hook )
);
// Pro-Gate assets + localised "Upgrade" strings are owned by the
// BoxCart_Pro_Gate class itself. The full implementation ships only
// in the Pro distribution zip; the Free shim's enqueue_assets() is a
// no-op so this call is safe in both builds.
BoxCart_Pro_Gate::enqueue_assets( $suffix );
// Conditionally enqueue page-specific scripts.
$this->enqueue_page_scripts( $hook );
// Enqueue media library, sortable, and fonts for settings page.
if ( false !== strpos( $hook, 'boxcart-settings' ) ) {
wp_enqueue_media();
wp_enqueue_script( 'jquery-ui-sortable' );
// Load local Google Fonts so the typography preview can render them.
$fonts_path = 'assets/css/fonts/boxcart-fonts' . $suffix . '.css';
wp_enqueue_style(
'boxcart-fonts',
BOXCART_PLUGIN_URL . $fonts_path,
array(),
$this->asset_version( $fonts_path )
);
}
}
/**
* Enqueue page-specific JavaScript files.
*
* @param string $hook Current admin page hook.
*/
private function enqueue_page_scripts( $hook ) {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
// Products pages (list and edit).
if ( false !== strpos( $hook, 'boxcart-products' ) ) {
wp_enqueue_media();
$products_path = 'assets/js/admin/admin-products' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-products',
BOXCART_PLUGIN_URL . $products_path,
array( 'boxcart-admin-common' ),
$this->asset_version( $products_path ),
true
);
}
// Categories page.
if ( false !== strpos( $hook, 'boxcart-categories' ) ) {
$categories_path = 'assets/js/admin/admin-categories' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-categories',
BOXCART_PLUGIN_URL . $categories_path,
array( 'boxcart-admin-common' ),
$this->asset_version( $categories_path ),
true
);
}
// Locations pages (list, edit, and schedule).
if ( false !== strpos( $hook, 'boxcart-locations' ) ) {
$locations_path = 'assets/js/admin/admin-locations' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-locations',
BOXCART_PLUGIN_URL . $locations_path,
array( 'boxcart-admin-common' ),
$this->asset_version( $locations_path ),
true
);
}
// Import/Export page (import wizard).
if ( false !== strpos( $hook, 'boxcart-import-export' ) ) {
$import_path = 'assets/js/admin/admin-import-wizard' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-import-wizard',
BOXCART_PLUGIN_URL . $import_path,
array( 'boxcart-admin-common' ),
$this->asset_version( $import_path ),
true
);
}
// Settings page.
if ( false !== strpos( $hook, 'boxcart-settings' ) ) {
$settings_path = 'assets/js/admin/admin-settings' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-settings',
BOXCART_PLUGIN_URL . $settings_path,
array( 'boxcart-admin-common' ),
$this->asset_version( $settings_path ),
true
);
}
// Orders page (single order view).
if ( false !== strpos( $hook, 'boxcart-orders' ) ) {
$orders_path = 'assets/js/admin/admin-orders' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-orders',
BOXCART_PLUGIN_URL . $orders_path,
array( 'boxcart-admin-common' ),
$this->asset_version( $orders_path ),
true
);
}
// Setup page (Guided Setup wizard).
if ( false !== strpos( $hook, 'boxcart-setup' ) ) {
wp_enqueue_media();
$setup_path = 'assets/js/admin/admin-setup' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-setup',
BOXCART_PLUGIN_URL . $setup_path,
array( 'boxcart-admin-common' ),
$this->asset_version( $setup_path ),
true
);
}
// Reports page (Chart.js).
if ( false !== strpos( $hook, 'boxcart-reports' ) ) {
wp_enqueue_script(
'boxcart-chartjs',
BOXCART_PLUGIN_URL . 'assets/js/admin/vendor/chart.umd.min.js',
array(),
'4.4.1',
true
);
$reports_path = 'assets/js/admin/admin-reports' . $suffix . '.js';
wp_enqueue_script(
'boxcart-admin-reports',
BOXCART_PLUGIN_URL . $reports_path,
array( 'boxcart-chartjs' ),
$this->asset_version( $reports_path ),
true
);
}
}
/**
* Get localized script data.
*
* @param string $hook Current admin page hook.
* @return array Script data for wp_localize_script.
*/
private function get_script_data( $hook = '' ) {
$data = array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'boxcart_admin_nonce' ),
'pricesEnabled' => BoxCart_Helpers::prices_enabled(),
'currencySymbol' => BoxCart_Helpers::get_currency_symbol(),
'importBatchSize' => 25,
'i18n' => array(
// General.
'confirmDelete' => __( 'Are you sure you want to delete this item?', 'boxcart' ),
'saving' => __( 'Saving...', 'boxcart' ),
'saved' => __( 'Saved', 'boxcart' ),
'error' => __( 'An error occurred', 'boxcart' ),
'errorTryAgain' => __( 'An error occurred. Please try again.', 'boxcart' ),
'loading' => __( 'Loading...', 'boxcart' ),
'sending' => __( 'Sending...', 'boxcart' ),
'deleting' => __( 'Deleting...', 'boxcart' ),
'copied' => __( 'Copied!', 'boxcart' ),
// Products.
'selectImage' => __( 'Select Product Image', 'boxcart' ),
'useImage' => __( 'Use this image', 'boxcart' ),
'changeImage' => __( 'Change Image', 'boxcart' ),
'uploadImage' => __( 'Upload Image', 'boxcart' ),
'clickToUpload' => __( 'Click to upload', 'boxcart' ),
'add' => __( 'Add', 'boxcart' ),
'adding' => __( 'Adding...', 'boxcart' ),
'duplicating' => __( 'Duplicating...', 'boxcart' ),
'quantityTypeRequired' => __( 'Please add at least one quantity type to define how this product is sold and priced.', 'boxcart' ),
'confirmDeleteProducts' => __( 'Are you sure you want to delete the selected products?', 'boxcart' ),
'confirmSetInStock' => __( 'Set the selected products to In Stock?', 'boxcart' ),
'confirmSetOutOfStock' => __( 'Set the selected products to Out of Stock?', 'boxcart' ),
'confirmDeleteAllProducts' => __( 'Are you sure you want to delete ALL products? This cannot be undone.', 'boxcart' ),
'deleteAllProducts' => __( 'Delete All Products', 'boxcart' ),
// Categories.
'addCategory' => __( 'Add Category', 'boxcart' ),
'editCategory' => __( 'Edit Category', 'boxcart' ),
'saveCategory' => __( 'Save Category', 'boxcart' ),
'confirmDeleteCategory' => __( 'Are you sure you want to delete "{name}"?', 'boxcart' ),
'categoryHasProducts' => __( 'This category has {count} product(s). They will be unassigned from this category.', 'boxcart' ),
// Locations.
'confirmDeleteLocation' => __( 'Are you sure you want to delete "{name}"?', 'boxcart' ),
'addSlot' => __( 'Add Time Slot', 'boxcart' ),
'editSlot' => __( 'Edit Time Slot', 'boxcart' ),
'saveSlot' => __( 'Save Slot', 'boxcart' ),
'confirmDeleteSlot' => __( 'Are you sure you want to delete this time slot?', 'boxcart' ),
'addClosure' => __( 'Add Closure', 'boxcart' ),
'confirmDeleteClosure' => __( 'Are you sure you want to remove this closure?', 'boxcart' ),
'addOverride' => __( 'Add Override', 'boxcart' ),
'confirmDeleteOverride' => __( 'Are you sure you want to remove this capacity override?', 'boxcart' ),
'endDateBeforeStart' => __( 'End date must be after start date.', 'boxcart' ),
// Orders.
'confirmPayment' => __( 'Confirm that payment has been received for this order?', 'boxcart' ),
'confirming' => __( 'Confirming...', 'boxcart' ),
'confirmPaymentReceived' => __( 'Confirm Payment Received', 'boxcart' ),
/* translators: %s: amount */
'confirmRefund' => __( 'Refund %s to the customer\'s card? This can\'t be undone.', 'boxcart' ),
'refundAmountInvalid' => __( 'Enter an amount to refund.', 'boxcart' ),
/* translators: %s: day name, e.g. Monday */
'copyDaySlots' => __( 'Copy %s\'s time slots', 'boxcart' ),
'dialogOk' => __( 'OK', 'boxcart' ),
'closeAnyway' => __( 'Close anyway', 'boxcart' ),
'dialogCancel' => __( 'Cancel', 'boxcart' ),
'wizardSaving' => __( 'Saving...', 'boxcart' ),
'wizardContinue' => __( 'Continue', 'boxcart' ),
'wizardView' => __( 'View', 'boxcart' ),
'wizardChange' => __( 'Change', 'boxcart' ),
'wizardEdit' => __( 'Edit', 'boxcart' ),
'wizardNetwork' => __( 'We couldn\'t reach your site. Check your connection and try again.', 'boxcart' ),
/* translators: 1: location name, 2: number of time slots */
'wizardLocationDone' => __( '%1$s is set up, with %2$d time slots on each open day.', 'boxcart' ),
/* translators: %s: product name */
'wizardProductDone' => __( '%s is in your shop.', 'boxcart' ),
'wizardTimesWrong' => __( 'Each time slot needs to end after it starts.', 'boxcart' ),
'wizardNoDays' => __( 'Choose at least one day customers can collect.', 'boxcart' ),
'wizardNoSlots' => __( 'Add at least one time slot customers can choose.', 'boxcart' ),
/* translators: 1: number of time slots, 2: list of days, e.g. "Mon, Tue and Wed" */
'wizardSlots' => __( 'Customers will choose from %1$d time slots on %2$s.', 'boxcart' ),
/* translators: %s: list of days, e.g. "Mon, Tue and Wed" */
'wizardOneSlot' => __( 'Customers will choose from one time slot on %s.', 'boxcart' ),
/* translators: joins the last two days in a list, e.g. "Mon, Tue and Wed" */
'wizardAnd' => __( 'and', 'boxcart' ),
'wizardChoosePhoto' => __( 'Choose a photo', 'boxcart' ),
'wizardUsePhoto' => __( 'Use this photo', 'boxcart' ),
'wizardAlmostTitle' => __( 'Almost there', 'boxcart' ),
'wizardAlmostIntro' => __( 'A few steps are still to do. Your shop can\'t take orders until it has its pages, a collection point and a product.', 'boxcart' ),
'wizardReadyTitle' => __( 'Your shop is ready', 'boxcart' ),
'wizardReadyIntro' => __( 'Place a test order to see what your customers will see, and check the order emails arrive.', 'boxcart' ),
'chooseCopyDays' => __( 'Choose at least one day to copy to.', 'boxcart' ),
'confirmReplaceSlots' => __( 'The time slots already on those days will be deleted and replaced. Continue?', 'boxcart' ),
'copying' => __( 'Copying...', 'boxcart' ),
'copySlots' => __( 'Copy Slots', 'boxcart' ),
'refunding' => __( 'Refunding...', 'boxcart' ),
'refund' => __( 'Refund', 'boxcart' ),
'sendMessage' => __( 'Send Message', 'boxcart' ),
// Settings — media uploads.
'mediaLibraryNotAvailable' => __( 'Media library not available. Please refresh the page.', 'boxcart' ),
'selectEmailLogo' => __( 'Select Email Logo', 'boxcart' ),
'useAsLogo' => __( 'Use as Logo', 'boxcart' ),
'changeLogo' => __( 'Change Logo', 'boxcart' ),
'uploadLogo' => __( 'Upload Logo', 'boxcart' ),
'selectCustomIcon' => __( 'Select Custom Icon', 'boxcart' ),
'useAsIcon' => __( 'Use as Icon', 'boxcart' ),
'changeIcon' => __( 'Change Icon', 'boxcart' ),
'uploadIcon' => __( 'Upload Icon', 'boxcart' ),
// Settings — email templates.
'confirmResetTemplate' => __( 'Reset this template to default values?', 'boxcart' ),
'resetting' => __( 'Resetting...', 'boxcart' ),
'resetTemplateFailed' => __( 'Failed to reset template', 'boxcart' ),
'resetToDefault' => __( 'Reset to Default', 'boxcart' ),
'failedToLoadPreview' => __( 'Failed to load preview', 'boxcart' ),
'previewInNewTab' => __( 'Preview in New Tab', 'boxcart' ),
'pleaseEnterEmail' => __( 'Please enter an email address', 'boxcart' ),
'failedToSendTestEmail' => __( 'Failed to send test email', 'boxcart' ),
'sendTestEmail' => __( 'Send Test Email', 'boxcart' ),
// Settings — reset & import/export.
'saveFailed' => __( 'Save failed', 'boxcart' ),
/* translators: %s: settings tab name */
'resetTabToDefaults' => __( 'Reset %s to Defaults', 'boxcart' ),
'confirmResetTab' => __( 'Are you sure you want to reset', 'boxcart' ),
'confirmResetTabSuffix' => __( 'settings to their defaults? This cannot be undone.', 'boxcart' ),
'resetComplete' => __( 'Reset complete!', 'boxcart' ),
'resetFailed' => __( 'Reset failed.', 'boxcart' ),
'resetFailedTryAgain' => __( 'Reset failed. Please try again.', 'boxcart' ),
'confirmResetAll' => __( 'Are you sure you want to reset ALL settings to their defaults? This will affect every tab and cannot be undone.', 'boxcart' ),
'confirmResetAllFinal' => __( 'This is your last chance. All custom settings will be lost. Continue?', 'boxcart' ),
'resettingAll' => __( 'Resetting all...', 'boxcart' ),
'allSettingsReset' => __( 'All settings reset!', 'boxcart' ),
'failedToResetTab' => __( 'Failed to reset tab: ', 'boxcart' ),
'resetAllSettings' => __( 'Reset All Settings', 'boxcart' ),
'exporting' => __( 'Exporting...', 'boxcart' ),
'exportFailed' => __( 'Export failed.', 'boxcart' ),
'exportFailedTryAgain' => __( 'Export failed. Please try again.', 'boxcart' ),
'confirmImport' => __( 'Import settings from this file? This will overwrite your current settings (sensitive credentials such as payment provider API keys are preserved). Continue?', 'boxcart' ),
'invalidJsonFile' => __( 'Invalid JSON file. Please select a valid BoxCart settings export file.', 'boxcart' ),
'importing' => __( 'Importing...', 'boxcart' ),
'imported' => __( 'Imported!', 'boxcart' ),
'importFailed' => __( 'Import failed.', 'boxcart' ),
'importFailedTryAgain' => __( 'Import failed. Please try again.', 'boxcart' ),
// Import wizard.
'importWizard' => array(
'uploadAndContinue' => __( 'Upload & Continue', 'boxcart' ),
'next' => __( 'Next', 'boxcart' ),
'back' => __( 'Back', 'boxcart' ),
'startImport' => __( 'Start Import', 'boxcart' ),
'uploading' => __( 'Uploading...', 'boxcart' ),
'importing' => __( 'Importing...', 'boxcart' ),
'importComplete' => __( 'Import complete!', 'boxcart' ),
'selectFile' => __( 'Select a CSV file or drag and drop', 'boxcart' ),
'supportedFormats' => __( 'Supports BoxCart, WooCommerce, and Shopify exports (max 5 MB)', 'boxcart' ),
'fileSelected' => __( 'File selected:', 'boxcart' ),
'invalidFileType' => __( 'Please select a CSV file.', 'boxcart' ),
'fileTooLarge' => __( 'File is too large. Maximum size is 5 MB.', 'boxcart' ),
'skipColumn' => __( '— Skip (do not import) —', 'boxcart' ),
'nameRequired' => __( 'The "Name" field must be mapped to import.', 'boxcart' ),
'noColumnsMapped' => __( 'No columns mapped yet.', 'boxcart' ),
/* translators: 1: number of columns mapped, 2: number of rows to import */
'mappingSummary' => __( '%1$d columns mapped, %2$d rows to import', 'boxcart' ),
'detectedFormat' => __( 'Detected:', 'boxcart' ),
'genericFormat' => __( 'Unknown format', 'boxcart' ),
'uploadFailed' => __( 'Upload failed. Please try again.', 'boxcart' ),
'batchFailed' => __( 'Import batch failed. Please try again.', 'boxcart' ),
'errorsLabel' => __( 'Errors', 'boxcart' ),
'coreFields' => __( 'Core Fields', 'boxcart' ),
'qtyType' => __( 'Quantity Type', 'boxcart' ),
'fieldName' => __( 'Name', 'boxcart' ),
'fieldSku' => __( 'SKU', 'boxcart' ),
'fieldDescription' => __( 'Description', 'boxcart' ),
'fieldCategories' => __( 'Categories', 'boxcart' ),
'fieldImageUrl' => __( 'Image URL', 'boxcart' ),
'fieldPrice' => __( 'Price', 'boxcart' ),
'fieldStockStatus' => __( 'Stock Status', 'boxcart' ),
'fieldStockQuantity' => __( 'Stock Quantity', 'boxcart' ),
'fieldPublished' => __( 'Published', 'boxcart' ),
'fieldSortOrder' => __( 'Sort Order', 'boxcart' ),
'fieldSlug' => __( 'Slug', 'boxcart' ),
'fieldParent' => __( 'Parent', 'boxcart' ),
'fieldLabel' => __( 'Label', 'boxcart' ),
'fieldAllowDecimals' => __( 'Allow Decimals', 'boxcart' ),
'fieldStep' => __( 'Step', 'boxcart' ),
'fieldMin' => __( 'Min', 'boxcart' ),
'fieldMax' => __( 'Max', 'boxcart' ),
'undoImport' => __( 'Undo Import', 'boxcart' ),
'undoing' => __( 'Undoing...', 'boxcart' ),
/* translators: %d: number of products that will be deleted */
'undoConfirm' => __( 'Are you sure you want to undo this import? This will delete %d created product(s).', 'boxcart' ),
'undoExpired' => __( 'Undo data not available.', 'boxcart' ),
'undoFailed' => __( 'Undo failed. Please try again.', 'boxcart' ),
),
),
);
// Settings-page-specific data.
if ( false !== strpos( $hook, 'boxcart-settings' ) ) {
$settings = get_option( 'boxcart_settings', array() );
$current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$settings_handler = new BoxCart_Admin_Settings();
$tab_labels = array(
'general' => __( 'General', 'boxcart' ),
'collection' => __( 'Collection', 'boxcart' ),
'orders' => __( 'Orders', 'boxcart' ),
'payments' => __( 'Payments', 'boxcart' ),
'customer' => __( 'Accounts', 'boxcart' ),
'emails' => __( 'Emails', 'boxcart' ),
'styling' => __( 'Styling', 'boxcart' ),
'sidecart' => __( 'Sidecart', 'boxcart' ),
'shortcodes' => __( 'Shortcodes', 'boxcart' ),
'misc' => __( 'Misc', 'boxcart' ),
);
$data['currentTab'] = $current_tab;
$data['currentTabLabel'] = isset( $tab_labels[ $current_tab ] ) ? $tab_labels[ $current_tab ] : $current_tab;
$data['resettableTabs'] = array_keys( $settings_handler->get_resettable_tabs() );
$data['showResetButton'] = ! empty( $settings['show_reset_buttons'] )
&& ! in_array( $current_tab, array( 'shortcodes', 'misc' ), true );
}
// Products-page-specific data.
if ( false !== strpos( $hook, 'boxcart-products' ) ) {
$data['productsUrl'] = admin_url( 'admin.php?page=boxcart-products&deleted=1' );
}
return $data;
}
}