| @@ -19,17 +19,27 @@ | ||
| 19 | 19 | use FluentCart\App\Models\AttributeTerm; |
| 20 | 20 | use FluentCart\App\Models\AttributeGroup; |
| 21 | 21 | use FluentCart\App\Models\ShippingMethod; |
| 22 | 22 | use FluentCart\App\Modules\Tax\TaxModule; |
| 23 | +use FluentCart\App\Modules\FluentPlayer\FluentPlayerBridge; | |
| 24 | +use FluentCart\App\Modules\AdminFooter\AdminFooter; | |
| 23 | 25 | use FluentCart\App\Helpers\CurrenciesHelper; |
| 24 | 26 | use FluentCart\App\Services\Filter\TaxFilter; |
| 25 | 27 | use FluentCart\App\Services\Filter\OrderFilter; |
| 26 | 28 | use FluentCart\App\Services\Filter\LicenseFilter; |
| 29 | +use FluentCart\App\Services\Filter\LicenseSiteFilter; | |
| 27 | 30 | use FluentCart\App\Services\Filter\ProductFilter; |
| 28 | 31 | use FluentCart\App\Services\Filter\CustomerFilter; |
| 32 | +use FluentCart\App\Services\Filter\CouponFilter; | |
| 33 | +use FluentCart\App\Services\Filter\LogFilter; | |
| 34 | +use FluentCart\App\Services\Filter\OrderBumpFilter; | |
| 35 | +use FluentCart\App\Modules\Shipping\Services\Filter\ShippingClassFilter; | |
| 36 | +use FluentCart\App\Modules\Shipping\Services\Filter\ShippingZoneFilter; | |
| 29 | 37 | use FluentCart\App\Modules\Integrations\AddOnModule; |
| 38 | +use FluentCart\App\Services\DateTime\DayjsFormatter; | |
| 30 | 39 | use FluentCart\App\Services\Translations\TransStrings; |
| 31 | 40 | use FluentCart\App\Services\Permission\PermissionManager; |
| 41 | +use FluentCart\Database\DataBackfills; | |
| 32 | 42 | use FluentCart\App\Modules\PaymentMethods\Core\GatewayManager; |
| 33 | 43 | |
| 34 | 44 | class MenuHandler |
| 35 | 45 | { |
| @@ -42,8 +52,9 @@ | ||
| 42 | 52 | |
| 43 | 53 | if ($page == 'fluent-cart') { |
| 44 | 54 | |
| 45 | 55 | \FluentCart\App\Events\FirstTimePluginActivation::handle(); |
| 56 | + (new AdminFooter())->register(); | |
| 46 | 57 | add_action('admin_enqueue_scripts', function () { |
| 47 | 58 | Vite::enqueueStyle('fluent_cart_admin_app_css', |
| 48 | 59 | 'styles/tailwind/style.css', |
| 49 | 60 | ); |
| @@ -54,8 +65,14 @@ | ||
| 54 | 65 | |
| 55 | 66 | } |
| 56 | 67 | }); |
| 57 | 68 | |
| 69 | + // Inline dark mode init script for all FluentCart admin pages (prevents flicker) | |
| 70 | + add_action('admin_head', [$this, 'outputFluentCartDarkModeScript']); | |
| 71 | + | |
| 72 | + // Enqueue dark mode toggle JS for taxonomy pages | |
| 73 | + add_action('admin_head', [$this, 'enqueueDarkModeToggleForTaxonomy']); | |
| 74 | + | |
| 58 | 75 | // Add a post display state for special WC pages. |
| 59 | 76 | add_filter('display_post_states', array($this, 'addDisplayPostStates'), 10, 2); |
| 60 | 77 | |
| 61 | 78 | } |
| @@ -106,8 +123,66 @@ | ||
| 106 | 123 | return $states; |
| 107 | 124 | |
| 108 | 125 | } |
| 109 | 126 | |
| 127 | + public function outputFluentCartDarkModeScript() | |
| 128 | + { | |
| 129 | + global $pagenow; | |
| 130 | + $page = $_GET['page'] ?? ''; | |
| 131 | + $postType = $_GET['post_type'] ?? ''; | |
| 132 | + | |
| 133 | + // post.php?post=ID&action=edit omits post_type; resolve it from the post ID. | |
| 134 | + if ($pagenow === 'post.php' && empty($postType)) { | |
| 135 | + $postId = (int) App::request()->get('post', 0); | |
| 136 | + $postType = $postId ? (string) get_post_type($postId) : ''; | |
| 137 | + } | |
| 138 | + | |
| 139 | + $isFluentCartPage = $page === 'fluent-cart'; | |
| 140 | + $isTaxonomyList = $pagenow === 'edit-tags.php' && $postType === FluentProducts::CPT_NAME; | |
| 141 | + $isTermEditPage = $pagenow === 'term.php' && $postType === FluentProducts::CPT_NAME; | |
| 142 | + $isProductEditPage = ($pagenow === 'post.php' || $pagenow === 'edit.php') && $postType === FluentProducts::CPT_NAME; | |
| 143 | + | |
| 144 | + if (!$isFluentCartPage && !$isTaxonomyList && !$isTermEditPage && !$isProductEditPage) { | |
| 145 | + return; | |
| 146 | + } | |
| 147 | + | |
| 148 | + // Runs synchronously in <head> before any paint — applies the dark class and | |
| 149 | + // data-fct-theme attribute to <html> so logos and the theme icon render correctly | |
| 150 | + // from the very first frame, with no light-to-dark flash. | |
| 151 | + echo '<script>' . $this->getDarkModeInitScript() . '</script>'; | |
| 152 | + } | |
| 153 | + | |
| 154 | + public function enqueueDarkModeToggleForTaxonomy() | |
| 155 | + { | |
| 156 | + global $pagenow; | |
| 157 | + $postType = $_GET['post_type'] ?? ''; | |
| 158 | + | |
| 159 | + $isTaxonomyList = $pagenow === 'edit-tags.php' && $postType === FluentProducts::CPT_NAME; | |
| 160 | + $isTermEditPage = $pagenow === 'term.php' && $postType === FluentProducts::CPT_NAME; | |
| 161 | + | |
| 162 | + if ($isTaxonomyList || $isTermEditPage) { | |
| 163 | + Vite::enqueueScript( | |
| 164 | + 'fluent_cart_dark_mode_toggle', | |
| 165 | + 'admin/dark-mode-toggle.js', | |
| 166 | + [], | |
| 167 | + FLUENTCART_VERSION, | |
| 168 | + true | |
| 169 | + ); | |
| 170 | + } | |
| 171 | + } | |
| 172 | + | |
| 173 | + private function getDarkModeInitScript(): string | |
| 174 | + { | |
| 175 | + return "(function(){" . | |
| 176 | + "var k='fluent_theme_mode',c='fluent_theme_dark'," . | |
| 177 | + "s=localStorage.getItem(k)||localStorage.getItem('fcart_admin_theme')," . | |
| 178 | + "t=s==='dark'?'dark':s==='light'?'light':'system'," . | |
| 179 | + "d=s==='dark'||s==='system:dark'||((!s||s==='system')&&window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches);" . | |
| 180 | + "document.documentElement.setAttribute('data-fct-theme',t);" . | |
| 181 | + "if(d)document.documentElement.classList.add(c);" . | |
| 182 | + "})();"; | |
| 183 | + } | |
| 184 | + | |
| 110 | 185 | public function init() |
| 111 | 186 | { |
| 112 | 187 | |
| 113 | 188 | add_action('admin_menu', array($this, 'addAdminMenu')); |
| @@ -348,8 +423,11 @@ | ||
| 348 | 423 | } |
| 349 | 424 | |
| 350 | 425 | public function renderAdminMenu() |
| 351 | 426 | { |
| 427 | + if (!apply_filters('fluent_cart/show_admin_top_bar', true)) { | |
| 428 | + return; | |
| 429 | + } | |
| 352 | 430 | AdminHelper::getAdminMenu(true); |
| 353 | 431 | } |
| 354 | 432 | |
| 355 | 433 | public function pushProductNav($post) |
| @@ -370,13 +448,22 @@ | ||
| 370 | 448 | |
| 371 | 449 | do_action('fluent_cart/loading_app', $app); |
| 372 | 450 | |
| 373 | 451 | //This should be done before enqueueing the global script. |
| 374 | - Vite::enqueueScript($slug . '_admin_app_start', 'admin/bootstrap/app.js', [$slug . '_global_admin_hooks']); | |
| 452 | + // wp-i18n backs the admin translator (resources/admin/utils/translator/Translator.js). | |
| 453 | + Vite::enqueueScript($slug . '_admin_app_start', 'admin/bootstrap/app.js', [$slug . '_global_admin_hooks', 'wp-i18n']); | |
| 375 | 454 | |
| 376 | 455 | //Don't register this script using vite. |
| 377 | - wp_enqueue_script($slug . '_global_admin_hooks', Vite::getEnqueuePath('admin/admin_hooks.js'), [], FLUENTCART_VERSION, true); | |
| 456 | + if (!wp_script_is('wp-hooks', 'registered')) { | |
| 457 | + $suffix = (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? '' : '.min'; | |
| 458 | + wp_register_script('wp-hooks', includes_url('js/dist/hooks' . $suffix . '.js'), ['wp-polyfill'], FLUENTCART_VERSION, true); | |
| 459 | + } | |
| 460 | + if (!wp_script_is('wp-hooks', 'enqueued')) { | |
| 461 | + wp_enqueue_script('wp-hooks'); | |
| 462 | + } | |
| 378 | 463 | |
| 464 | + wp_enqueue_script($slug . '_global_admin_hooks', Vite::getEnqueuePath('admin/admin_hooks.js'), ['wp-hooks'], FLUENTCART_VERSION, true); | |
| 465 | + | |
| 379 | 466 | $manager = GatewayManager::getInstance(); |
| 380 | 467 | $payment_routes = $manager->getRoutes(); |
| 381 | 468 | // $payment_routes = apply_filters('fluent_cart/payments/payment_method_settings_routes', []); |
| 382 | 469 | |
| @@ -384,15 +471,17 @@ | ||
| 384 | 471 | |
| 385 | 472 | $settings = new StoreSettings(); |
| 386 | 473 | $checkoutUrl = add_query_arg(Helper::INSTANT_CHECKOUT_URL_PARAM, '=', $settings->getCheckoutPage()); |
| 387 | 474 | $restVars = Helper::getRestInfo(); |
| 475 | + $hasDataMigrations = current_user_can('manage_options') && DataBackfills::hasPending(); | |
| 388 | 476 | |
| 389 | 477 | $filterOptions = [ |
| 390 | 478 | 'order_filter_options' => OrderFilter::getTableFilterOptions(), |
| 391 | 479 | 'customer_filter_options' => CustomerFilter::getTableFilterOptions(), |
| 392 | 480 | 'product_filter_options' => ProductFilter::getTableFilterOptions(), |
| 393 | - 'license_filter_options' => LicenseFilter::getTableFilterOptions(), | |
| 394 | - 'tax_filter_options' => TaxFilter::getTableFilterOptions(), | |
| 481 | + 'license_filter_options' => LicenseFilter::getTableFilterOptions(), | |
| 482 | + 'license_site_filter_options' => LicenseSiteFilter::getTableFilterOptions(), | |
| 483 | + 'tax_filter_options' => TaxFilter::getTableFilterOptions(), | |
| 395 | 484 | ]; |
| 396 | 485 | $filterOptions = apply_filters('fluent_cart/admin_filter_options', $filterOptions, []); |
| 397 | 486 | |
| 398 | 487 | // Structured table config keyed by table name (used by Table.js base class) |
| @@ -400,13 +489,34 @@ | ||
| 400 | 489 | 'order_table' => ['filters' => Arr::get($filterOptions, 'order_filter_options', [])], |
| 401 | 490 | 'customers' => ['filters' => Arr::get($filterOptions, 'customer_filter_options', [])], |
| 402 | 491 | 'product_table' => ['filters' => Arr::get($filterOptions, 'product_filter_options', [])], |
| 403 | 492 | 'licenses' => ['filters' => Arr::get($filterOptions, 'license_filter_options', [])], |
| 493 | + 'license_sites' => ['filters' => Arr::get($filterOptions, 'license_site_filter_options', [])], | |
| 404 | 494 | 'taxes_table' => ['filters' => Arr::get($filterOptions, 'tax_filter_options', [])], |
| 405 | 495 | 'subscriptions' => ['filters' => Arr::get($filterOptions, 'subscription_filter_options', [])], |
| 406 | 496 | 'shipping_zone_table' => ['filters' => Arr::get($filterOptions, 'shipping_zone_filter_options', [])], |
| 497 | + // The Order Sources report filters orders, so its advanced-filter UI | |
| 498 | + // reuses the Orders filter vocabulary rather than defining its own. | |
| 499 | + 'source_report' => ['filters' => Arr::get($filterOptions, 'order_filter_options', [])], | |
| 407 | 500 | ]; |
| 408 | 501 | |
| 502 | + // Tables that declare sort options on their filter class but carry no | |
| 503 | + // filter-options entry above: the Sort popover still needs them, and | |
| 504 | + // they are what `fluent_cart/{filterName}_table_sorts` feeds. | |
| 505 | + $sortOnlyTables = [ | |
| 506 | + 'coupon_table' => CouponFilter::class, | |
| 507 | + 'log_table' => LogFilter::class, | |
| 508 | + 'order_bump_table' => OrderBumpFilter::class, | |
| 509 | + 'shipping_class_table' => ShippingClassFilter::class, | |
| 510 | + 'shipping_zone_table' => ShippingZoneFilter::class, | |
| 511 | + ]; | |
| 512 | + | |
| 513 | + foreach ($sortOnlyTables as $tableName => $filterClass) { | |
| 514 | + if (empty($tableConfig[$tableName]['filters']['sorts'])) { | |
| 515 | + $tableConfig[$tableName]['filters']['sorts'] = call_user_func([$filterClass, 'getSortOptions']); | |
| 516 | + } | |
| 517 | + } | |
| 518 | + | |
| 409 | 519 | $tableConfig = apply_filters('fluent_cart/admin_table_saved_views', $tableConfig, [ |
| 410 | 520 | 'filterOptions' => $filterOptions |
| 411 | 521 | ]); |
| 412 | 522 | |
| @@ -417,8 +527,9 @@ | ||
| 417 | 527 | $appConfig['version'] = FLUENTCART_VERSION; |
| 418 | 528 | $appConfig['permissions'] = PermissionManager::getUserPermissions(); |
| 419 | 529 | $appConfig['isProActive'] = App::isProActive(); |
| 420 | 530 | $appConfig['proVersion'] = defined('FLUENTCART_PRO_PLUGIN_VERSION') ? FLUENTCART_PRO_PLUGIN_VERSION : null; |
| 531 | + $appConfig['fluentPlayer'] = FluentPlayerBridge::adminAppConfig(); | |
| 421 | 532 | |
| 422 | 533 | $appConfig['logos'] = [ |
| 423 | 534 | 'dark' => Vite::getAssetUrl('images/logo/logo-full-dark.svg'), |
| 424 | 535 | 'light' => Vite::getAssetUrl('images/logo/logo-full.svg'), |
| @@ -423,9 +534,9 @@ | ||
| 423 | 534 | 'dark' => Vite::getAssetUrl('images/logo/logo-full-dark.svg'), |
| 424 | 535 | 'light' => Vite::getAssetUrl('images/logo/logo-full.svg'), |
| 425 | 536 | ]; |
| 426 | 537 | $appConfig['isModuleTabEnabled'] = $settings->isModuleTabEnabled(); |
| 427 | - $appConfig['upgrade_url'] = 'https://fluentcart.com/pricing/'; | |
| 538 | + $appConfig['upgrade_url'] = Helper::getUpgradeUrl(); | |
| 428 | 539 | |
| 429 | 540 | $max_upload_size = wp_max_upload_size(); // Returns size in bytes |
| 430 | 541 | $adminLocalizeData = apply_filters('fluent_cart/admin_app_data', [ |
| 431 | 542 | 'app_config' => $appConfig, |
| @@ -430,8 +541,9 @@ | ||
| 430 | 541 | $adminLocalizeData = apply_filters('fluent_cart/admin_app_data', [ |
| 431 | 542 | 'app_config' => $appConfig, |
| 432 | 543 | 'slug' => $app->config->get('app.slug'), |
| 433 | 544 | 'admin_url' => admin_url('admin.php?page=fluent-cart#/'), |
| 545 | + 'wp_admin_url' => rtrim(admin_url(), '/'), | |
| 434 | 546 | 'frontend_url' => URL::getFrontEndUrl(''), |
| 435 | 547 | 'nonce' => wp_create_nonce($slug), |
| 436 | 548 | 'rest' => $restVars, |
| 437 | 549 | 'brand_logo' => $this->getMenuIcon(), |
| @@ -456,9 +568,9 @@ | ||
| 456 | 568 | 'order_statues' => Status::getOrderStatuses(), |
| 457 | 569 | 'editable_order_statues' => Status::getEditableOrderStatuses(), |
| 458 | 570 | 'editable_customer_statues' => Status::getEditableCustomerStatuses(), |
| 459 | 571 | 'shipping_statuses' => Status::getShippingStatuses(), |
| 460 | - 'allow_bulk_payment_status_change' => true, | |
| 572 | + // Attribute library powering the Advanced Variation feature. | |
| 461 | 573 | 'variation_attributes' => AttributeGroup::with(['terms'])->get(), |
| 462 | 574 | 'variation_terms' => AttributeTerm::query()->get()->keyBy('id'), |
| 463 | 575 | 'product_image_base_uri' => Helper::getProductImageBaseUri(), |
| 464 | 576 | 'add_on_modules' => AddOnModule::showAddOns(), |
| @@ -483,14 +595,30 @@ | ||
| 483 | 595 | 'site_url' => site_url(), |
| 484 | 596 | 'modules_settings' => ModuleSettings::getAllSettings(), |
| 485 | 597 | 'purchase_fluent_cart_link' => 'https://fluentcart.com/', |
| 486 | 598 | 'admin_notices' => apply_filters('fluent_cart/admin_notices', []), |
| 599 | + // POSTs data-backfills/run until done; endpoint requires manage_options | |
| 600 | + 'has_data_migrations' => $hasDataMigrations, | |
| 487 | 601 | 'subscription_intervals' => Helper::getAvailableSubscriptionIntervalOptions(), |
| 488 | 602 | |
| 489 | - 'datei18' => TransStrings::dateTimeStrings(), | |
| 603 | + 'datei18' => DayjsFormatter::localizedStrings(), | |
| 490 | 604 | 'el_strings' => TransStrings::elStrings(), |
| 491 | 605 | 'wp_locale' => get_locale(), |
| 492 | 606 | 'is_full_name_required' => CheckoutFieldsSchema::isFullNameRequired(), |
| 607 | + 'business_details' => [ | |
| 608 | + 'company_name' => [ | |
| 609 | + 'enabled' => CheckoutFieldsSchema::isCompanyNameEnabled(), | |
| 610 | + 'required' => CheckoutFieldsSchema::isCompanyNameRequired(), | |
| 611 | + ], | |
| 612 | + 'vat_number' => [ | |
| 613 | + 'enabled' => CheckoutFieldsSchema::isVatNumberEnabled(), | |
| 614 | + 'required' => CheckoutFieldsSchema::isVatNumberRequired(), | |
| 615 | + ], | |
| 616 | + 'legal_registration_id' => [ | |
| 617 | + 'enabled' => CheckoutFieldsSchema::isLegalRegistrationIdEnabled(), | |
| 618 | + 'required' => CheckoutFieldsSchema::isLegalRegistrationIdRequired(), | |
| 619 | + ], | |
| 620 | + ], | |
| 493 | 621 | 'fct_editor_frame' => '', |
| 494 | 622 | ]); |
| 495 | 623 | |
| 496 | 624 | wp_localize_script($slug . '_admin_app_start', 'fluentCartAdminApp', $adminLocalizeData); |