Przeglądaj źródła

Merge branch '1.5.0.studiou-wc-product-cat-manage'

Dalibor Votruba 1 miesiąc temu
rodzic
commit
ef1a350ab9
20 zmienionych plików z 1451 dodań i 430 usunięć
  1. 6 0
      studiou-wc-product-cat-manage/.gitignore
  2. 34 6
      studiou-wc-product-cat-manage/CLAUDE.md
  3. 23 0
      studiou-wc-product-cat-manage/assets/css/admin.css
  4. 127 17
      studiou-wc-product-cat-manage/assets/js/admin.js
  5. 9 1
      studiou-wc-product-cat-manage/directory-structure.txt
  6. 484 0
      studiou-wc-product-cat-manage/implementation-plan-tabs.md
  7. 155 15
      studiou-wc-product-cat-manage/includes/class-studiou-wc-product-cat-manage-manipulator.php
  8. BIN
      studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage-cs_CZ.mo
  9. 17 1
      studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage-cs_CZ.po
  10. 16 0
      studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage.po
  11. 13 1
      studiou-wc-product-cat-manage/readme.md
  12. 55 5
      studiou-wc-product-cat-manage/studiou-wc-product-cat-manage.php
  13. 29 384
      studiou-wc-product-cat-manage/views/manipulations.php
  14. 74 0
      studiou-wc-product-cat-manage/views/manipulations/tab-batch-description.php
  15. 81 0
      studiou-wc-product-cat-manage/views/manipulations/tab-clear-media.php
  16. 77 0
      studiou-wc-product-cat-manage/views/manipulations/tab-delete-products.php
  17. 71 0
      studiou-wc-product-cat-manage/views/manipulations/tab-move.php
  18. 29 0
      studiou-wc-product-cat-manage/views/manipulations/tab-notes.php
  19. 48 0
      studiou-wc-product-cat-manage/views/manipulations/tab-remove-unassigned.php
  20. 103 0
      studiou-wc-product-cat-manage/views/manipulations/tab-upvp.php

+ 6 - 0
studiou-wc-product-cat-manage/.gitignore

@@ -0,0 +1,6 @@
+# Auto-generated, version-stamped asset copies used for cache busting on
+# hosts that strip the ?ver= query string (e.g. studiou.cz). These are
+# created at runtime from the source admin.js / admin.css and must not be
+# committed.
+/assets/js/admin-*.js
+/assets/css/admin-*.css

+ 34 - 6
studiou-wc-product-cat-manage/CLAUDE.md

@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 **QDR - Studiou WC Export/Import Product Categories** is a WordPress plugin that extends WooCommerce to provide batch export/import functionality for both product categories and products, along with category manipulation tools.
 
-**Current Version:** 1.5.3
+**Current Version:** 1.6.0
 
 ## Requirements
 
@@ -22,7 +22,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 - **studiou-wc-product-cat-manage.php** - Main plugin file containing:
   - Plugin initialization and dependency loading
   - Admin menu registration under Products menu
-  - Asset enqueueing for admin pages
+  - Asset enqueueing for admin pages — CSS/JS are served under version-stamped filenames (`admin-X.Y.Z.js` / `admin-X.Y.Z.css`, auto-generated from the source files) so cache busting works on hosts that strip the `?ver=` query string. Versioned copies are gitignored.
   - WooCommerce HPOS compatibility declaration
   - Text domain loading for translations
 
@@ -55,6 +55,8 @@ The plugin follows a modular class-based architecture:
   - Remove Not Assigned Media (deletes unused uncategorized media) (NEW in v1.5.2)
   - Update product variant prices (regular_price only, by category + attribute filter) (NEW in v1.5.3)
   - Uses direct database queries for reliable product detection
+  - `get_categories_with_counts()` / `get_media_categories_with_counts()` are transient-cached (5 min, busted on term changes) — uncached bodies live in the `*_uncached()` variants (NEW in v1.6.0)
+  - `handle_load_tab_ajax()` renders a manipulations tab partial on demand (NEW in v1.6.0)
   - Batch processing for delete operations (10 items per batch)
   - Auto-detects media category taxonomy (any custom taxonomy registered for attachments)
   - Robust media deletion with post-deletion verification and manual fallback cleanup
@@ -93,7 +95,9 @@ The plugin follows a modular class-based architecture:
 
 - **import.php** - Category import page UI
 - **export.php** - Category export page UI
