Selaa lähdekoodia

feat: Select2 category search + Reverb Categories tab

Product tab:
- Category dropdown now uses Select2 (type-to-search with clear button).
  OC3 admin already bundles Select2 so no extra dependency is needed.

Settings page — new Reverb Categories tab (4th tab):
- Scrollable table of every Reverb category with its full_name and UUID;
  pre-populated from the cached category list on page load
- Live client-side filter input (search-as-you-type across the table)
- "Refresh from Reverb" button calls refreshCategories() AJAX action which
  bypasses the 24 h cache, hits /categories/flat, saves the result, and
  re-renders the table in-place with the fresh data
- refreshReverbCategories() model method extracted from getReverbCategories()
  so both the normal (cached) and forced-refresh paths share the same fetch
  and persist logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benjamin Harris 1 viikko sitten
vanhempi
sitoutus
7c1669d4e2

+ 27 - 2
upload/admin/controller/extension/module/reverb.php

@@ -31,7 +31,7 @@ class ControllerExtensionModuleReverb extends Controller {
 
         // Language strings required by the view
         $lang_keys = [
-            'tab_settings', 'tab_categories', 'tab_log',
+            'tab_settings', 'tab_categories', 'tab_reverb_cats', 'tab_log',
             'text_api_settings', 'text_shipping_settings', 'text_sync_settings', 'text_manual_sync',
             'entry_api_token', 'help_api_token',
             'entry_status', 'entry_sync_direction',
@@ -44,6 +44,8 @@ class ControllerExtensionModuleReverb extends Controller {
             'entry_default_qty', 'help_default_qty',
             'text_category_mapping_help', 'text_no_categories',
             'column_oc_category', 'column_reverb_category',
+            'text_reverb_cats_help', 'button_refresh_cats', 'text_filter_cats',
+            'column_cat_name', 'column_cat_uuid',
             'column_date', 'column_product', 'column_direction', 'column_status', 'column_message',
             'text_push', 'text_pull', 'text_error', 'text_no_log', 'button_clear_log',
             'text_success', 'text_log_success',
@@ -108,7 +110,9 @@ class ControllerExtensionModuleReverb extends Controller {
         $data['cancel']          = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module', true);
         $data['sync_url']        = $this->url->link('extension/module/reverb/sync', 'user_token=' . $this->session->data['user_token'], true);
         $data['import_url']      = $this->url->link('extension/module/reverb/importOrders', 'user_token=' . $this->session->data['user_token'], true);
-        $data['clear_log_url']   = $this->url->link('extension/module/reverb/clearLog', 'user_token=' . $this->session->data['user_token'], true);
+        $data['clear_log_url']        = $this->url->link('extension/module/reverb/clearLog', 'user_token=' . $this->session->data['user_token'], true);
+        $data['refresh_cats_url']     = $this->url->link('extension/module/reverb/refreshCategories', 'user_token=' . $this->session->data['user_token'], true);
+        $data['reverb_categories_flat'] = $this->model_extension_module_reverb->getReverbCategories();
         $data['categories_url']  = $this->url->link('extension/module/reverb/reverbCategories', 'user_token=' . $this->session->data['user_token'], true);
 
         $data['header']      = $this->load->controller('common/header');
@@ -358,6 +362,27 @@ class ControllerExtensionModuleReverb extends Controller {
         $this->response->setOutput(json_encode(['success' => true, 'message' => $this->language->get('text_log_cleared')]));
     }
 
+    // -------------------------------------------------------------------------
+    // Refresh Reverb category list (AJAX — bypasses 24 h cache)
+    // -------------------------------------------------------------------------
+
+    public function refreshCategories() {
+        $this->load->language('extension/module/reverb');
+        $this->load->model('extension/module/reverb');
+        $this->response->addHeader('Content-Type: application/json');
+        if (!$this->user->hasPermission('modify', 'extension/module/reverb')) {
+            $this->response->setOutput(json_encode(['success' => false, 'error' => $this->language->get('error_permission')]));
+            return;
+        }
+        $categories = $this->model_extension_module_reverb->refreshReverbCategories();
+        $this->response->setOutput(json_encode([
+            'success'    => true,
+            'count'      => count($categories),
+            'message'    => sprintf($this->language->get('text_cats_refreshed'), count($categories)),
+            'categories' => $categories,
+        ]));
+    }
+
     // -------------------------------------------------------------------------
     // Reverb categories (AJAX — for category mapping dropdowns)
     // -------------------------------------------------------------------------

+ 9 - 0
upload/admin/language/en-gb/extension/module/reverb.php

@@ -9,6 +9,7 @@ $_['text_module']                = 'Modules';
 // Tabs
 $_['tab_settings']               = 'Settings';
 $_['tab_categories']             = 'Category Mapping';
+$_['tab_reverb_cats']            = 'Reverb Categories';
 $_['tab_log']                    = 'Sync Log';
 
 // Settings tab
@@ -35,6 +36,14 @@ $_['column_oc_category']         = 'OpenCart Category';
 $_['column_reverb_category']     = 'Reverb Category';
 $_['text_no_categories']         = 'No categories selected. Choose categories in the Settings tab first.';
 
+// Reverb categories tab
+$_['text_reverb_cats_help']      = 'All categories available on Reverb. Use this list to find the exact category name and UUID to assign to your products. Click Refresh to fetch the latest list from Reverb.';
+$_['button_refresh_cats']        = 'Refresh from Reverb';
+$_['text_cats_refreshed']        = '%d categories loaded from Reverb.';
+$_['text_filter_cats']           = 'Search categories…';
+$_['column_cat_name']            = 'Category';
+$_['column_cat_uuid']            = 'UUID';
+
 // Sync log tab
 $_['button_clear_log']           = 'Clear Log';
 $_['text_log_cleared']           = 'Sync log cleared.';

+ 8 - 2
upload/admin/model/extension/module/reverb.php

@@ -257,9 +257,15 @@ class ModelExtensionModuleReverb extends Model {
             return json_decode($cached, true);
         }
 
+        return $this->refreshReverbCategories();
+    }
+
+    public function refreshReverbCategories() {
+        $cached = $this->config->get('module_reverb_categories_cache');
+
         try {
-            $api  = $this->getApi();
-            $resp = $api->getCategories();
+            $api        = $this->getApi();
+            $resp       = $api->getCategories();
             $categories = isset($resp['categories']) ? $resp['categories'] : [];
 
             $this->load->model('setting/setting');

+ 86 - 0
upload/admin/view/template/extension/module/reverb.twig

@@ -42,6 +42,7 @@
         <ul class="nav nav-tabs">
           <li class="active"><a href="#tab-settings" data-toggle="tab">{{ tab_settings }}</a></li>
           <li><a href="#tab-categories" data-toggle="tab">{{ tab_categories }}</a></li>
+          <li><a href="#tab-reverb-cats" data-toggle="tab">{{ tab_reverb_cats }}</a></li>
           <li><a href="#tab-log" data-toggle="tab">{{ tab_log }}</a></li>
         </ul>
 
@@ -232,6 +233,46 @@
             {% endif %}
           </div><!-- #tab-categories -->
 
+          <!-- ================================================================ -->
+          <!-- TAB: Reverb Categories -->
+          <!-- ================================================================ -->
+          <div class="tab-pane" id="tab-reverb-cats">
+            <p class="help-block">{{ text_reverb_cats_help }}</p>
+
+            <div style="margin-bottom:12px;">
+              <button type="button" id="btn-refresh-cats" class="btn btn-primary btn-sm">
+                <i class="fa fa-refresh"></i> {{ button_refresh_cats }}
+              </button>
+              <span id="refresh-cats-result" style="margin-left:10px;"></span>
+            </div>
+
+            <div style="margin-bottom:10px;">
+              <input type="text" id="cat-filter" class="form-control"
+                     placeholder="{{ text_filter_cats }}" style="max-width:400px;" />
+            </div>
+
+            <div style="max-height:500px;overflow-y:auto;">
+              <table class="table table-bordered table-hover table-condensed" id="reverb-cats-table">
+                <thead>
+                  <tr>
+                    <th>{{ column_cat_name }}</th>
+                    <th style="width:310px;">{{ column_cat_uuid }}</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {% for cat in reverb_categories_flat %}
+                  <tr>
+                    <td>{{ cat.full_name|default(cat.name) }}</td>
+                    <td><code>{{ cat.uuid }}</code></td>
+                  </tr>
+                  {% else %}
+                  <tr><td colspan="2" class="text-muted text-center">No categories loaded yet. Click Refresh to fetch from Reverb.</td></tr>
+                  {% endfor %}
+                </tbody>
+              </table>
+            </div>
+          </div><!-- #tab-reverb-cats -->
+
           <!-- ================================================================ -->
           <!-- TAB: Sync Log -->
           <!-- ================================================================ -->
@@ -344,6 +385,51 @@ $(function() {
         });
     });
 
+    // Reverb Categories tab — refresh button
+    $('#btn-refresh-cats').on('click', function() {
+        var $btn    = $(this);
+        var $result = $('#refresh-cats-result');
+        var $tbody  = $('#reverb-cats-table tbody');
+
+        $btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Refreshing…');
+        $result.text('');
+
+        var _p = new URLSearchParams(window.location.search);
+        var _url = 'index.php?route=extension/module/reverb/refreshCategories&user_token=' + encodeURIComponent(_p.get('user_token') || '');
+
+        $.ajax({
+            url: _url, type: 'GET', dataType: 'json',
+            success: function(data) {
+                if (data.success) {
+                    $result.removeClass('text-danger').addClass('text-success').text(data.message);
+                    var rows = '';
+                    $.each(data.categories, function(i, cat) {
+                        rows += '<tr><td>' + $('<span>').text(cat.full_name || cat.name).html() + '</td>'
+                              + '<td><code>' + $('<span>').text(cat.uuid).html() + '</code></td></tr>';
+                    });
+                    $tbody.html(rows || '<tr><td colspan="2" class="text-muted text-center">No categories returned.</td></tr>');
+                    $('#cat-filter').trigger('input');
+                } else {
+                    $result.removeClass('text-success').addClass('text-danger').text(data.error || 'Refresh failed.');
+                }
+            },
+            error: function() {
+                $result.removeClass('text-success').addClass('text-danger').text('Request failed.');
+            },
+            complete: function() {
+                $btn.prop('disabled', false).html('<i class="fa fa-refresh"></i> {{ button_refresh_cats }}');
+            }
+        });
+    });
+
+    // Reverb Categories tab — live filter
+    $('#cat-filter').on('input', function() {
+        var q = $(this).val().toLowerCase();
+        $('#reverb-cats-table tbody tr').each(function() {
+            $(this).toggle(!q || $(this).text().toLowerCase().indexOf(q) !== -1);
+        });
+    });
+
     $('#btn-clear-log').on('click', function() {
         if (!confirm('Clear the entire sync log?')) { return; }
         var $btn    = $(this);

+ 13 - 2
upload/admin/view/template/extension/module/reverb_product.twig

@@ -38,7 +38,7 @@
     <div class="form-group">
       <label class="col-sm-2 control-label">Reverb Category</label>
       <div class="col-sm-6">
-        <select name="reverb_category_uuid" class="form-control">
+        <select name="reverb_category_uuid" id="reverb-category-select" class="form-control">
           <option value="">-- Use category mapping default --</option>
           {% for group_name, group_cats in reverb_categories_grouped %}
           <optgroup label="{{ group_name }}">
@@ -51,7 +51,7 @@
           </optgroup>
           {% endfor %}
         </select>
-        <p class="help-block">Leave blank to use the default mapping from the Reverb module settings.</p>
+        <p class="help-block">Start typing to search. Leave blank to use the category mapping default.</p>
       </div>
     </div>
 
@@ -108,6 +108,17 @@
     </div>
 
     <script type="text/javascript">
+    // Searchable category dropdown — OC3 admin bundles Select2
+    (function () {
+      if (typeof $.fn.select2 !== 'undefined') {
+        $('#reverb-category-select').select2({
+          placeholder: '-- Use category mapping default --',
+          allowClear: true,
+          width: '100%'
+        });
+      }
+    }());
+
     (function () {
       var $btn = document.getElementById('btn-clear-listing');
       if (!$btn) return;