Jelajahi Sumber

feat: store selector, default qty, and clear listing link

Settings tab — Order Import section:
- Checkbox list of all OC stores (Default + any extra stores); saves as
  module_reverb_order_stores[]; first selected store_id is applied to
  imported orders (replaces hardcoded store_id = 0)
- Default Product Quantity field; used as fallback when a Reverb order
  line has no quantity (replaces hardcoded qty = 1)

Product tab:
- "Remove link" button shown next to the Reverb listing ID; calls
  clearListingId() which nulls reverb_listing_id and last_synced_at so
  the next sync treats the product as new and creates a fresh listing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benjamin Harris 1 Minggu lalu
induk
melakukan
c542f1b725

+ 42 - 0
upload/admin/controller/extension/module/reverb.php

@@ -40,6 +40,8 @@ class ControllerExtensionModuleReverb extends Controller {
             'entry_shipping_international', 'help_shipping_international',
             'help_sync_categories', 'button_sync_now',
             'text_order_import', 'button_import_orders',
+            'entry_order_stores', 'help_order_stores', 'text_select_all', 'text_unselect_all',
+            'entry_default_qty', 'help_default_qty',
             'text_category_mapping_help', 'text_no_categories',
             'column_oc_category', 'column_reverb_category',
             'column_date', 'column_product', 'column_direction', 'column_status', 'column_message',
@@ -64,6 +66,8 @@ class ControllerExtensionModuleReverb extends Controller {
             'module_reverb_sync_categories',
             'module_reverb_shipping_domestic',
             'module_reverb_shipping_international',
+            'module_reverb_order_stores',
+            'module_reverb_default_qty',
         ];
         foreach ($fields as $key) {
             $data[$key] = $this->request->post[$key] ?? $this->config->get($key);
@@ -74,6 +78,14 @@ class ControllerExtensionModuleReverb extends Controller {
         $data['module_reverb_sync_categories']     = $data['module_reverb_sync_categories'] ?? [];
         $data['module_reverb_shipping_domestic']   = $data['module_reverb_shipping_domestic'] ?? '0.00';
         $data['module_reverb_shipping_international'] = $data['module_reverb_shipping_international'] ?? '0.00';
+        $data['module_reverb_order_stores']        = $data['module_reverb_order_stores'] ?? [0];
+        $data['module_reverb_default_qty']         = $data['module_reverb_default_qty'] ?? 1;
+
+        // Stores list for the checkbox UI
+        $this->load->model('setting/store');
+        $stores = $this->model_setting_store->getStores();
+        array_unshift($stores, ['store_id' => 0, 'name' => 'Default']);
+        $data['stores'] = $stores;
 
         // All OC categories for the multi-select
         $data['categories'] = $this->getCategoryTree();
@@ -294,6 +306,7 @@ class ControllerExtensionModuleReverb extends Controller {
         $reverb_row = $this->model_extension_module_reverb->getProductMap($product_id);
 
         $data = [
+            'product_id'                => $product_id,
             'reverb_sync_enabled'       => $reverb_row ? (int)$reverb_row['sync_enabled']      : 0,
             'reverb_condition_uuid'     => $reverb_row ? $reverb_row['condition_uuid']          : '',
             'reverb_category_uuid'      => $reverb_row ? $reverb_row['reverb_category_uuid']   : '',
@@ -302,11 +315,33 @@ class ControllerExtensionModuleReverb extends Controller {
             'reverb_upc_does_not_apply' => $reverb_row ? (int)$reverb_row['upc_does_not_apply'] : 1,
             'reverb_conditions'         => $this->model_extension_module_reverb->getListingConditions(),
             'reverb_categories_grouped' => $this->model_extension_module_reverb->getReverbCategoriesGrouped(),
+            'clear_listing_url'         => $this->url->link('extension/module/reverb/clearListingId', 'user_token=' . $this->session->data['user_token'], true),
         ];
 
         $this->response->setOutput($this->load->view('extension/module/reverb_product', $data));
     }
 
+    // -------------------------------------------------------------------------
+    // Clear Reverb listing ID from a product (AJAX)
+    // -------------------------------------------------------------------------
+
+    public function clearListingId() {
+        $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;
+        }
+        $product_id = (int)($this->request->get['product_id'] ?? 0);
+        if (!$product_id) {
+            $this->response->setOutput(json_encode(['success' => false, 'error' => 'Invalid product ID.']));
+            return;
+        }
+        $this->model_extension_module_reverb->clearListingId($product_id);
+        $this->response->setOutput(json_encode(['success' => true]));
+    }
+
     // -------------------------------------------------------------------------
     // Clear sync log (AJAX)
     // -------------------------------------------------------------------------
@@ -389,6 +424,11 @@ class ControllerExtensionModuleReverb extends Controller {
             }
         }
 
+        $order_stores = $this->config->get('module_reverb_order_stores');
+        if (!is_array($order_stores) || empty($order_stores)) {
+            $order_stores = [0];
+        }
+
         return [
             'api_token'              => $this->config->get('module_reverb_api_token'),
             'sync_direction'         => $this->config->get('module_reverb_sync_direction') ?? 'push',
@@ -396,6 +436,8 @@ class ControllerExtensionModuleReverb extends Controller {
             'shipping_international' => $this->config->get('module_reverb_shipping_international') ?? '0',
             'currency'               => $this->config->get('config_currency') ?? 'AUD',
             'store_url'              => $store_url ?? '',
+            'order_stores'           => $order_stores,
+            'default_qty'            => max(1, (int)($this->config->get('module_reverb_default_qty') ?? 1)),
         ];
     }
 

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

@@ -66,6 +66,12 @@ $_['text_not_synced']            = 'Not yet synced';
 $_['text_order_import']          = 'Order Import';
 $_['button_import_orders']       = 'Import Orders from Reverb';
 $_['text_orders_imported']       = 'Import complete. %d order(s) imported, %d skipped.';
+$_['entry_order_stores']         = 'Import Orders to Store(s)';
+$_['help_order_stores']          = 'Imported Reverb orders will be assigned to the first selected store. Select at least one.';
+$_['text_select_all']            = 'Select All';
+$_['text_unselect_all']          = 'Unselect All';
+$_['entry_default_qty']          = 'Default Product Quantity';
+$_['help_default_qty']           = 'Fallback quantity applied to an imported order line when Reverb does not supply one.';
 
 // Success / error messages
 $_['text_success']               = 'Settings saved successfully.';

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

@@ -311,6 +311,14 @@ class ModelExtensionModuleReverb extends Model {
         ");
     }
 
+    public function clearListingId($product_id) {
+        $this->db->query("
+            UPDATE `" . DB_PREFIX . "reverb_product_map`
+            SET `reverb_listing_id` = '', `last_synced_at` = NULL
+            WHERE `product_id` = '" . (int)$product_id . "'
+        ");
+    }
+
     public function clearSyncLog() {
         $this->db->query("DELETE FROM `" . DB_PREFIX . "reverb_sync_log`");
     }
@@ -466,7 +474,12 @@ class ModelExtensionModuleReverb extends Model {
         $shipping_amt = (float)($o['shipping']['amount']        ?? $o['shipping']        ?? 0);
         $total_amt    = (float)($o['total']['amount']           ?? $o['total']           ?? ($product_amt + $shipping_amt));
         $currency     = $o['total']['currency'] ?? $settings['currency'] ?? 'AUD';
-        $qty          = (int)($o['quantity'] ?? 1);
+
+        // Store and quantity from settings
+        $order_stores = $settings['order_stores'] ?? [0];
+        $store_id     = (int)($order_stores[0] ?? 0);
+        $default_qty  = max(1, (int)($settings['default_qty'] ?? 1));
+        $qty          = (int)($o['quantity'] ?? 0) ?: $default_qty;
 
         // OC order status mapping
         $status_map  = ['paid' => 2, 'shipped' => 3, 'received' => 5, 'picked_up' => 5];
@@ -498,7 +511,7 @@ class ModelExtensionModuleReverb extends Model {
             INSERT INTO `" . DB_PREFIX . "order` SET
             `invoice_prefix`          = 'REV-',
             `invoice_no`              = 0,
-            `store_id`                = 0,
+            `store_id`                = $store_id,
             `store_name`              = '$store_name',
             `store_url`               = '$store_url',
             `customer_id`             = 0,

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

@@ -143,6 +143,36 @@
 
             <h4>{{ text_order_import }}</h4>
 
+            <div class="form-group">
+              <label class="col-sm-2 control-label">{{ entry_order_stores }}</label>
+              <div class="col-sm-10">
+                <div class="well well-sm" style="height:150px;overflow:auto;">
+                  {% for store in stores %}
+                  <div class="checkbox">
+                    <label>
+                      <input type="checkbox" name="module_reverb_order_stores[]" value="{{ store.store_id }}"
+                        {% if store.store_id in module_reverb_order_stores %}checked{% endif %}>
+                      {% if store.store_id == 0 %}<b>(Default)</b>{% else %}{{ store.name }}{% endif %}
+                    </label>
+                  </div>
+                  {% endfor %}
+                </div>
+                <a href="#" onclick="$(this).closest('.form-group').find(':checkbox').prop('checked',true);return false;">{{ text_select_all }}</a>
+                /
+                <a href="#" onclick="$(this).closest('.form-group').find(':checkbox').prop('checked',false);return false;">{{ text_unselect_all }}</a>
+                <span class="help-block">{{ help_order_stores }}</span>
+              </div>
+            </div>
+
+            <div class="form-group">
+              <label class="col-sm-2 control-label">{{ entry_default_qty }}</label>
+              <div class="col-sm-2">
+                <input type="number" name="module_reverb_default_qty" min="1"
+                       value="{{ module_reverb_default_qty }}" class="form-control" />
+                <span class="help-block">{{ help_default_qty }}</span>
+              </div>
+            </div>
+
             <div class="form-group">
               <div class="col-sm-offset-2 col-sm-10">
                 <button type="button" id="btn-import-orders" class="btn btn-info">

+ 39 - 11
upload/admin/view/template/extension/module/reverb_product.twig

@@ -86,25 +86,53 @@
       </div>
     </div>
 
-    {% if reverb_listing_id %}
-    <div class="form-group">
+    <div class="form-group" id="reverb-listing-row">
       <label class="col-sm-2 control-label">Reverb Listing</label>
       <div class="col-sm-10">
-        <p class="form-control-static">
+        {% if reverb_listing_id %}
+        <p class="form-control-static" id="reverb-listing-link">
           <a href="https://reverb.com/item/{{ reverb_listing_id }}" target="_blank" rel="noopener">
             <i class="fa fa-external-link"></i> View on Reverb (ID: {{ reverb_listing_id }})
           </a>
+          &nbsp;
+          <button type="button" class="btn btn-danger btn-xs" id="btn-clear-listing"
+                  data-product-id="{{ product_id }}" data-clear-url="{{ clear_listing_url }}">
+            <i class="fa fa-unlink"></i> Remove link
+          </button>
+          <span id="clear-listing-result" style="margin-left:8px;"></span>
         </p>
+        {% else %}
+        <p class="form-control-static text-muted" id="reverb-not-synced">Not yet synced to Reverb.</p>
+        {% endif %}
       </div>
     </div>
-    {% else %}
-    <div class="form-group">
-      <label class="col-sm-2 control-label">Reverb Listing</label>
-      <div class="col-sm-10">
-        <p class="form-control-static text-muted">Not yet synced to Reverb.</p>
-      </div>
-    </div>
-    {% endif %}
+
+    <script type="text/javascript">
+    (function () {
+      var $btn = document.getElementById('btn-clear-listing');
+      if (!$btn) return;
+      $btn.addEventListener('click', function () {
+        if (!confirm('Remove the Reverb listing link from this product? The listing will not be deleted from Reverb, but this product will be treated as unsynced on the next push.')) return;
+        $btn.disabled = true;
+        var $result = document.getElementById('clear-listing-result');
+        var url = $btn.dataset.clearUrl + '&product_id=' + encodeURIComponent($btn.dataset.productId);
+        fetch(url).then(function(r){ return r.json(); }).then(function(data) {
+          if (data.success) {
+            var $link = document.getElementById('reverb-listing-link');
+            $link.innerHTML = '<span class="text-muted">Not yet synced to Reverb.</span>';
+          } else {
+            $result.className = 'text-danger';
+            $result.textContent = data.error || 'Failed.';
+            $btn.disabled = false;
+          }
+        }).catch(function() {
+          $result.className = 'text-danger';
+          $result.textContent = 'Request failed.';
+          $btn.disabled = false;
+        });
+      });
+    }());
+    </script>
 
   </div>
 </div>