-- **manipulations.php** - Category manipulations page UI with three operations:
+- **manipulations.php** - Category manipulations page **shell** (tab bar + empty content container). As of v1.6.0 it renders no heavy data; each tab is loaded on demand via the `studiou_wcpcm_load_tab` AJAX handler.
+- **manipulations/** - Tab partials, one per operation (NEW in v1.6.0): `tab-move.php`, `tab-batch-description.php`, `tab-delete-products.php`, `tab-clear-media.php`, `tab-remove-unassigned.php`, `tab-upvp.php`, `tab-notes.php`. Each partial fetches only its own data.
+  The page UI provides these operations:
   - Move products between categories (two dropdowns + "Do" button)
   - Batch description between categories (checkbox list + textarea + "Do" button)
   - Delete products in category (checkbox list + "Delete" button + progress bar)
@@ -201,6 +205,29 @@ STUDIOU_WCPCM_PLUGIN_URL     // Plugin URL
 STUDIOU_WCPCM_PLUGIN_BASENAME // Plugin basename
 ```
 
+### Release Process (version bump)
+
+On every release, bump the version in **all** of these places (keep them identical):
+
+1. `studiou-wc-product-cat-manage.php` — the `Version:` plugin header **and** the `STUDIOU_WCPCM_VERSION` constant
+2. `CLAUDE.md` — the **Current Version** line at the top
+3. `readme.md` — the `**Version: X.Y.Z**` line and a new `### Version X.Y.Z` changelog entry
+4. `assets/js/admin.js` — the `console.log('STUDIOU WC: Admin script loaded vX.Y.Z')` banner
+5. `languages/*.po` — the `Project-Id-Version` header (optional but preferred)
+
+#### Asset file renaming — automatic, do NOT do it by hand
+
+Admin CSS/JS are served under **version-stamped filenames** (`assets/js/admin-X.Y.Z.js`, `assets/css/admin-X.Y.Z.css`) so cache busting works on hosts that strip the `?ver=` query string (e.g. studiou.cz).
+
+- The renaming is handled at runtime by `get_cache_busted_asset_url()` in the main plugin file. It copies the source `admin.js` / `admin.css` to a versioned filename built from `STUDIOU_WCPCM_VERSION`, refreshes it whenever the source changes, and deletes stale copies.
+- **Always edit the source files** `assets/js/admin.js` and `assets/css/admin.css` — never the `admin-X.Y.Z.*` copies.
+- The `admin-X.Y.Z.*` copies are **generated build artifacts**: they are `.gitignore`d and must not be committed.
+- After bumping `STUDIOU_WCPCM_VERSION`, the new versioned filenames are produced automatically on the next admin page load — no manual file renaming is required.
+
+#### Translations
+
+After changing any translatable string, update `languages/studiou-wc-product-cat-manage.po` and `languages/studiou-wc-product-cat-manage-cs_CZ.po`, then recompile `languages/studiou-wc-product-cat-manage-cs_CZ.mo` with `msgfmt`.
+
 ### Admin Menu Structure
 
 All pages are added under **Products** menu:
@@ -343,11 +370,12 @@ The plugin registers the following AJAX actions:
 12. **studiou_wcpcm_upvp_get_attribute_values** - Lists values for the selected attribute, scoped to selected categories (NEW in v1.5.3)
 13. **studiou_wcpcm_upvp_count** - Counts and samples variations matching the current filter (NEW in v1.5.3)
 14. **studiou_wcpcm_upvp_apply** - Commits new regular price to matching variations via WC API (NEW in v1.5.3)
+15. **studiou_wcpcm_load_tab** - Renders a Category Manipulations tab partial on demand (NEW in v1.6.0)
 
 ### Product Operations (NEW in v1.4)
-15. **studiou_wcpcm_product_import_parse** - Parses CSV file and stores in transient (Step 1)
-16. **studiou_wcpcm_product_import_batch** - Processes batch of 5 products (Step 2, called recursively)
-17. **studiou_wcpcm_product_export** - Generates product CSV export by category selection
+16. **studiou_wcpcm_product_import_parse** - Parses CSV file and stores in transient (Step 1)
+17. **studiou_wcpcm_product_import_batch** - Processes batch of 5 products (Step 2, called recursively)
+18. **studiou_wcpcm_product_export** - Generates product CSV export by category selection
 
 All AJAX handlers include:
 - Nonce verification (`studiou-wcpcm-nonce`)

+ 23 - 0
studiou-wc-product-cat-manage/assets/css/admin.css

@@ -308,4 +308,27 @@
     padding: 6px 10px;
     border-bottom: 1px solid #eee;
     text-align: left;
+}
+
+/* Tabbed Category Manipulations page */
+.studiou-wcpcm-tabs {
+    margin-bottom: 0;
+}
+
+.studiou-wcpcm-tab-content {
+    margin-top: 20px;
+}
+
+.studiou-wcpcm-tab-loading,
+.studiou-wcpcm-tab-error {
+    padding: 40px;
+    text-align: center;
+    color: #666;
+    background: #fff;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+}
+
+.studiou-wcpcm-tab-error {
+    color: #a00;
 }

+ 127 - 17
studiou-wc-product-cat-manage/assets/js/admin.js

@@ -19,7 +19,7 @@
     // Initialize admin scripts
     $(document).ready(function() {
         // Debug: Log that script is loaded
-        console.log('STUDIOU WC: Admin script loaded v1.5.3');
+        console.log('STUDIOU WC: Admin script loaded v1.6.0');
 
         if (typeof studiouWcpcm === 'undefined') {
             console.error('STUDIOU WC: studiouWcpcm object is NOT defined - AJAX will not work');
@@ -30,16 +30,134 @@
 
         safeInit('Import form', '#studiou-wcpcm-import-form', initImportForm);
         safeInit('Export form', '#studiou-wcpcm-export-form', initExportForm);
-        safeInit('Move form', '#studiou-wcpcm-move-form', initMoveForm);
-        safeInit('Batch description form', '#studiou-wcpcm-batch-description-form', initBatchDescriptionForm);
-        safeInit('Delete products form', '#studiou-wcpcm-delete-products-form', initDeleteProductsForm);
-        safeInit('Clear media form', '#studiou-wcpcm-clear-media-form', initClearMediaForm);
-        safeInit('Remove unassigned media form', '#studiou-wcpcm-remove-unassigned-media-form', initRemoveUnassignedMediaForm);
         safeInit('Sample download', '#studiou-wcpcm-download-sample', initSampleDownload);
         safeInit('Product import form', '#studiou-wcpcm-product-import-form', initProductImportForm);
         safeInit('Product export form', '#studiou-wcpcm-product-export-form', initProductExportForm);
-        safeInit('Update variant prices form', '#studiou-wcpcm-upvp-form', initUpdateVariantPricesForm);
+
+        // Category Manipulations page: the manipulation forms live inside
+        // lazily-loaded tab partials, so they are initialised by the tab
+        // controller after their partial is injected (see initManipulationTabs).
+        safeInit('Manipulation tabs', '.studiou-wcpcm-tabs', initManipulationTabs);
     });
+
+    /**
+     * Initialize the tabbed Category Manipulations page.
+     *
+     * Each tab's markup is fetched on demand via the studiou_wcpcm_load_tab
+     * AJAX handler. The first time a tab is opened its partial is rendered into
+     * its own panel and its form handlers are wired; the panel is then kept in
+     * the DOM (hidden) so switching tabs preserves both state and event
+     * handlers - no re-fetch and no re-init.
+     */
+    function initManipulationTabs() {
+        // Maps a tab slug to the init function that wires its partial's handlers.
+        var TAB_INIT = {
+            'move':              initMoveForm,
+            'batch-description': initBatchDescriptionForm,
+            'delete-products':   initDeleteProductsForm,
+            'clear-media':       initClearMediaForm,
+            'remove-unassigned': initRemoveUnassignedMediaForm,
+            'upvp':              initUpdateVariantPricesForm,
+            'notes':             null // static, nothing to wire
+        };
+
+        var i18n     = (studiouWcpcm && studiouWcpcm.i18n) ? studiouWcpcm.i18n : {};
+        var $tabs    = $('.studiou-wcpcm-tabs .nav-tab');
+        var $content = $('#studiou-wcpcm-tab-content');
+        var loaded   = {}; // slug -> true once its panel exists in the DOM
+        var allowed  = $tabs.map(function() { return $(this).data('tab'); }).get();
+
+        function panelFor(slug) {
+            return $content.children('.studiou-wcpcm-tab-panel[data-tab="' + slug + '"]');
+        }
+
+        function activate(slug) {
+            if (allowed.indexOf(slug) === -1) {
+                slug = allowed[0];
+            }
+
+            $tabs.removeClass('nav-tab-active')
+                 .filter('[data-tab="' + slug + '"]').addClass('nav-tab-active');
+
+            if (window.history && history.replaceState) {
+                history.replaceState(null, '', '#' + slug);
+            } else {
+                window.location.hash = slug;
+            }
+
+            // Drop any stale loading / error placeholders, hide every panel.
+            $content.find('.studiou-wcpcm-tab-loading, .studiou-wcpcm-tab-error').remove();
+            $content.children('.studiou-wcpcm-tab-panel').hide();
+
+            // Already loaded - just reveal its panel (handlers + state intact).
+            if (loaded[slug]) {
+                panelFor(slug).show();
+                return;
+            }
+
+            $content.append('<div class="studiou-wcpcm-tab-loading">' +
+                (i18n.tabLoading || 'Loading...') + '</div>');
+
+            $.ajax({
+                url: studiouWcpcm.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcpcm_load_tab',
+                    nonce: studiouWcpcm.nonce,
+                    tab: slug
+                },
+                success: function(response) {
+                    $content.find('.studiou-wcpcm-tab-loading').remove();
+
+                    if (response.success && response.data && typeof response.data.html === 'string') {
+                        // Stale click guard: only reveal if this tab is still active.
+                        var stillActive = ($tabs.filter('.nav-tab-active').data('tab') === slug);
+
+                        var $panel = $('<div class="studiou-wcpcm-tab-panel"></div>')
+                            .attr('data-tab', slug)
+                            .html(response.data.html);
+                        $content.append($panel);
+                        loaded[slug] = true;
+
+                        var initFn = TAB_INIT[slug];
+                        if (typeof initFn === 'function') {
+                            try {
+                                initFn();
+                            } catch (e) {
+                                console.error('STUDIOU WC: tab init "' + slug + '" failed', e);
+                            }
+                        }
+
+                        if (stillActive) {
+                            $content.children('.studiou-wcpcm-tab-panel').hide();
+                            $panel.show();
+                        } else {
+                            $panel.hide();
+                        }
+                    } else {
+                        var msg = (response.data && response.data.message) ||
+                            (i18n.tabLoadError || 'Failed to load tab');
+                        $content.append('<div class="studiou-wcpcm-tab-error">' + msg + '</div>');
+                    }
+                },
+                error: function(xhr, status, error) {
+                    console.error('STUDIOU WC: load_tab AJAX error:', status, error);
+                    $content.find('.studiou-wcpcm-tab-loading').remove();
+                    $content.append('<div class="studiou-wcpcm-tab-error">' +
+                        (i18n.tabLoadError || 'Failed to load tab') + ': ' + error + '</div>');
+                }
+            });
+        }
+
+        $tabs.on('click', function(e) {
+            e.preventDefault();
+            activate($(this).data('tab'));
+        });
+
+        // Initial tab: a valid URL hash, otherwise the first tab.
+        var initial = (window.location.hash || '').replace(/^#/, '');
+        activate(allowed.indexOf(initial) !== -1 ? initial : allowed[0]);
+    }
     
     /**
      * Initialize import form
@@ -1338,20 +1456,12 @@
         $('#studiou-wcpcm-product-import-progress-bar').attr('data-progress', Math.round(percentage) + '%');
     }
 
-    // Additional debug: Check if elements exist after DOM load
+    // Additional debug: Check page state after DOM load
     $(window).on('load', function() {
         console.log('STUDIOU WC: Window loaded');
-        console.log('STUDIOU WC: Move form exists:', $('#studiou-wcpcm-move-form').length > 0);
-        console.log('STUDIOU WC: Move button exists:', $('#studiou-wcpcm-move-button').length > 0);
-        console.log('STUDIOU WC: Source select exists:', $('#source_category').length > 0);
-        console.log('STUDIOU WC: Target select exists:', $('#target_category').length > 0);
-        console.log('STUDIOU WC: Batch description form exists:', $('#studiou-wcpcm-batch-description-form').length > 0);
-        console.log('STUDIOU WC: Batch description button exists:', $('#studiou-wcpcm-batch-description-button').length > 0);
-        console.log('STUDIOU WC: Delete products form exists:', $('#studiou-wcpcm-delete-products-form').length > 0);
+        console.log('STUDIOU WC: Manipulation tabs exist:', $('.studiou-wcpcm-tabs').length > 0);
         console.log('STUDIOU WC: Product import form exists:', $('#studiou-wcpcm-product-import-form').length > 0);
         console.log('STUDIOU WC: Product export form exists:', $('#studiou-wcpcm-product-export-form').length > 0);
-        console.log('STUDIOU WC: Clear media form exists:', $('#studiou-wcpcm-clear-media-form').length > 0);
-        console.log('STUDIOU WC: UPVP form exists:', $('#studiou-wcpcm-upvp-form').length > 0);
     });
 
     /**

+ 9 - 1
studiou-wc-product-cat-manage/directory-structure.txt

@@ -16,6 +16,14 @@ studiou-wc-product-cat-manage/
 ├── views/
 │   ├── import.php
 │   ├── export.php
-│   └── manipulations.php
+│   ├── manipulations.php
+│   └── manipulations/
+│       ├── tab-move.php
+│       ├── tab-batch-description.php
+│       ├── tab-delete-products.php
+│       ├── tab-clear-media.php
+│       ├── tab-remove-unassigned.php
+│       ├── tab-upvp.php
+│       └── tab-notes.php
 ├── readme.md
 └── studiou-wc-product-cat-manage.php

+ 484 - 0
studiou-wc-product-cat-manage/implementation-plan-tabs.md

@@ -0,0 +1,484 @@
+# Implementation Plan — Tabbed Category Manipulations Page (lazy partial loading)
+
+**Target version:** 1.6.0 (bump from current 1.5.3)
+**Scope:** Restructure the **Products → Category Manipulations** admin page so its 6 operation sections + the *Important Notes* block become **tabs**, where each tab's HTML (and its expensive data) is fetched via a separate AJAX "partial" request only when the tab is first opened.
+**Goal:** Eliminate the page-load timeout. Today `views/manipulations.php` runs three expensive data calls *before any HTML is sent*, so one slow page request must finish all of them. After this change the shell page renders instantly and each tab pays only for its own calculation, in its own request.
+
+---
+
+## 1. Why the page times out today
+
+`views/manipulations.php` runs all of the following synchronously on every page load, before output:
+
+```php
+$categories             = $manipulator->get_categories_with_counts();        // N+1 queries
+$media_categories       = $manipulator->get_media_categories_with_counts();   // N+1 queries
+$unassigned_media_count = $manipulator->get_unassigned_unused_media_count();  // heavy multi-table scan
+```
+
+- `get_categories_with_counts()` — loads every `product_cat` term, then runs **one count query per category** (`get_category_product_count()`). On a store with hundreds of categories this is hundreds of queries.
+- `get_media_categories_with_counts()` — same N+1 shape for the media-category taxonomy.
+- `get_unassigned_unused_media_count()` — calls `get_unassigned_unused_media_ids()`, which scans the whole `posts`/`postmeta` tables plus every `_product_image_gallery` row.
+
+All three must complete inside a single PHP request before the page renders. On a large store that single request exceeds `max_execution_time`. The fix is to (a) render the page shell with **zero** heavy calls, and (b) move each section's data fetch into its own on-demand AJAX request.
+
+---
+
+## 2. Target behaviour
+
+The page **Products → Category Manipulations** becomes:
+
+- A title, the existing notice area, and a horizontal **tab bar** (WordPress core `.nav-tab-wrapper` markup).
+- An empty content container `#studiou-wcpcm-tab-content`.
+- On load, the controller activates a tab (from the URL hash, else the first tab) and fetches its partial via AJAX.
+- Clicking a tab fetches that tab's partial **once**, then caches the rendered HTML in the DOM. Re-clicking a cached tab just shows it — no new request.
+- The active tab is reflected in the URL hash (`#move`, `#clear-media`, …) so a browser refresh — and the `location.reload()` calls that several operations already perform on success — return to the same tab and re-fetch its (now up-to-date) partial.
+
+Seven tabs, in this order:
+
+| Slug | Tab label (reuses existing translated heading) | Heavy data it needs |
+| --- | --- | --- |
+| `move` | Move products between categories | `get_categories_with_counts()` |
+| `batch-description` | Batch description between categories | `get_categories_with_counts()` |
+| `delete-products` | Delete products in category | `get_categories_with_counts()` |
+| `clear-media` | Clear Media Categories | `get_media_categories_with_counts()` |
+| `remove-unassigned` | Remove Not Assigned Media | `get_unassigned_unused_media_count()` |
+| `upvp` | Update product variant prices | `get_categories_with_counts()` |
+| `notes` | Important Notes | none (static) |
+
+Each tab request computes only *its own* data. A user who only needs the *Move* tab never pays for the media scans.
+
+---
+
+## 3. Files to create / modify
+
+### 3.1 New files
+
+| File | Purpose |
+| --- | --- |
+| `views/manipulations/tab-move.php` | Partial — "Move products between categories" card body |
+| `views/manipulations/tab-batch-description.php` | Partial — "Batch description between categories" card body |
+| `views/manipulations/tab-delete-products.php` | Partial — "Delete products in category" card body |
+| `views/manipulations/tab-clear-media.php` | Partial — "Clear Media Categories" card body |
+| `views/manipulations/tab-remove-unassigned.php` | Partial — "Remove Not Assigned Media" card body |
+| `views/manipulations/tab-upvp.php` | Partial — "Update product variant prices" card body |
+| `views/manipulations/tab-notes.php` | Partial — "Important Notes" card body |
+
+### 3.2 Modified files
+
+| File | Change |
+| --- | --- |
+| `studiou-wc-product-cat-manage.php` | Bump `Version:` header and `STUDIOU_WCPCM_VERSION` to `1.6.0`. Add 3 new `i18n` keys (`tabLoading`, `tabLoadError`, see §7). |
+| `includes/class-studiou-wc-product-cat-manage-manipulator.php` | Add `handle_load_tab_ajax()` + register the `studiou_wcpcm_load_tab` action. Add cached wrappers `get_categories_with_counts()` / `get_media_categories_with_counts()` + cache-invalidation hooks (see §5). |
+| `views/manipulations.php` | Replace the whole body with the lightweight shell (tab bar + empty container). **No heavy data calls.** |
+| `assets/css/admin.css` | Add styles for the tab content container loading state (tab bar itself uses WP core styles). |
+| `assets/js/admin.js` | Add the tab controller; convert per-tab `init*Form()` functions to be invoked *after* the partial is injected, via a slug→init registry. |
+| `languages/studiou-wc-product-cat-manage.pot` | Add new translation strings. |
+| `languages/studiou-wc-product-cat-manage-cs_CZ.po` | Add Czech translations. |
+| `languages/studiou-wc-product-cat-manage-cs_CZ.mo` | Regenerate (`msgfmt`). |
+| `readme.md` | Bump version, note the tabbed UI under *Features*, add **Version 1.6.0** changelog entry. |
+| `CLAUDE.md` | Bump *Current Version* to 1.6.0, document the tab partials, the new AJAX endpoint, the `views/manipulations/` folder. |
+| `directory-structure.txt` | Add the new `views/manipulations/` folder and its 7 partial files. |
+
+No existing AJAX handler, no existing operation logic, and no DB schema changes.
+
+---
+
+## 4. Shell page — `views/manipulations.php`
+
+Replace the entire current contents with a shell that does **no** `$manipulator` calls:
+
+```php
+<?php
+// If this file is called directly, abort.
+if (!defined('WPINC')) { die; }
+
+$tabs = array(
+    'move'              => __('Move products between categories', 'studiou-wc-product-cat-manage'),
+    'batch-description' => __('Batch description between categories', 'studiou-wc-product-cat-manage'),
+    'delete-products'   => __('Delete products in category', 'studiou-wc-product-cat-manage'),
+    'clear-media'       => __('Clear Media Categories', 'studiou-wc-product-cat-manage'),
+    'remove-unassigned' => __('Remove Not Assigned Media', 'studiou-wc-product-cat-manage'),
+    'upvp'              => __('Update product variant prices', 'studiou-wc-product-cat-manage'),
+    'notes'             => __('Important Notes', 'studiou-wc-product-cat-manage'),
+);
+?>
+<div class="wrap studiou-wcpcm-wrap">
+    <h1><?php echo esc_html__('Category Manipulations', 'studiou-wc-product-cat-manage'); ?></h1>
+
+    <div class="studiou-wcpcm-notice-area"></div>
+
+    <h2 class="nav-tab-wrapper studiou-wcpcm-tabs">
+        <?php foreach ($tabs as $slug => $label): ?>
+            <a href="#<?php echo esc_attr($slug); ?>"
+               class="nav-tab"
+               data-tab="<?php echo esc_attr($slug); ?>">
+                <?php echo esc_html($label); ?>
+            </a>
+        <?php endforeach; ?>
+    </h2>
+
+    <div id="studiou-wcpcm-tab-content" class="studiou-wcpcm-tab-content">
+        <div class="studiou-wcpcm-tab-loading">
+            <?php echo esc_html__('Loading...', 'studiou-wc-product-cat-manage'); ?>
+        </div>
+    </div>
+</div>
+```
+
+`render_manipulations_page()` in the main plugin file is unchanged — it still just `include`s this file. The expensive `$manipulator` calls are deleted from here entirely.
+
+---
+
+## 5. Backend — `class-studiou-wc-product-cat-manage-manipulator.php`
+
+### 5.1 Register the new AJAX action
+
+In `__construct()`, alongside the existing `add_action('wp_ajax_studiou_wcpcm_*', ...)` lines:
+
+```php
+add_action('wp_ajax_studiou_wcpcm_load_tab', array($this, 'handle_load_tab_ajax'));
+```
+
+### 5.2 New handler `handle_load_tab_ajax()`
+
+Follows the established handler pattern in this file (`ob` cleaning, `error_reporting(0)` save/restore, nonce, capability):
+
+```php
+public function handle_load_tab_ajax() {
+    while (ob_get_level()) { ob_end_clean(); }
+
+    if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')) {
+        wp_send_json_error(array('message' => __('Security check failed', 'studiou-wc-product-cat-manage')));
+        return;
+    }
+    if (!current_user_can('manage_woocommerce')) {
+        wp_send_json_error(array('message' => __('You do not have sufficient permissions', 'studiou-wc-product-cat-manage')));
+        return;
+    }
+
+    $tab     = isset($_POST['tab']) ? sanitize_key($_POST['tab']) : '';
+    $allowed = array('move', 'batch-description', 'delete-products', 'clear-media', 'remove-unassigned', 'upvp', 'notes');
+
+    if (!in_array($tab, $allowed, true)) {
+        wp_send_json_error(array('message' => __('Unknown tab', 'studiou-wc-product-cat-manage')));
+        return;
+    }
+
+    $partial = STUDIOU_WCPCM_PLUGIN_DIR . 'views/manipulations/tab-' . $tab . '.php';
+    if (!file_exists($partial)) {
+        wp_send_json_error(array('message' => __('Tab content not found', 'studiou-wc-product-cat-manage')));
+        return;
+    }
+
+    // $manipulator is available to the partial; each partial fetches only the data it needs.
+    $manipulator = $this;
+
+    try {
+        ob_start();
+        include $partial;
+        $html = ob_get_clean();
+        wp_send_json_success(array('tab' => $tab, 'html' => $html));
+    } catch (Exception $e) {
+        if (ob_get_level()) { ob_end_clean(); }
+        error_log('STUDIOU WC TABS: tab "' . $tab . '" render failed: ' . $e->getMessage());
+        wp_send_json_error(array('message' => $e->getMessage()));
+    }
+}
+```
+
+The whitelist (`$allowed`) plus `sanitize_key()` plus the explicit `tab-<slug>.php` filename pattern means the `include` path can never be influenced into a directory traversal.
+
+### 5.3 Each partial is self-contained
+
+Each partial receives `$manipulator` (set to `$this` by the handler) and fetches only what it needs at the top, e.g. `views/manipulations/tab-move.php`:
+
+```php
+<?php
+if (!defined('WPINC')) { die; }
+/** @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator */
+$categories = $manipulator->get_categories_with_counts();
+?>
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Move products between categories', 'studiou-wc-product-cat-manage'); ?></h2>
+    ... existing "Move" card markup, copied verbatim from current manipulations.php lines 30-81 ...
+</div>
+```
+
+Mapping of which existing markup moves into which partial (line numbers refer to the current `views/manipulations.php`):
+
+- `tab-move.php` — current lines 29–82 (Move card). Needs `$categories`.
+- `tab-batch-description.php` — lines 85–141. Needs `$categories`.
+- `tab-delete-products.php` — lines 144–203. Needs `$categories`.
+- `tab-clear-media.php` — lines 206–269 (keeps the `empty($media_categories)` fallback branch). Needs `$media_categories = $manipulator->get_media_categories_with_counts();`.
+- `tab-remove-unassigned.php` — lines 272–302. Needs `$unassigned_media_count = $manipulator->get_unassigned_unused_media_count();`.
+- `tab-upvp.php` — lines 305–390. Needs `$categories`.
+- `tab-notes.php` — lines 393–406 (static `<ul>`; no data call).
+
+The `.studiou-wcpcm-card` wrapper stays inside each partial so the existing CSS keeps applying unchanged.
+
+### 5.4 Cache the N+1 category counts (addresses the "calculations" cost)
+
+Four of the seven tabs (`move`, `batch-description`, `delete-products`, `upvp`) each call `get_categories_with_counts()`. With lazy loading that is now four *separate* requests, each re-running the full N+1. To stop repeated recomputation, wrap the two N+1 methods in a transient cache.
+
+Rename the current bodies to private `*_uncached()` methods and add caching public wrappers:
+
+```php
+public function get_categories_with_counts() {
+    $key  = 'studiou_wcpcm_cat_counts';
+    $data = get_transient($key);
+    if (false === $data) {
+        $data = $this->get_categories_with_counts_uncached();
+        set_transient($key, $data, 5 * MINUTE_IN_SECONDS);
+    }
+    return $data;
+}
+
+public function get_media_categories_with_counts() {
+    $key  = 'studiou_wcpcm_media_cat_counts';
+    $data = get_transient($key);
+    if (false === $data) {
+        $data = $this->get_media_categories_with_counts_uncached();
+        set_transient($key, $data, 5 * MINUTE_IN_SECONDS);
+    }
+    return $data;
+}
+```
+
+Invalidate both transients whenever categories or product/category relationships change. Register in `__construct()`:
+
+```php
+$bust = function() {
+    delete_transient('studiou_wcpcm_cat_counts');
+    delete_transient('studiou_wcpcm_media_cat_counts');
+};
+add_action('created_term',          $bust);
+add_action('edited_term',           $bust);
+add_action('delete_term',           $bust);
+add_action('set_object_terms',      $bust);   // product <-> category assignment changes
+```
+
+Also call the buster directly at the end of `move_products`, `delete_products`/`delete_products_batch`, `clear_media`/`clear_media_batch` so the counts shown after an operation's `location.reload()` are fresh even within the 5-minute window.
+
+The 5-minute TTL is a safety net only; the explicit invalidation keeps the numbers correct. The `get_unassigned_unused_media_count()` scan is left uncached — it is only triggered by its own single tab and there is no cheap, reliable invalidation signal for "media used anywhere".
+
+> Note: §5.4 caching is the secondary optimisation. The primary timeout fix is §4 + §6 — splitting one giant request into seven small on-demand ones. Caching can be implemented in the same release or deferred without affecting the timeout fix.
+
+---
+
+## 6. Frontend — `assets/js/admin.js`
+
+### 6.1 The problem with the current init model
+
+Today every form is initialised in `$(document).ready()` via `safeInit(name, selector, initFn)`. With lazy tabs, the manipulation forms do **not** exist in the DOM at `ready` time — they arrive later inside an AJAX partial. So their `init*Form()` functions must run *after* the partial is injected, not on `ready`.
+
+### 6.2 Tab controller
+
+Add a controller that owns tab switching, lazy fetch, caching, and post-inject init. Register it on `ready` only when the tab bar is present:
+
+```js
+safeInit('Manipulation tabs', '.studiou-wcpcm-tabs', initManipulationTabs);
+```
+
+```js
+// Maps a tab slug to the init function that wires its partial's handlers.
+var TAB_INIT = {
+    'move':              initMoveForm,
+    'batch-description': initBatchDescriptionForm,
+    'delete-products':   initDeleteProductsForm,
+    'clear-media':       initClearMediaForm,
+    'remove-unassigned': initRemoveUnassignedMediaForm,
+    'upvp':              initUpdateVariantPricesForm,
+    'notes':             null   // static, nothing to wire
+};
+
+function initManipulationTabs() {
+    var $tabs    = $('.studiou-wcpcm-tabs .nav-tab');
+    var $content = $('#studiou-wcpcm-tab-content');
+    var cache    = {};   // slug -> rendered HTML, populated on first load
+    var allowed  = $tabs.map(function() { return $(this).data('tab'); }).get();
+
+    function activate(slug) {
+        if (allowed.indexOf(slug) === -1) { slug = allowed[0]; }
+
+        $tabs.removeClass('nav-tab-active')
+             .filter('[data-tab="' + slug + '"]').addClass('nav-tab-active');
+
+        if (window.history && history.replaceState) {
+            history.replaceState(null, '', '#' + slug);
+        } else {
+            window.location.hash = slug;
+        }
+
+        if (cache[slug] !== undefined) {
+            $content.html(cache[slug]);
+            return;   // already loaded + already initialised; just show it
+        }
+
+        $content.html('<div class="studiou-wcpcm-tab-loading">' +
+            (studiouWcpcm.i18n.tabLoading || 'Loading...') + '</div>');
+
+        $.ajax({
+            url: studiouWcpcm.ajaxUrl,
+            type: 'POST',
+            data: { action: 'studiou_wcpcm_load_tab', nonce: studiouWcpcm.nonce, tab: slug },
+            success: function(response) {
+                if (response.success && response.data && response.data.html) {
+                    cache[slug] = response.data.html;
+                    $content.html(response.data.html);
+                    var initFn = TAB_INIT[slug];
+                    if (typeof initFn === 'function') {
+                        try { initFn(); }
+                        catch (e) { console.error('STUDIOU WC: tab init "' + slug + '" failed', e); }
+                    }
+                } else {
+                    $content.html('<div class="studiou-wcpcm-tab-error">' +
+                        ((response.data && response.data.message) ||
+                         studiouWcpcm.i18n.tabLoadError || 'Failed to load tab') + '</div>');
+                }
+            },
+            error: function(xhr, status, error) {
+                $content.html('<div class="studiou-wcpcm-tab-error">' +
+                    (studiouWcpcm.i18n.tabLoadError || 'Failed to load tab') + ': ' + error + '</div>');
+            }
+        });
+    }
+
+    $tabs.on('click', function(e) {
+        e.preventDefault();
+        activate($(this).data('tab'));
+    });
+
+    // Initial tab: URL hash if valid, else first tab.
+    var initial = (window.location.hash || '').replace(/^#/, '');
+    activate(allowed.indexOf(initial) !== -1 ? initial : allowed[0]);
+}
+```
+
+### 6.3 Changes to the existing init functions
+
+- **Remove** these lines from the `$(document).ready()` block (their forms no longer exist at `ready`):
+  `safeInit('Move form', ...)`, `safeInit('Batch description form', ...)`, `safeInit('Delete products form', ...)`, `safeInit('Clear media form', ...)`, `safeInit('Remove unassigned media form', ...)`, `safeInit('Update variant prices form', ...)`.
+- **Keep** the `safeInit` calls for `Import form`, `Export form`, `Sample download`, `Product import form`, `Product export form` — those live on other admin pages and still load normally.
+- The six `init*Form()` function bodies themselves are **unchanged** — they already query by ID/selector and bind handlers; they just get invoked from `TAB_INIT` after injection instead of on `ready`.
+- Each tab partial is fetched at most once and cached, so each `init*Form()` runs exactly once → no double-bound handlers. (If a tab were ever re-fetched, re-binding would duplicate handlers; the cache guarantees it is not. A defensive `.off()` before `.on()` inside each init is an optional hardening step.)
+- `initUpdateVariantPricesForm()` already calls `loadAttributes()` at the end of its body — that AJAX now fires when the UPVP tab is opened, which is the desired lazy behaviour.
+- Operations that currently call `location.reload()` on success (move, delete, clear-media, remove-unassigned) keep doing so. After reload the shell re-reads the URL hash and re-fetches that tab's partial, which now shows refreshed counts. No JS change needed for that path beyond the hash being set by `activate()`.
+- The `$(window).on('load', ...)` debug block that logs `Move form exists`, `UPVP form exists`, etc. will now log `false` for tab forms at window-load time. Update it to drop the stale per-form checks (or leave it — it is debug-only and harmless). Recommended: trim it to just the tab-bar presence check.
+
+---
+
+## 7. Translations / i18n
+
+New strings to add to the `i18n` map in `wp_localize_script('studiou-wcpcm-admin', 'studiouWcpcm', ...)` in `studiou-wc-product-cat-manage.php`:
+
+| Key | English | Czech |
+| --- | --- | --- |
+| `tabLoading` | Loading... | Načítání... |
+| `tabLoadError` | Failed to load tab | Nepodařilo se načíst záložku |
+
+New translatable strings introduced in PHP (shell + handler), to add to `.pot` / `cs_CZ.po`:
+
+| English | Czech |
+| --- | --- |
+| Loading... | Načítání... |
+| Unknown tab | Neznámá záložka |
+| Tab content not found | Obsah záložky nenalezen |
+
+All seven tab labels reuse strings that are **already** translated (they are the current section `<h2>` headings), so no new label translations are required. Recompile `cs_CZ.mo` with `msgfmt` after editing the `.po`.
+
+---
+
+## 8. CSS — `assets/css/admin.css`
+
+The tab bar uses WordPress core classes (`.nav-tab-wrapper`, `.nav-tab`, `.nav-tab-active`) which are already styled by `wp-admin`. Only add styling for the content container states:
+
+```css
+.studiou-wcpcm-tab-content {
+    margin-top: 20px;
+}
+
+.studiou-wcpcm-tab-loading,
+.studiou-wcpcm-tab-error {
+    padding: 40px;
+    text-align: center;
+    color: #666;
+    background: #fff;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+}
+
+.studiou-wcpcm-tab-error {
+    color: #a00;
+}
+```
+
+The existing `.studiou-wcpcm-card` rules are reused untouched — each partial still wraps its body in a `.studiou-wcpcm-card`.
+
+---
+
+## 9. Documentation updates
+
+### 9.1 `CLAUDE.md`
+- Bump *Current Version* `1.5.3` → `1.6.0`.
+- Under *Views (views/)*, change the `manipulations.php` bullet to note it is now a lightweight shell, and add a `views/manipulations/` sub-folder bullet listing the 7 tab partials.
+- Under *AJAX Handlers → Category Operations*, add `studiou_wcpcm_load_tab` — renders a manipulations tab partial on demand (NEW in v1.6.0).
+- Under the manipulator class bullets, note the new transient-cached `get_categories_with_counts()` / `get_media_categories_with_counts()` wrappers.
+
+### 9.2 `readme.md`
+- Bump version to `1.6.0`.
+- Under *Features*, note the Category Manipulations page is now tabbed with lazy-loaded sections.
+- Add changelog entry:
+  ```
+  ### Version 1.6.0
+  - Category Manipulations page restructured into tabs
+    - Each tab's content + data is loaded on demand via a separate AJAX request
+    - Fixes page-load timeout on stores with many categories / media files
+    - Active tab is preserved in the URL hash across refreshes
+  - New AJAX handler: studiou_wcpcm_load_tab
+  - Category/media count queries are now transient-cached (5 min, invalidated on term changes)
+  ```
+
+### 9.3 `directory-structure.txt`
+- Add the `views/manipulations/` folder and its 7 `tab-*.php` files.
+
+---
+
+## 10. Step-by-step execution order
+
+1. Bump version constant + plugin header to `1.6.0`.
+2. Create the `views/manipulations/` folder and the 7 `tab-*.php` partials by moving each section's existing markup out of `views/manipulations.php` (see §5.3 line mapping). Add the `$manipulator` data-fetch line at the top of each non-static partial.
+3. Replace `views/manipulations.php` with the shell from §4.
+4. Add `handle_load_tab_ajax()` + register `studiou_wcpcm_load_tab` in the manipulator constructor.
+5. Add the transient-cached count wrappers + invalidation hooks (§5.4).
+6. Add `initManipulationTabs()` + the `TAB_INIT` registry to `admin.js`; remove the six obsolete `safeInit` calls from `$(document).ready()`; trim the window-load debug block.
+7. Extend `wp_localize_script` `i18n` with `tabLoading` / `tabLoadError`.
+8. Add the tab-content CSS rules.
+9. Update `.pot`, `cs_CZ.po`; regenerate `cs_CZ.mo`.
+10. Update `readme.md`, `CLAUDE.md`, `directory-structure.txt`.
+11. Manual smoke test — see §11.
+12. Hard-refresh in the browser (studiou.cz strips `?ver=` query strings, so the asset version bump alone will not bust the cache — instruct the user to hard-refresh).
+
+---
+
+## 11. Test plan
+
+1. **Shell loads instantly** — open Category Manipulations; the page renders immediately with the tab bar and a "Loading..." placeholder; no timeout even on a store with hundreds of categories.
+2. **First tab auto-loads** — with no URL hash, the *Move* tab fetches and renders; its category dropdowns populate.
+3. **Lazy loading** — open the *Clear Media Categories* tab; confirm the media-category scan runs only now (not on initial page load) — check via query monitor / debug log.
+4. **Tab caching** — switch away from a tab and back; no second AJAX request fires; the form state is preserved.
+5. **Each operation still works** — Move, Batch description, Delete products, Clear media, Remove unassigned media, UPVP — run each from inside its tab; handlers, spinners, progress bars, and confirmation dialogs behave as before.
+6. **Post-operation reload** — after a Move/Delete/Clear that calls `location.reload()`, the page returns to the same tab (via hash) and shows refreshed counts.
+7. **URL hash** — manually load `…&page=studiou-category-manipulations#upvp`; the UPVP tab opens directly. An invalid hash falls back to the first tab.
+8. **Count caching** — open *Move*, then *Delete products*, then *UPVP* in succession; the N+1 category-count query runs once, subsequent tabs read the transient. After editing a category, the next tab load recomputes.
+9. **Media fallback** — on a store with no media-category taxonomy, the *Clear Media Categories* tab still shows the graceful "install a media category plugin" message.
+10. **Security** — `studiou_wcpcm_load_tab` with a bad nonce → "Security check failed"; an unknown `tab` value → "Unknown tab"; a user without `manage_woocommerce` → permission error.
+11. **Path safety** — confirm a crafted `tab` param (e.g. `../../wp-config`) is rejected by the whitelist and never reaches `include`.
+12. **Translations** — switch the site to `cs_CZ`; tab labels and the "Loading..." / error strings render in Czech.
+13. **Other pages unaffected** — Import/Export Categories and Product Import/Export pages still work (their `safeInit` calls are untouched).
+
+---
+
+## 12. Rollback
+
+The change is structural but non-destructive — no DB schema changes, no data writes, all existing operation handlers and their logic are untouched. To roll back: restore the previous monolithic `views/manipulations.php`, delete the `views/manipulations/` folder, revert `admin.js` / `admin.css` / the manipulator class / the main plugin file to 1.5.3. The two new transients (`studiou_wcpcm_cat_counts`, `studiou_wcpcm_media_cat_counts`) are self-expiring and need no cleanup.

+ 155 - 15
studiou-wc-product-cat-manage/includes/class-studiou-wc-product-cat-manage-manipulator.php

@@ -45,6 +45,17 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
         add_action('wp_ajax_studiou_wcpcm_upvp_count', array($this, 'handle_upvp_count_ajax'));
         add_action('wp_ajax_studiou_wcpcm_upvp_apply', array($this, 'handle_upvp_apply_ajax'));
 
+        // Tabbed manipulations page - load a tab partial on demand
+        add_action('wp_ajax_studiou_wcpcm_load_tab', array($this, 'handle_load_tab_ajax'));
+
+        // Invalidate the cached category/media counts whenever categories or
+        // product <-> category relationships change.
+        add_action('created_term', array($this, 'bust_count_cache'));
+        add_action('edited_term', array($this, 'bust_count_cache'));
+        add_action('delete_term', array($this, 'bust_count_cache'));
+        add_action('set_object_terms', array($this, 'bust_count_cache'));
+        add_action('deleted_term_relationships', array($this, 'bust_count_cache'));
+
         // Add a test AJAX handler to verify AJAX is working
         add_action('wp_ajax_studiou_wcpcm_test', array($this, 'handle_test_ajax'));
 
@@ -53,7 +64,87 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
             error_log('STUDIOU WC: AJAX handlers registered - move_products, batch_description, delete_products, and clear_media');
         }
     }
-    
+
+    /**
+     * Invalidate the cached category / media-category count data.
+     *
+     * Hooked to term and term-relationship changes so the counts shown on the
+     * manipulations tabs stay accurate.
+     */
+    public function bust_count_cache() {
+        delete_transient('studiou_wcpcm_category_counts');
+        delete_transient('studiou_wcpcm_media_cat_counts');
+    }
+
+    /**
+     * Handle AJAX request to render a Category Manipulations tab partial.
+     *
+     * Each tab partial fetches only its own data, so the manipulations page
+     * shell can render instantly and heavy calculations run on demand.
+     */
+    public function handle_load_tab_ajax() {
+        // Clean any output buffers to prevent JSON corruption
+        while (ob_get_level()) {
+            ob_end_clean();
+        }
+
+        // Check nonce
+        if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')) {
+            wp_send_json_error(array(
+                'message' => __('Security check failed', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        // Check permissions
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(array(
+                'message' => __('You do not have sufficient permissions', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        $tab = isset($_POST['tab']) ? sanitize_key($_POST['tab']) : '';
+        $allowed = array('move', 'batch-description', 'delete-products', 'clear-media', 'remove-unassigned', 'upvp', 'notes');
+
+        if (!in_array($tab, $allowed, true)) {
+            wp_send_json_error(array(
+                'message' => __('Unknown tab', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        $partial = STUDIOU_WCPCM_PLUGIN_DIR . 'views/manipulations/tab-' . $tab . '.php';
+
+        if (!file_exists($partial)) {
+            wp_send_json_error(array(
+                'message' => __('Tab content not found', 'studiou-wc-product-cat-manage')
+            ));
+            return;
+        }
+
+        // Made available to the partial; each partial fetches only what it needs.
+        $manipulator = $this;
+
+        try {
+            ob_start();
+            include $partial;
+            $html = ob_get_clean();
+
+            wp_send_json_success(array(
+                'tab' => $tab,
+                'html' => $html
+            ));
+        } catch (Exception $e) {
+            if (ob_get_level()) {
+                ob_end_clean();
+            }
+            error_log('STUDIOU WC TABS: tab "' . $tab . '" render failed: ' . $e->getMessage());
+            wp_send_json_error(array('message' => $e->getMessage()));
+        }
+    }
+
+
     /**
      * Handle AJAX batch description request
      */
@@ -527,29 +618,60 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
     }
     
     /**
-     * Get all categories with product counts for select lists
+     * Get all categories with product counts for select lists.
+     *
+     * Transient-cached wrapper around get_categories_with_counts_uncached().
+     * The N+1 per-category count queries are expensive on large stores, so the
+     * result is cached for 5 minutes and busted on any category change.
      *
      * @return array Array of categories with ID, name, and product count
      */
     public function get_categories_with_counts() {
-        $args = array(
-            'taxonomy' => 'product_cat',
-            'orderby' => 'name',
-            'hide_empty' => false,
-        );
+        $data = get_transient('studiou_wcpcm_category_counts');
+
+        if (false === $data) {
+            $data = $this->get_categories_with_counts_uncached();
+            set_transient('studiou_wcpcm_category_counts', $data, 5 * MINUTE_IN_SECONDS);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Get all categories with product counts for select lists (uncached).
+     *
+     * Queries the product_cat terms directly via $wpdb instead of get_terms()
+     * on purpose: the WooCommerce Protected Categories plugin filters
+     * get_terms() and hides protected categories in admin-ajax / front-end
+     * contexts. Since v1.6.0 the manipulations tabs fetch this list inside an
+     * admin-ajax request, so get_terms() would drop protected categories from
+     * the dropdowns. Admins must be able to see and operate on every category,
+     * protected ones included, so we bypass the term-query filters entirely.
+     *
+     * @return array Array of categories with ID, name, and product count
+     */
+    public function get_categories_with_counts_uncached() {
+        global $wpdb;
+
+        $terms = $wpdb->get_results($wpdb->prepare("
+            SELECT t.term_id, t.name
+            FROM {$wpdb->terms} t
+            INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
+            WHERE tt.taxonomy = %s
+            ORDER BY t.name ASC
+        ", 'product_cat'));
 
-        $categories = get_terms($args);
         $categories_with_counts = array();
 
-        if (!is_wp_error($categories)) {
-            foreach ($categories as $category) {
-                $product_count = $this->get_category_product_count($category->term_id);
+        if (!empty($terms)) {
+            foreach ($terms as $term) {
+                $product_count = $this->get_category_product_count($term->term_id);
 
                 $categories_with_counts[] = array(
-                    'id' => $category->term_id,
-                    'name' => $category->name,
+                    'id' => (int) $term->term_id,
+                    'name' => $term->name,
                     'count' => $product_count,
-                    'display_name' => sprintf('%s (%d products)', $category->name, $product_count)
+                    'display_name' => sprintf('%s (%d products)', $term->name, $product_count)
                 );
             }
         }
@@ -864,11 +986,29 @@ class Studiou_WC_Product_Cat_Manage_Manipulator {
     }
 
     /**
-     * Get all media categories with file counts for select lists
+     * Get all media categories with file counts for select lists.
+     *
+     * Transient-cached wrapper around get_media_categories_with_counts_uncached().
      *
      * @return array Array of media categories with ID, name, and file count
      */
     public function get_media_categories_with_counts() {
+        $data = get_transient('studiou_wcpcm_media_cat_counts');
+
+        if (false === $data) {
+            $data = $this->get_media_categories_with_counts_uncached();
+            set_transient('studiou_wcpcm_media_cat_counts', $data, 5 * MINUTE_IN_SECONDS);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Get all media categories with file counts for select lists (uncached).
+     *
+     * @return array Array of media categories with ID, name, and file count
+     */
+    public function get_media_categories_with_counts_uncached() {
         $taxonomy = $this->get_media_category_taxonomy();
 
         if (!$taxonomy) {

BIN
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage-cs_CZ.mo


+ 17 - 1
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage-cs_CZ.po

@@ -1088,4 +1088,20 @@ msgstr "Chyba aktualizace cen variant"
 
 #: views/manipulations.php
 msgid "The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified."
-msgstr "Operace aktualizace cen variant mění pouze běžnou cenu variant produktů. Položky objednávek, obsah košíku ani historická data objednávek nejsou nikdy měněny."
+msgstr "Operace aktualizace cen variant mění pouze běžnou cenu variant produktů. Položky objednávek, obsah košíku ani historická data objednávek nejsou nikdy měněny."
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "Loading..."
+msgstr "Načítání..."
+
+#: studiou-wc-product-cat-manage.php
+msgid "Failed to load tab"
+msgstr "Nepodařilo se načíst záložku"
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Unknown tab"
+msgstr "Neznámá záložka"
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Tab content not found"
+msgstr "Obsah záložky nenalezen"

+ 16 - 0
studiou-wc-product-cat-manage/languages/studiou-wc-product-cat-manage.po

@@ -1054,4 +1054,20 @@ msgstr ""
 
 #: views/manipulations.php
 msgid "The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php views/manipulations.php
+msgid "Loading..."
+msgstr ""
+
+#: studiou-wc-product-cat-manage.php
+msgid "Failed to load tab"
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Unknown tab"
+msgstr ""
+
+#: includes/class-studiou-wc-product-cat-manage-manipulator.php
+msgid "Tab content not found"
 msgstr ""

+ 13 - 1
studiou-wc-product-cat-manage/readme.md

@@ -1,6 +1,6 @@
 # QDR - Studiou WC Export/Import Product Categories
 
-**Version: 1.5.3**
+**Version: 1.6.0**
 
 A WordPress plugin that extends WooCommerce to provide batch export and import functionality for both product categories and products, along with category manipulation tools.
 
@@ -28,6 +28,7 @@ The plugin provides a complete solution for bulk product and category management
 - **Clear Media Categories** - permanently delete all media files from selected media categories (NEW in v1.5)
 - **Remove Not Assigned Media** - permanently delete all unused uncategorized media files (NEW in v1.5.2)
 - **Update product variant prices** - bulk-update regular price of variations by category + attribute filter (NEW in v1.5.3)
+- **Tabbed Category Manipulations page** - each operation is a tab whose content loads on demand, preventing page-load timeouts on large stores (NEW in v1.6.0)
 
 ### Product Management (NEW in v1.4)
 - **Export products to CSV** - Export variable products and variations by category selection
@@ -351,6 +352,17 @@ For support, please visit [https://www.quadarax.com/plugins/studiou-wc-product-c
 
 ## Changelog
 
+### Version 1.6.0
+- **Category Manipulations page restructured into tabs**
+  - Each of the 6 operations plus Important Notes is now a separate tab
+  - A tab's HTML and its (expensive) data are fetched on demand via a separate AJAX request the first time the tab is opened
+  - Fixes the page-load timeout on stores with many product categories or media files - the shell page no longer runs every category/media calculation up front
+  - The active tab is preserved in the URL hash, so a refresh (and the automatic reload after move/delete/clear operations) returns to the same tab
+- New AJAX handler: `studiou_wcpcm_load_tab` renders a manipulations tab partial on demand
+- Category and media-category count queries are now transient-cached (5 minutes, automatically invalidated when categories or product/category relationships change)
+- 7 new view partials under `views/manipulations/`
+- **Cache busting** - admin CSS/JS are now served under version-stamped filenames (`admin-X.Y.Z.js` / `admin-X.Y.Z.css`), so a version bump forces browsers to fetch fresh assets even on hosts that strip the `?ver=` query string. No more manual hard refresh needed after an update.
+
 ### Version 1.5.3
 - **NEW: Update product variant prices** - bulk-update regular price of product variations
   - Filter by selected product categories (with Check All / Uncheck All)

+ 55 - 5
studiou-wc-product-cat-manage/studiou-wc-product-cat-manage.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Export/Import Product Categories
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-product-cat-manage
  * Description: Allows batch import and export WooCommerce product categories and products
- * Version: 1.5.3
+ * Version: 1.6.0
  * Requires at least: 6.8.1
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
@@ -23,7 +23,7 @@ if (!defined('WPINC')) {
 }
 
 // Define plugin constants
-if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.5.3');
+if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.6.0');
 if (!defined('STUDIOU_WCPCM_PLUGIN_DIR')) define('STUDIOU_WCPCM_PLUGIN_DIR', plugin_dir_path(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_URL')) define('STUDIOU_WCPCM_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_BASENAME')) define('STUDIOU_WCPCM_PLUGIN_BASENAME', plugin_basename(__FILE__));
@@ -242,14 +242,14 @@ class Studiou_WC_Product_Cat_Manage {
             
             wp_enqueue_style(
                 'studiou-wcpcm-admin',
-                STUDIOU_WCPCM_PLUGIN_URL . 'assets/css/admin.css',
+                $this->get_cache_busted_asset_url('assets/css/admin', 'css'),
                 array(),
                 STUDIOU_WCPCM_VERSION
             );
 
             wp_enqueue_script(
                 'studiou-wcpcm-admin',
-                STUDIOU_WCPCM_PLUGIN_URL . 'assets/js/admin.js',
+                $this->get_cache_busted_asset_url('assets/js/admin', 'js'),
                 array('jquery'),
                 STUDIOU_WCPCM_VERSION,
                 true
@@ -291,6 +291,8 @@ class Studiou_WC_Product_Cat_Manage {
                     'upvpApplyError' => __('Variant prices update error', 'studiou-wc-product-cat-manage'),
                     'upvpNoMatches' => __('No variations match the current filter.', 'studiou-wc-product-cat-manage'),
                     'upvpConfirmApply' => __('Are you sure you want to update %d variations?', 'studiou-wc-product-cat-manage'),
+                    'tabLoading' => __('Loading...', 'studiou-wc-product-cat-manage'),
+                    'tabLoadError' => __('Failed to load tab', 'studiou-wc-product-cat-manage'),
                 )
             ));
             
@@ -299,7 +301,55 @@ class Studiou_WC_Product_Cat_Manage {
             }
         }
     }
-    
+
+    /**
+     * Build a cache-busting URL for a plugin asset.
+     *
+     * studiou.cz (and similar host optimisers) strip the `?ver=` query string
+     * from static assets, so a version bump alone does not bust the browser
+     * cache. To work around that, this serves the asset under a filename that
+     * embeds the plugin version, e.g. assets/js/admin-1.6.0.js. The versioned
+     * copy is generated automatically from the source file and refreshed
+     * whenever the source changes, so no manual file renaming is needed.
+     *
+     * Falls back to the plain source URL if the copy cannot be written.
+     *
+     * @param string $rel_no_ext Asset path relative to the plugin dir, without
+     *                           extension, e.g. 'assets/js/admin'.
+     * @param string $ext        File extension without the dot, e.g. 'js'.
+     * @return string Absolute URL to the asset.
+     */
+    private function get_cache_busted_asset_url($rel_no_ext, $ext) {
+        $source_rel = $rel_no_ext . '.' . $ext;
+        $source_abs = STUDIOU_WCPCM_PLUGIN_DIR . $source_rel;
+
+        // Source missing - nothing we can do, return the plain URL.
+        if (!file_exists($source_abs)) {
+            return STUDIOU_WCPCM_PLUGIN_URL . $source_rel;
+        }
+
+        $versioned_rel = $rel_no_ext . '-' . STUDIOU_WCPCM_VERSION . '.' . $ext;
+        $versioned_abs = STUDIOU_WCPCM_PLUGIN_DIR . $versioned_rel;
+
+        // Regenerate the versioned copy if it is missing or out of date.
+        if (!file_exists($versioned_abs) || filemtime($source_abs) > filemtime($versioned_abs)) {
+            // Remove stale versioned copies of this asset.
+            $old_files = glob(STUDIOU_WCPCM_PLUGIN_DIR . $rel_no_ext . '-*.' . $ext);
+            if (is_array($old_files)) {
+                foreach ($old_files as $old_file) {
+                    @unlink($old_file);
+                }
+            }
+
+            // Copy failed (e.g. read-only filesystem) - fall back to plain URL.
+            if (!@copy($source_abs, $versioned_abs)) {
+                return STUDIOU_WCPCM_PLUGIN_URL . $source_rel;
+            }
+        }
+
+        return STUDIOU_WCPCM_PLUGIN_URL . $versioned_rel;
+    }
+
     /**
      * Render import page
      */

+ 29 - 384
studiou-wc-product-cat-manage/views/manipulations.php

@@ -1,6 +1,11 @@
 <?php
 /**
- * Category Manipulations page view
+ * Category Manipulations page view (shell)
+ *
+ * Renders only the tab bar and an empty content container. Each tab's
+ * markup and its (potentially expensive) data is fetched on demand via the
+ * studiou_wcpcm_load_tab AJAX handler. This keeps the page-load request
+ * lightweight and avoids PHP timeouts on large stores.
  *
  * @since      1.0.0
  * @package    Studiou_WC_Product_Cat_Manage
@@ -11,397 +16,37 @@ if (!defined('WPINC')) {
     die;
 }
 
-// Get categories with counts for select boxes
-$manipulator = new Studiou_WC_Product_Cat_Manage_Manipulator(new Studiou_WC_Product_Cat_Manage_DB());
-$categories = $manipulator->get_categories_with_counts();
-$media_categories = $manipulator->get_media_categories_with_counts();
-$unassigned_media_count = $manipulator->get_unassigned_unused_media_count();
+$tabs = array(
+    'move'              => __('Move products between categories', 'studiou-wc-product-cat-manage'),
+    'batch-description' => __('Batch description between categories', 'studiou-wc-product-cat-manage'),
+    'delete-products'   => __('Delete products in category', 'studiou-wc-product-cat-manage'),
+    'clear-media'       => __('Clear Media Categories', 'studiou-wc-product-cat-manage'),
+    'remove-unassigned' => __('Remove Not Assigned Media', 'studiou-wc-product-cat-manage'),
+    'upvp'              => __('Update product variant prices', 'studiou-wc-product-cat-manage'),
+    'notes'             => __('Important Notes', 'studiou-wc-product-cat-manage'),
+);
 ?>
 
 <div class="wrap studiou-wcpcm-wrap">
     <h1><?php echo esc_html__('Category Manipulations', 'studiou-wc-product-cat-manage'); ?></h1>
-    
+
     <div class="studiou-wcpcm-notice-area">
         <!-- Area for displaying notices -->
     </div>
-    
-    <!-- Move Products Between Categories Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Move products between categories', 'studiou-wc-product-cat-manage'); ?></h2>
-        
-        <p><?php echo esc_html__('This operation allows you to move all products from one category to another. Products will be removed from the source category and added to the target category.', 'studiou-wc-product-cat-manage'); ?></p>
-        
-        <form id="studiou-wcpcm-move-form" method="post">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="source_category"><?php echo esc_html__('Source Category', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select name="source_category" id="source_category" class="regular-text" required>
-                            <option value=""><?php echo esc_html__('-- Select Source Category --', 'studiou-wc-product-cat-manage'); ?></option>
-                            <?php foreach ($categories as $category): ?>
-                                <option value="<?php echo esc_attr($category['id']); ?>">
-                                    <?php echo esc_html($category['display_name']); ?>
-                                </option>
-                            <?php endforeach; ?>
-                        </select>
-                        <p class="description"><?php echo esc_html__('Select the category from which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="target_category"><?php echo esc_html__('Target Category', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select name="target_category" id="target_category" class="regular-text" required>
-                            <option value=""><?php echo esc_html__('-- Select Target Category --', 'studiou-wc-product-cat-manage'); ?></option>
-                            <?php foreach ($categories as $category): ?>
-                                <option value="<?php echo esc_attr($category['id']); ?>">
-                                    <?php echo esc_html($category['display_name']); ?>
-                                </option>
-                            <?php endforeach; ?>
-                        </select>
-                        <p class="description"><?php echo esc_html__('Select the category to which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-            </table>
-            
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary" id="studiou-wcpcm-move-button">
-                    <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-        
-        <div class="studiou-wcpcm-move-results" style="display: none;">
-            <h3><?php echo esc_html__('Move Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-move-message"></div>
-        </div>
-    </div>
-    
-    <!-- Batch Description Between Categories Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Batch description between categories', 'studiou-wc-product-cat-manage'); ?></h2>
-        
-        <p><?php echo esc_html__('This operation allows you to set the same description for multiple categories at once.', 'studiou-wc-product-cat-manage'); ?></p>
-        
-        <form id="studiou-wcpcm-batch-description-form" method="post">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <div class="studiou-wcpcm-categories-list-container">
-                            <div class="studiou-wcpcm-list-actions">
-                                <button type="button" id="studiou-wcpcm-check-all" class="button">
-                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                                <button type="button" id="studiou-wcpcm-uncheck-all" class="button">
-                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                            </div>
-                            <div class="studiou-wcpcm-categories-list" id="categories_list">
-                                <?php foreach ($categories as $category): ?>
-                                    <label class="studiou-wcpcm-category-item">
-                                        <input type="checkbox" name="selected_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                        <span><?php echo esc_html($category['display_name']); ?></span>
-                                    </label>
-                                <?php endforeach; ?>
-                            </div>
-                        </div>
-                        <p class="description"><?php echo esc_html__('Select the categories for which you want to set the description.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="category_description"><?php echo esc_html__('Product categories description', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <textarea name="category_description" id="category_description" rows="5" cols="50" class="large-text"></textarea>
-                        <p class="description"><?php echo esc_html__('Enter the description that will be applied to all selected categories.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-            </table>
-            
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary" id="studiou-wcpcm-batch-description-button">
-                    <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-        
-        <div class="studiou-wcpcm-batch-description-results" style="display: none;">
-            <h3><?php echo esc_html__('Batch Description Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-batch-description-message"></div>
-        </div>
-    </div>
-    
-    <!-- Delete Products in Category Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Delete products in category', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('This operation allows you to permanently delete all products and their variants from selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <form id="studiou-wcpcm-delete-products-form" method="post">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="delete_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <div class="studiou-wcpcm-categories-list-container">
-                            <div class="studiou-wcpcm-list-actions">
-                                <button type="button" id="studiou-wcpcm-delete-check-all" class="button">
-                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                                <button type="button" id="studiou-wcpcm-delete-uncheck-all" class="button">
-                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                            </div>
-                            <div class="studiou-wcpcm-categories-list" id="delete_categories_list">
-                                <?php foreach ($categories as $category): ?>
-                                    <label class="studiou-wcpcm-category-item">
-                                        <input type="checkbox" name="delete_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                        <span><?php echo esc_html($category['display_name']); ?></span>
-                                    </label>
-                                <?php endforeach; ?>
-                            </div>
-                        </div>
-                        <p class="description"><?php echo esc_html__('Select the categories from which all products will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
-                    </td>
-                </tr>
-            </table>
 
-            <div class="studiou-wcpcm-warning-message">
-                <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all products (including variants) in the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
-            </div>
+    <h2 class="nav-tab-wrapper studiou-wcpcm-tabs">
+        <?php foreach ($tabs as $slug => $label): ?>
+            <a href="#<?php echo esc_attr($slug); ?>"
+               class="nav-tab"
+               data-tab="<?php echo esc_attr($slug); ?>">
+                <?php echo esc_html($label); ?>
+            </a>
+        <?php endforeach; ?>
+    </h2>
 
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-delete-products-button">
-                    <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-
-        <div class="studiou-wcpcm-delete-progress" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div class="studiou-wcpcm-progress-bar-container">
-                <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-delete-progress-bar"></div>
-            </div>
-            <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-delete-progress-text"></div>
+    <div id="studiou-wcpcm-tab-content" class="studiou-wcpcm-tab-content">
+        <div class="studiou-wcpcm-tab-loading">
+            <?php echo esc_html__('Loading...', 'studiou-wc-product-cat-manage'); ?>
         </div>
-
-        <div class="studiou-wcpcm-delete-results" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-delete-message"></div>
-        </div>
-    </div>
-
-    <!-- Clear Media Categories Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Clear Media Categories', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('This operation allows you to permanently delete all media files from selected media categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <?php if (empty($media_categories)): ?>
-            <p><em><?php echo esc_html__('No media categories found. Please install a media category plugin (e.g., Media Library Categories) to use this feature.', 'studiou-wc-product-cat-manage'); ?></em></p>
-        <?php else: ?>
-            <form id="studiou-wcpcm-clear-media-form" method="post" onsubmit="return false;">
-                <table class="form-table">
-                    <tr>
-                        <th scope="row">
-                            <label for="clear_media_categories_list"><?php echo esc_html__('Select Media Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                        </th>
-                        <td>
-                            <div class="studiou-wcpcm-categories-list-container">
-                                <div class="studiou-wcpcm-list-actions">
-                                    <button type="button" id="studiou-wcpcm-clear-media-check-all" class="button">
-                                        <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                    </button>
-                                    <button type="button" id="studiou-wcpcm-clear-media-uncheck-all" class="button">
-                                        <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                    </button>
-                                </div>
-                                <div class="studiou-wcpcm-categories-list" id="clear_media_categories_list">
-                                    <?php foreach ($media_categories as $category): ?>
-                                        <label class="studiou-wcpcm-category-item">
-                                            <input type="checkbox" name="media_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                            <span><?php echo esc_html($category['display_name']); ?></span>
-                                        </label>
-                                    <?php endforeach; ?>
-                                </div>
-                            </div>
-                            <p class="description"><?php echo esc_html__('Select the media categories from which all files will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
-                        </td>
-                    </tr>
-                </table>
-
-                <div class="studiou-wcpcm-warning-message">
-                    <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all media files in the selected categories. The files will be removed from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
-                </div>
-
-                <div class="submit-buttons">
-                    <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-clear-media-button">
-                        <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
-                    </button>
-                    <span class="spinner"></span>
-                </div>
-            </form>
-
-            <div class="studiou-wcpcm-clear-media-progress" style="display: none;">
-                <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
-                <div class="studiou-wcpcm-progress-bar-container">
-                    <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-clear-media-progress-bar"></div>
-                </div>
-                <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-clear-media-progress-text"></div>
-            </div>
-
-            <div class="studiou-wcpcm-clear-media-results" style="display: none;">
-                <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
-                <div id="studiou-wcpcm-clear-media-message"></div>
-            </div>
-        <?php endif; ?>
-    </div>
-
-    <!-- Remove Not Assigned Media Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Remove Not Assigned Media', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('This operation permanently deletes all media files that are not assigned to any media category and are not used as featured image, product gallery image, or attached to any post.', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <form id="studiou-wcpcm-remove-unassigned-media-form" method="post" onsubmit="return false;">
-            <div class="studiou-wcpcm-warning-message">
-                <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all unused uncategorized media files from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
-            </div>
-
-            <div class="submit-buttons">
-                <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-remove-unassigned-media-button">
-                    <?php echo esc_html(sprintf(__('Delete unused uncategorized media (%d)', 'studiou-wc-product-cat-manage'), $unassigned_media_count)); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-
-        <div class="studiou-wcpcm-remove-unassigned-media-progress" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div class="studiou-wcpcm-progress-bar-container">
-                <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-remove-unassigned-media-progress-bar"></div>
-            </div>
-            <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-remove-unassigned-media-progress-text"></div>
-        </div>
-
-        <div class="studiou-wcpcm-remove-unassigned-media-results" style="display: none;">
-            <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-remove-unassigned-media-message"></div>
-        </div>
-    </div>
-
-    <!-- Update Product Variant Prices Operation -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Update product variant prices', 'studiou-wc-product-cat-manage'); ?></h2>
-
-        <p><?php echo esc_html__('Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected.', 'studiou-wc-product-cat-manage'); ?></p>
-
-        <form id="studiou-wcpcm-upvp-form" method="post" onsubmit="return false;">
-            <table class="form-table">
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <div class="studiou-wcpcm-categories-list-container">
-                            <div class="studiou-wcpcm-list-actions">
-                                <button type="button" id="studiou-wcpcm-upvp-check-all" class="button">
-                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                                <button type="button" id="studiou-wcpcm-upvp-uncheck-all" class="button">
-                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
-                                </button>
-                            </div>
-                            <div class="studiou-wcpcm-categories-list" id="upvp_categories_list">
-                                <?php foreach ($categories as $category): ?>
-                                    <label class="studiou-wcpcm-category-item">
-                                        <input type="checkbox" name="upvp_categories[]" value="<?php echo esc_attr($category['id']); ?>">
-                                        <span><?php echo esc_html($category['display_name']); ?></span>
-                                    </label>
-                                <?php endforeach; ?>
-                            </div>
-                        </div>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_attribute"><?php echo esc_html__('Variant attribute', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select id="upvp_attribute" name="upvp_attribute" class="regular-text">
-                            <option value=""><?php echo esc_html__('-- Select attribute --', 'studiou-wc-product-cat-manage'); ?></option>
-                        </select>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_attribute_value"><?php echo esc_html__('Variant attribute value', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <select id="upvp_attribute_value" name="upvp_attribute_value" class="regular-text" disabled>
-                            <option value=""><?php echo esc_html__('-- Select value --', 'studiou-wc-product-cat-manage'); ?></option>
-                        </select>
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <label for="upvp_new_price"><?php echo esc_html__('New Price', 'studiou-wc-product-cat-manage'); ?></label>
-                    </th>
-                    <td>
-                        <input type="number" step="0.01" min="0" id="upvp_new_price" name="upvp_new_price" class="regular-text">
-                    </td>
-                </tr>
-                <tr>
-                    <th scope="row">
-                        <?php echo esc_html__('Affected variations', 'studiou-wc-product-cat-manage'); ?>
-                    </th>
-                    <td>
-                        <strong id="studiou-wcpcm-upvp-affected-count">0</strong>
-                    </td>
-                </tr>
-            </table>
-
-            <div class="submit-buttons">
-                <button type="button" id="studiou-wcpcm-upvp-dry-run-button" class="button">
-                    <?php echo esc_html__('Dry Run', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <button type="button" id="studiou-wcpcm-upvp-apply-button" class="button button-primary">
-                    <?php echo esc_html__('Apply', 'studiou-wc-product-cat-manage'); ?>
-                </button>
-                <span class="spinner"></span>
-            </div>
-        </form>
-
-        <div class="studiou-wcpcm-upvp-results" style="display: none;">
-            <h3><?php echo esc_html__('Results', 'studiou-wc-product-cat-manage'); ?></h3>
-            <div id="studiou-wcpcm-upvp-message"></div>
-        </div>
-    </div>
-
-    <!-- Important Notes -->
-    <div class="studiou-wcpcm-card">
-        <h2><?php echo esc_html__('Important Notes', 'studiou-wc-product-cat-manage'); ?></h2>
-        <ul>
-            <li><?php echo esc_html__('Products can be assigned to multiple categories. This operation will remove the product from the source category and add it to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('If a product is only assigned to the source category, it will be moved entirely to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('If a product is assigned to multiple categories including the source category, it will remain in the other categories and be moved to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('The operation processes all products regardless of their status (published, draft, private).', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><?php echo esc_html__('The batch description operation will overwrite the existing descriptions for all selected categories.', 'studiou-wc-product-cat-manage'); ?></li>
-            <li><strong><?php echo esc_html__('The delete products operation will permanently delete all products and variants from the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
-            <li><strong><?php echo esc_html__('The clear media categories operation will permanently delete all media files from the selected media categories and remove them from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
-            <li><strong><?php echo esc_html__('The remove not assigned media operation will permanently delete all media files that are not categorized and not used anywhere. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
-            <li><?php echo esc_html__('The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified.', 'studiou-wc-product-cat-manage'); ?></li>
-        </ul>
     </div>
-</div>
+</div>

+ 74 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-batch-description.php

@@ -0,0 +1,74 @@
+<?php
+/**
+ * Category Manipulations tab partial: Batch description between categories.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Batch Description Between Categories Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Batch description between categories', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to set the same description for multiple categories at once.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-batch-description-form" method="post">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <div class="studiou-wcpcm-categories-list-container">
+                        <div class="studiou-wcpcm-list-actions">
+                            <button type="button" id="studiou-wcpcm-check-all" class="button">
+                                <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                            <button type="button" id="studiou-wcpcm-uncheck-all" class="button">
+                                <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                        </div>
+                        <div class="studiou-wcpcm-categories-list" id="categories_list">
+                            <?php foreach ($categories as $category): ?>
+                                <label class="studiou-wcpcm-category-item">
+                                    <input type="checkbox" name="selected_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                    <span><?php echo esc_html($category['display_name']); ?></span>
+                                </label>
+                            <?php endforeach; ?>
+                        </div>
+                    </div>
+                    <p class="description"><?php echo esc_html__('Select the categories for which you want to set the description.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="category_description"><?php echo esc_html__('Product categories description', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <textarea name="category_description" id="category_description" rows="5" cols="50" class="large-text"></textarea>
+                    <p class="description"><?php echo esc_html__('Enter the description that will be applied to all selected categories.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+        </table>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary" id="studiou-wcpcm-batch-description-button">
+                <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-batch-description-results" style="display: none;">
+        <h3><?php echo esc_html__('Batch Description Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-batch-description-message"></div>
+    </div>
+</div>

+ 81 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-clear-media.php

@@ -0,0 +1,81 @@
+<?php
+/**
+ * Category Manipulations tab partial: Clear Media Categories.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$media_categories = $manipulator->get_media_categories_with_counts();
+?>
+<!-- Clear Media Categories Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Clear Media Categories', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to permanently delete all media files from selected media categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <?php if (empty($media_categories)): ?>
+        <p><em><?php echo esc_html__('No media categories found. Please install a media category plugin (e.g., Media Library Categories) to use this feature.', 'studiou-wc-product-cat-manage'); ?></em></p>
+    <?php else: ?>
+        <form id="studiou-wcpcm-clear-media-form" method="post" onsubmit="return false;">
+            <table class="form-table">
+                <tr>
+                    <th scope="row">
+                        <label for="clear_media_categories_list"><?php echo esc_html__('Select Media Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                    </th>
+                    <td>
+                        <div class="studiou-wcpcm-categories-list-container">
+                            <div class="studiou-wcpcm-list-actions">
+                                <button type="button" id="studiou-wcpcm-clear-media-check-all" class="button">
+                                    <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                                </button>
+                                <button type="button" id="studiou-wcpcm-clear-media-uncheck-all" class="button">
+                                    <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                                </button>
+                            </div>
+                            <div class="studiou-wcpcm-categories-list" id="clear_media_categories_list">
+                                <?php foreach ($media_categories as $category): ?>
+                                    <label class="studiou-wcpcm-category-item">
+                                        <input type="checkbox" name="media_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                        <span><?php echo esc_html($category['display_name']); ?></span>
+                                    </label>
+                                <?php endforeach; ?>
+                            </div>
+                        </div>
+                        <p class="description"><?php echo esc_html__('Select the media categories from which all files will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
+                    </td>
+                </tr>
+            </table>
+
+            <div class="studiou-wcpcm-warning-message">
+                <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all media files in the selected categories. The files will be removed from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
+            </div>
+
+            <div class="submit-buttons">
+                <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-clear-media-button">
+                    <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
+                </button>
+                <span class="spinner"></span>
+            </div>
+        </form>
+
+        <div class="studiou-wcpcm-clear-media-progress" style="display: none;">
+            <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
+            <div class="studiou-wcpcm-progress-bar-container">
+                <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-clear-media-progress-bar"></div>
+            </div>
+            <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-clear-media-progress-text"></div>
+        </div>
+
+        <div class="studiou-wcpcm-clear-media-results" style="display: none;">
+            <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
+            <div id="studiou-wcpcm-clear-media-message"></div>
+        </div>
+    <?php endif; ?>
+</div>

+ 77 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-delete-products.php

@@ -0,0 +1,77 @@
+<?php
+/**
+ * Category Manipulations tab partial: Delete products in category.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Delete Products in Category Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Delete products in category', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to permanently delete all products and their variants from selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-delete-products-form" method="post">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="delete_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <div class="studiou-wcpcm-categories-list-container">
+                        <div class="studiou-wcpcm-list-actions">
+                            <button type="button" id="studiou-wcpcm-delete-check-all" class="button">
+                                <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                            <button type="button" id="studiou-wcpcm-delete-uncheck-all" class="button">
+                                <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                        </div>
+                        <div class="studiou-wcpcm-categories-list" id="delete_categories_list">
+                            <?php foreach ($categories as $category): ?>
+                                <label class="studiou-wcpcm-category-item">
+                                    <input type="checkbox" name="delete_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                    <span><?php echo esc_html($category['display_name']); ?></span>
+                                </label>
+                            <?php endforeach; ?>
+                        </div>
+                    </div>
+                    <p class="description"><?php echo esc_html__('Select the categories from which all products will be permanently deleted.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+        </table>
+
+        <div class="studiou-wcpcm-warning-message">
+            <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all products (including variants) in the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
+        </div>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-delete-products-button">
+                <?php echo esc_html__('Delete', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-delete-progress" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div class="studiou-wcpcm-progress-bar-container">
+            <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-delete-progress-bar"></div>
+        </div>
+        <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-delete-progress-text"></div>
+    </div>
+
+    <div class="studiou-wcpcm-delete-results" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-delete-message"></div>
+    </div>
+</div>

+ 71 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-move.php

@@ -0,0 +1,71 @@
+<?php
+/**
+ * Category Manipulations tab partial: Move products between categories.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Move Products Between Categories Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Move products between categories', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation allows you to move all products from one category to another. Products will be removed from the source category and added to the target category.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-move-form" method="post">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="source_category"><?php echo esc_html__('Source Category', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select name="source_category" id="source_category" class="regular-text" required>
+                        <option value=""><?php echo esc_html__('-- Select Source Category --', 'studiou-wc-product-cat-manage'); ?></option>
+                        <?php foreach ($categories as $category): ?>
+                            <option value="<?php echo esc_attr($category['id']); ?>">
+                                <?php echo esc_html($category['display_name']); ?>
+                            </option>
+                        <?php endforeach; ?>
+                    </select>
+                    <p class="description"><?php echo esc_html__('Select the category from which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="target_category"><?php echo esc_html__('Target Category', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select name="target_category" id="target_category" class="regular-text" required>
+                        <option value=""><?php echo esc_html__('-- Select Target Category --', 'studiou-wc-product-cat-manage'); ?></option>
+                        <?php foreach ($categories as $category): ?>
+                            <option value="<?php echo esc_attr($category['id']); ?>">
+                                <?php echo esc_html($category['display_name']); ?>
+                            </option>
+                        <?php endforeach; ?>
+                    </select>
+                    <p class="description"><?php echo esc_html__('Select the category to which products will be moved.', 'studiou-wc-product-cat-manage'); ?></p>
+                </td>
+            </tr>
+        </table>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary" id="studiou-wcpcm-move-button">
+                <?php echo esc_html__('Do', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-move-results" style="display: none;">
+        <h3><?php echo esc_html__('Move Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-move-message"></div>
+    </div>
+</div>

+ 29 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-notes.php

@@ -0,0 +1,29 @@
+<?php
+/**
+ * Category Manipulations tab partial: Important Notes (static).
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+?>
+<!-- Important Notes -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Important Notes', 'studiou-wc-product-cat-manage'); ?></h2>
+    <ul>
+        <li><?php echo esc_html__('Products can be assigned to multiple categories. This operation will remove the product from the source category and add it to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('If a product is only assigned to the source category, it will be moved entirely to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('If a product is assigned to multiple categories including the source category, it will remain in the other categories and be moved to the target category.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('The operation processes all products regardless of their status (published, draft, private).', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><?php echo esc_html__('The batch description operation will overwrite the existing descriptions for all selected categories.', 'studiou-wc-product-cat-manage'); ?></li>
+        <li><strong><?php echo esc_html__('The delete products operation will permanently delete all products and variants from the selected categories. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
+        <li><strong><?php echo esc_html__('The clear media categories operation will permanently delete all media files from the selected media categories and remove them from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
+        <li><strong><?php echo esc_html__('The remove not assigned media operation will permanently delete all media files that are not categorized and not used anywhere. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></li>
+        <li><?php echo esc_html__('The update product variant prices operation only modifies the regular price of product variations. Order line items, cart contents, and historical order data are never modified.', 'studiou-wc-product-cat-manage'); ?></li>
+    </ul>
+</div>

+ 48 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-remove-unassigned.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Category Manipulations tab partial: Remove Not Assigned Media.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$unassigned_media_count = $manipulator->get_unassigned_unused_media_count();
+?>
+<!-- Remove Not Assigned Media Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Remove Not Assigned Media', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('This operation permanently deletes all media files that are not assigned to any media category and are not used as featured image, product gallery image, or attached to any post.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-remove-unassigned-media-form" method="post" onsubmit="return false;">
+        <div class="studiou-wcpcm-warning-message">
+            <p><strong><?php echo esc_html__('WARNING: This operation will permanently delete all unused uncategorized media files from the server. This action cannot be undone!', 'studiou-wc-product-cat-manage'); ?></strong></p>
+        </div>
+
+        <div class="submit-buttons">
+            <button type="submit" class="button button-primary button-danger" id="studiou-wcpcm-remove-unassigned-media-button">
+                <?php echo esc_html(sprintf(__('Delete unused uncategorized media (%d)', 'studiou-wc-product-cat-manage'), $unassigned_media_count)); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-remove-unassigned-media-progress" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Progress', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div class="studiou-wcpcm-progress-bar-container">
+            <div class="studiou-wcpcm-progress-bar" id="studiou-wcpcm-remove-unassigned-media-progress-bar"></div>
+        </div>
+        <div class="studiou-wcpcm-progress-text" id="studiou-wcpcm-remove-unassigned-media-progress-text"></div>
+    </div>
+
+    <div class="studiou-wcpcm-remove-unassigned-media-results" style="display: none;">
+        <h3><?php echo esc_html__('Deletion Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-remove-unassigned-media-message"></div>
+    </div>
+</div>

+ 103 - 0
studiou-wc-product-cat-manage/views/manipulations/tab-upvp.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * Category Manipulations tab partial: Update product variant prices.
+ *
+ * @package Studiou_WC_Product_Cat_Manage
+ *
+ * @var Studiou_WC_Product_Cat_Manage_Manipulator $manipulator
+ */
+
+// If this file is called directly, abort.
+if (!defined('WPINC')) {
+    die;
+}
+
+$categories = $manipulator->get_categories_with_counts();
+?>
+<!-- Update Product Variant Prices Operation -->
+<div class="studiou-wcpcm-card">
+    <h2><?php echo esc_html__('Update product variant prices', 'studiou-wc-product-cat-manage'); ?></h2>
+
+    <p><?php echo esc_html__('Bulk-update the regular price of product variations matching the selected categories and a chosen variation attribute. Only product records are modified — existing orders and cart contents are not affected.', 'studiou-wc-product-cat-manage'); ?></p>
+
+    <form id="studiou-wcpcm-upvp-form" method="post" onsubmit="return false;">
+        <table class="form-table">
+            <tr>
+                <th scope="row">
+                    <label for="upvp_categories_list"><?php echo esc_html__('Select Categories', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <div class="studiou-wcpcm-categories-list-container">
+                        <div class="studiou-wcpcm-list-actions">
+                            <button type="button" id="studiou-wcpcm-upvp-check-all" class="button">
+                                <?php echo esc_html__('Check All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                            <button type="button" id="studiou-wcpcm-upvp-uncheck-all" class="button">
+                                <?php echo esc_html__('Uncheck All', 'studiou-wc-product-cat-manage'); ?>
+                            </button>
+                        </div>
+                        <div class="studiou-wcpcm-categories-list" id="upvp_categories_list">
+                            <?php foreach ($categories as $category): ?>
+                                <label class="studiou-wcpcm-category-item">
+                                    <input type="checkbox" name="upvp_categories[]" value="<?php echo esc_attr($category['id']); ?>">
+                                    <span><?php echo esc_html($category['display_name']); ?></span>
+                                </label>
+                            <?php endforeach; ?>
+                        </div>
+                    </div>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="upvp_attribute"><?php echo esc_html__('Variant attribute', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select id="upvp_attribute" name="upvp_attribute" class="regular-text">
+                        <option value=""><?php echo esc_html__('-- Select attribute --', 'studiou-wc-product-cat-manage'); ?></option>
+                    </select>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="upvp_attribute_value"><?php echo esc_html__('Variant attribute value', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <select id="upvp_attribute_value" name="upvp_attribute_value" class="regular-text" disabled>
+                        <option value=""><?php echo esc_html__('-- Select value --', 'studiou-wc-product-cat-manage'); ?></option>
+                    </select>
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <label for="upvp_new_price"><?php echo esc_html__('New Price', 'studiou-wc-product-cat-manage'); ?></label>
+                </th>
+                <td>
+                    <input type="number" step="0.01" min="0" id="upvp_new_price" name="upvp_new_price" class="regular-text">
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    <?php echo esc_html__('Affected variations', 'studiou-wc-product-cat-manage'); ?>
+                </th>
+                <td>
+                    <strong id="studiou-wcpcm-upvp-affected-count">0</strong>
+                </td>
+            </tr>
+        </table>
+
+        <div class="submit-buttons">
+            <button type="button" id="studiou-wcpcm-upvp-dry-run-button" class="button">
+                <?php echo esc_html__('Dry Run', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <button type="button" id="studiou-wcpcm-upvp-apply-button" class="button button-primary">
+                <?php echo esc_html__('Apply', 'studiou-wc-product-cat-manage'); ?>
+            </button>
+            <span class="spinner"></span>
+        </div>
+    </form>
+
+    <div class="studiou-wcpcm-upvp-results" style="display: none;">
+        <h3><?php echo esc_html__('Results', 'studiou-wc-product-cat-manage'); ?></h3>
+        <div id="studiou-wcpcm-upvp-message"></div>
+    </div>
+</div>