# Offer Manager Admin Form Redesign — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Redesign the admin offer form — group panel with inline product search + drag-to-reorder, tag-chip inputs for Terms and Hero Badges, CSS grid for Images tab, slug auto-generate button, segmented status toggle, and chip display for Categories/Manufacturers.

**Architecture:** Single file redesign of `offer_manager_form.twig` (568 lines → ~700 lines). No PHP changes needed for admin. All changes are HTML/CSS/JS within the Twig template. Bootstrap 3 + jQuery UI already available.

**Tech Stack:** Twig, Bootstrap 3, jQuery, jQuery UI autocomplete, HTML5 drag-and-drop

**Prerequisite:** Plan A Task 3 must be applied first (the `request.term` fix is replicated here in the redesigned autocomplete functions, so if Plan A ran first the resulting code is consistent).

---

## File Map

| File | Change |
|------|--------|
| `admin/view/template/extension/module/offer_manager_form.twig` | Full redesign of all interactive sections |

---

## Task 1: Slug auto-generate button

**Files:**
- Modify: `admin/view/template/extension/module/offer_manager_form.twig`

- [ ] **Step 1: Find current slug field**

The slug field (lines ~70–77):
```twig
              <div class="form-group required">
                <label class="col-sm-2 control-label" for="input-slug"><span data-toggle="tooltip" title="{{ help_slug }}">{{ entry_slug }}</span></label>
                <div class="col-sm-10">
                  <input type="text" name="slug" value="{{ slug }}" placeholder="{{ entry_slug }}" id="input-slug" class="form-control" />
                  {% if error_slug %}
                  <div class="text-danger">{{ error_slug }}</div>
                  {% endif %}
                </div>
              </div>
```

- [ ] **Step 2: Replace with slug row including auto-generate button**

```twig
              <div class="form-group required">
                <label class="col-sm-2 control-label" for="input-slug"><span data-toggle="tooltip" title="{{ help_slug }}">{{ entry_slug }}</span></label>
                <div class="col-sm-10">
                  <div class="input-group">
                    <input type="text" name="slug" value="{{ slug }}" placeholder="{{ entry_slug }}" id="input-slug" class="form-control" />
                    <span class="input-group-btn">
                      <button type="button" id="btn-slug-auto" class="btn btn-default" title="Auto-generate from title">&#9889; Auto</button>
                    </span>
                  </div>
                  {% if error_slug %}
                  <div class="text-danger">{{ error_slug }}</div>
                  {% endif %}
                  <p class="help-block" id="slug-preview" style="font-size:11px;color:#888;margin-top:3px;"></p>
                </div>
              </div>
```

- [ ] **Step 3: Add JS for auto-generate button at bottom of script block**

In the `<script>` block near the bottom of the file, add after the last existing JS:
```javascript
/* ── Slug auto-generate ── */
document.getElementById('btn-slug-auto').addEventListener('click', function() {
  var titleEl = document.querySelector('[name^="offer_description"][name$="[title]"]');
  if (!titleEl) { return; }
  var raw = titleEl.value.trim();
  if (!raw) { return; }
  var slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
  var slugInput = document.getElementById('input-slug');
  if (slugInput.value && slugInput.value !== slug) {
    if (!confirm('Replace current slug "' + slugInput.value + '" with "' + slug + '"?')) { return; }
  }
  slugInput.value = slug;
  var preview = document.getElementById('slug-preview');
  if (preview) { preview.textContent = '/offers/' + slug; }
});

document.getElementById('input-slug').addEventListener('input', function() {
  var preview = document.getElementById('slug-preview');
  if (preview && this.value) { preview.textContent = '/offers/' + this.value; }
});
```

- [ ] **Step 4: Verify no syntax errors by checking Twig renders (grep)**

Run: `grep -n "btn-slug-auto\|input-slug" /var/www/skyland/src/admin/view/template/extension/module/offer_manager_form.twig`
Expected: both IDs found.

- [ ] **Step 5: Commit**

```bash
git add admin/view/template/extension/module/offer_manager_form.twig
git commit -m "feat(admin): add slug auto-generate button on offer form"
```

---

## Task 2: Status segmented toggle

**Files:**
- Modify: `admin/view/template/extension/module/offer_manager_form.twig`

- [ ] **Step 1: Find current status select**

Current block (lines ~125–138):
```twig
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-status">{{ entry_status }}</label>
                <div class="col-sm-10">
                  <select name="status" id="input-status" class="form-control">
                    {% if status %}
                    <option value="1" selected="selected">{{ text_enabled }}</option>
                    <option value="0">{{ text_disabled }}</option>
                    {% else %}
                    <option value="1">{{ text_enabled }}</option>
                    <option value="0" selected="selected">{{ text_disabled }}</option>
                    {% endif %}
                  </select>
                </div>
              </div>
```

- [ ] **Step 2: Replace with segmented toggle**

```twig
              <div class="form-group">
                <label class="col-sm-2 control-label">{{ entry_status }}</label>
                <div class="col-sm-10">
                  <div class="btn-group offer-status-toggle" role="group">
                    <button type="button" class="btn {% if status %}btn-primary{% else %}btn-default{% endif %}" id="btn-status-enabled" data-value="1">{{ text_enabled }}</button>
                    <button type="button" class="btn {% if not status %}btn-danger{% else %}btn-default{% endif %}" id="btn-status-disabled" data-value="0">{{ text_disabled }}</button>
                  </div>
                  <input type="hidden" name="status" id="input-status-hidden" value="{{ status ? '1' : '0' }}" />
                </div>
              </div>
```

- [ ] **Step 3: Add JS for toggle**

In the `<script>` block, add:
```javascript
/* ── Status toggle ── */
(function() {
  var btnEnabled  = document.getElementById('btn-status-enabled');
  var btnDisabled = document.getElementById('btn-status-disabled');
  var hidden      = document.getElementById('input-status-hidden');
  if (!btnEnabled || !btnDisabled || !hidden) { return; }
  function setStatus(val) {
    hidden.value = val;
    if (val === '1') {
      btnEnabled.className  = btnEnabled.className.replace('btn-default', 'btn-primary');
      btnDisabled.className = btnDisabled.className.replace('btn-danger', 'btn-default');
    } else {
      btnEnabled.className  = btnEnabled.className.replace('btn-primary', 'btn-default');
      btnDisabled.className = btnDisabled.className.replace('btn-default', 'btn-danger');
    }
  }
  btnEnabled.addEventListener('click',  function() { setStatus('1'); });
  btnDisabled.addEventListener('click', function() { setStatus('0'); });
})();
```

- [ ] **Step 4: Commit**

```bash
git add admin/view/template/extension/module/offer_manager_form.twig
git commit -m "feat(admin): replace status select with segmented toggle button"
```

---

## Task 3: Terms tag-chip input

**Files:**
- Modify: `admin/view/template/extension/module/offer_manager_form.twig`

- [ ] **Step 1: Find current terms textarea**

```twig
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-terms"><span data-toggle="tooltip" title="{{ help_terms }}">{{ entry_terms }}</span></label>
                <div class="col-sm-10">
                  <textarea name="terms" rows="5" placeholder="{{ entry_terms }}" id="input-terms" class="form-control">{{ terms }}</textarea>
                </div>
              </div>
```

- [ ] **Step 2: Replace with tag-chip widget**

```twig
              <div class="form-group">
                <label class="col-sm-2 control-label"><span data-toggle="tooltip" title="{{ help_terms }}">{{ entry_terms }}</span></label>
                <div class="col-sm-10">
                  <div class="offer-chip-box" id="terms-chip-box">
                    {% if terms %}
                    {% for term in terms|split("\n") %}
                    {% set t = term|trim %}
                    {% if t %}
                    <span class="offer-chip" data-value="{{ t|e }}">{{ t|e }} <button type="button" class="offer-chip__remove" aria-label="Remove">&times;</button><input type="hidden" name="terms[]" value="{{ t|e }}" /></span>
                    {% endif %}
                    {% endfor %}
                    {% endif %}
                    <input type="text" class="offer-chip-input" id="terms-chip-input" placeholder="{{ entry_terms }}" autocomplete="off" />
                  </div>
                  <p class="help-block">Press Enter or comma to add. Click × to remove.</p>
                </div>
              </div>
```

- [ ] **Step 3: Add CSS for chip boxes (inline style block or in the existing `<style>` if one exists)**

Add inside a `<style>` block near the top of the script section:
```css
.offer-chip-box {
  min-height: 38px;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 4px 8px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  background: #fff;
  cursor: text;
}
.offer-chip-box:focus-within { border-color: #66afe9; box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); }
.offer-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: #edf0ff;
  border: 1px solid #c5cef5;
  border-radius: 12px;
  padding: 2px 8px;
  font-size: 12px;
  color: #3749bb;
}
.offer-chip__remove {
  border: none;
  background: none;
  color: #888;
  font-size: 14px;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}
.offer-chip-input {
  border: none;
  outline: none;
  font-size: 13px;
  min-width: 80px;
  flex: 1;
  padding: 2px 4px;
}
```

- [ ] **Step 4: Add JS for Terms chip input**

```javascript
/* ── Generic chip-box factory ── */
function initChipBox(boxId, inputId) {
  var box   = document.getElementById(boxId);
  var input = document.getElementById(inputId);
  if (!box || !input) { return; }

  function addChip(value) {
    value = value.trim();
    if (!value) { return; }
    if (box.querySelector('[data-value="' + value.replace(/"/g, '\\"') + '"]')) { return; }
    var chip = document.createElement('span');
    chip.className = 'offer-chip';
    chip.setAttribute('data-value', value);
    chip.innerHTML = escapeHtml(value) +
      ' <button type="button" class="offer-chip__remove" aria-label="Remove">&times;</button>' +
      '<input type="hidden" name="' + input.getAttribute('data-name') + '" value="' + escapeHtml(value) + '" />';
    chip.querySelector('.offer-chip__remove').addEventListener('click', function() { chip.parentNode.removeChild(chip); });
    box.insertBefore(chip, input);
    input.value = '';
  }

  box.querySelectorAll('.offer-chip__remove').forEach(function(btn) {
    btn.addEventListener('click', function() { btn.closest('.offer-chip').remove(); });
  });

  input.addEventListener('keydown', function(e) {
    if (e.key === 'Enter' || e.key === ',') {
      e.preventDefault();
      addChip(input.value);
    } else if (e.key === 'Backspace' && input.value === '') {
      var chips = box.querySelectorAll('.offer-chip');
      if (chips.length) { chips[chips.length - 1].remove(); }
    }
  });

  box.addEventListener('click', function() { input.focus(); });
}

/* Set data-name on chip inputs */
document.getElementById('terms-chip-input') && document.getElementById('terms-chip-input').setAttribute('data-name', 'terms[]');
initChipBox('terms-chip-box', 'terms-chip-input');
```

**Note:** `escapeHtml` is defined at the top of the script block — reuse the existing function.

- [ ] **Step 5: Commit**

```bash
git add admin/view/template/extension/module/offer_manager_form.twig
git commit -m "feat(admin): replace terms textarea with tag-chip input"
```

---

## Task 4: Hero Badges tag-chip input

**Files:**
- Modify: `admin/view/template/extension/module/offer_manager_form.twig`

- [ ] **Step 1: Find current hero_badges textarea**

```twig
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-hero-badges"><span data-toggle="tooltip" title="{{ help_hero_badges }}">{{ entry_hero_badges }}</span></label>
                <div class="col-sm-10">
                  <textarea name="hero_badges" rows="4" placeholder="{{ entry_hero_badges }}" id="input-hero-badges" class="form-control">{{ hero_badges }}</textarea>
                </div>
```

- [ ] **Step 2: Replace with chip widget**

```twig
              <div class="form-group">
                <label class="col-sm-2 control-label"><span data-toggle="tooltip" title="{{ help_hero_badges }}">{{ entry_hero_badges }}</span></label>
                <div class="col-sm-10">
                  <div class="offer-chip-box" id="hero-badges-chip-box">
                    {% if hero_badges %}
                    {% for badge in hero_badges|split("\n") %}
                    {% set b = badge|trim %}
                    {% if b %}
                    <span class="offer-chip" data-value="{{ b|e }}">{{ b|e }} <button type="button" class="offer-chip__remove" aria-label="Remove">&times;</button><input type="hidden" name="hero_badges[]" value="{{ b|e }}" /></span>
                    {% endif %}
                    {% endfor %}
                    {% endif %}
                    <input type="text" class="offer-chip-input" id="hero-badges-chip-input" placeholder="{{ entry_hero_badges }}" autocomplete="off" />
                  </div>
                  <p class="help-block">Each chip = one badge pill on the offer hero section.</p>
                </div>
              </div>
```

- [ ] **Step 3: Add JS init**

```javascript
document.getElementById('hero-badges-chip-input') && document.getElementById('hero-badges-chip-input').setAttribute('data-name', 'hero_badges[]');
initChipBox('hero-badges-chip-box', 'hero-badges-chip-input');
```

- [ ] **Step 4: Update PHP controller to handle `hero_badges[]` array**

In `admin/controller/extension/module/offer_manager.php`, find where `hero_badges` is read from POST. It currently reads a textarea string. Change to handle both array and string (backwards compatible):

Find something like:
```php
$data['hero_badges'] = $this->request->post['hero_badges'];
```
Or wherever `hero_badges` is extracted. Update so that if it's an array, join with newline:
```php
$hero_badges_raw = isset($this->request->post['hero_badges']) ? $this->request->post['hero_badges'] : '';
$data['hero_badges'] = is_array($hero_badges_raw) ? implode("\n", array_filter(array_map('trim', $hero_badges_raw))) : (string)$hero_badges_raw;
```
Apply same pattern to `terms`.

- [ ] **Step 5: Commit**

```bash
git add admin/view/template/extension/module/offer_manager_form.twig
git add admin/controller/extension/module/offer_manager.php
git commit -m "feat(admin): replace hero_badges and terms textareas with tag-chip inputs"
```

---

## Task 5: Images tab — CSS grid card redesign

**Files:**
- Modify: `admin/view/template/extension/module/offer_manager_form.twig`

- [ ] **Step 1: Find current images tab table**

The images tab (lines ~141–175) uses a `<table id="offer-images">` with thead/tbody/tfoot.

- [ ] **Step 2: Replace table with card grid**

Replace the entire `<div class="tab-pane" id="tab-images">` content with:
```twig
            <div class="tab-pane" id="tab-images">
              <p class="help-block">{{ text_image_help }} Drag cards to reorder.</p>
              <div id="offer-images-grid" class="offer-img-grid">
                {% set image_row = 0 %}
                {% for offer_image in offer_images %}
                <div class="offer-img-card" draggable="true" data-img-row="{{ image_row }}">
                  <div class="offer-img-card__thumb" data-toggle="image" id="thumb-image{{ image_row }}" title="Click to change">
                    <img src="{{ offer_image.thumb }}" alt="" data-placeholder="{{ placeholder }}" />
                  </div>
                  <input type="hidden" name="offer_image[{{ image_row }}][image]" value="{{ offer_image.image }}" id="input-image{{ image_row }}" />
                  <input type="text" name="offer_image[{{ image_row }}][link]" value="{{ offer_image.link }}" placeholder="{{ entry_link }}" class="offer-img-card__link form-control input-sm" />
                  <input type="hidden" name="offer_image[{{ image_row }}][sort_order]" value="{{ image_row }}" class="offer-img-card__sort" />
                  <div class="offer-img-card__foot">
                    <span class="offer-img-card__drag">&#8286; drag</span>
                    <button type="button" class="btn btn-danger btn-xs btn-remove-image" data-row="{{ image_row }}"><i class="fa fa-times"></i></button>
                  </div>
                </div>
                {% set image_row = image_row + 1 %}
                {% endfor %}
                <div class="offer-img-card offer-img-card--add" id="btn-add-image-card" title="{{ button_add_image }}">
                  <div class="offer-img-card__add-icon">+<br /><small>{{ button_add_image }}</small></div>
                </div>
              </div>
            </div>
```

- [ ] **Step 3: Add CSS for image card grid**

Add to the `<style>` block:
```css
.offer-img-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  padding: 10px 0;
}
.offer-img-card {
  border: 1px solid #dde;
  border-radius: 4px;
  overflow: hidden;
  background: #fff;
  cursor: default;
}
.offer-img-card.drag-over { border: 2px dashed #3749bb; }
.offer-img-card__thumb {
  background: #f0f2f8;
  min-height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.offer-img-card__thumb img { max-width: 100%; max-height: 80px; object-fit: contain; }
.offer-img-card__link {
  width: 100%;
  border: none;
  border-top: 1px solid #eee;
  border-radius: 0;
  font-size: 11px;
}
.offer-img-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 4px 8px;
  border-top: 1px solid #eee;
  font-size: 11px;
  color: #888;
}
.offer-img-card__drag { cursor: move; }
.offer-img-card--add {
  border: 2px dashed #c5cef5;
  background: #f8f9ff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 110px;
}
.offer-img-card__add-icon { text-align: center; color: #3749bb; font-size: 24px; line-height: 1.2; }
```

- [ ] **Step 4: Update addOfferImage() JS function**

Replace the existing `addOfferImage()` function with:
```javascript
function addOfferImage() {
  var grid    = document.getElementById('offer-images-grid');
  var addCard = document.getElementById('btn-add-image-card');
  var card    = document.createElement('div');
  card.className   = 'offer-img-card';
  card.draggable   = true;
  card.setAttribute('data-img-row', image_row);
  card.innerHTML =
    '<div class="offer-img-card__thumb" data-toggle="image" id="thumb-image' + image_row + '" title="Click to change">' +
    '  <img src="' + heroPlaceholder + '" alt="" data-placeholder="' + heroPlaceholder + '" />' +
    '</div>' +
    '<input type="hidden" name="offer_image[' + image_row + '][image]" value="" id="input-image' + image_row + '" />' +
    '<input type="text" name="offer_image[' + image_row + '][link]" value="" placeholder="{{ entry_link }}" class="offer-img-card__link form-control input-sm" />' +
    '<input type="hidden" name="offer_image[' + image_row + '][sort_order]" value="' + image_row + '" class="offer-img-card__sort" />' +
    '<div class="offer-img-card__foot">' +
    '  <span class="offer-img-card__drag">&#8286; drag</span>' +
    '  <button type="button" class="btn btn-danger btn-xs btn-remove-image" data-row="' + image_row + '"><i class="fa fa-times"></i></button>' +
    '</div>';
  grid.insertBefore(card, addCard);
  initImageCardDrag(card);
  image_row++;
  /* Trigger OpenCart image picker binding refresh */
  if (typeof Framework !== 'undefined') { Framework.start(); }
}
```

- [ ] **Step 5: Add drag-to-reorder JS for image cards + reindex on submit**

```javascript
/* ── Image card drag-to-reorder ── */
var imgDragSrc = null;

function initImageCardDrag(card) {
  card.addEventListener('dragstart', function(e) {
    imgDragSrc = card;
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', '');
  });
  card.addEventListener('dragover', function(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    card.classList.add('drag-over');
  });
  card.addEventListener('dragleave', function() { card.classList.remove('drag-over'); });
  card.addEventListener('drop', function(e) {
    e.preventDefault();
    card.classList.remove('drag-over');
    if (imgDragSrc && imgDragSrc !== card) {
      var grid = document.getElementById('offer-images-grid');
      var cards = Array.from(grid.querySelectorAll('.offer-img-card:not(.offer-img-card--add)'));
      var fromIdx = cards.indexOf(imgDragSrc);
      var toIdx   = cards.indexOf(card);
      if (fromIdx > toIdx) { grid.insertBefore(imgDragSrc, card); }
      else { grid.insertBefore(imgDragSrc, card.nextSibling); }
    }
  });
}

document.querySelectorAll('#offer-images-grid .offer-img-card:not(.offer-img-card--add)').forEach(initImageCardDrag);
document.getElementById('btn-add-image-card').addEventListener('click', addOfferImage);

/* Reindex sort_order inputs before form submit */
document.getElementById('form-offer').addEventListener('submit', function() {
  document.querySelectorAll('#offer-images-grid .offer-img-card:not(.offer-img-card--add)').forEach(function(card, idx) {
    var sortInput = card.querySelector('.offer-img-card__sort');
    if (sortInput) { sortInput.value = idx; }
  });
});
```

- [ ] **Step 6: Wire btn-remove-image for grid (event delegation)**

Replace or supplement existing `.btn-remove-image` click handler. The old handler uses `$('#offer-images tbody')` which no longer exists. Add:
```javascript
document.getElementById('offer-images-grid').addEventListener('click', function(e) {
  var btn = e.target.closest('.btn-remove-image');
  if (btn) {
    var card = btn.closest('.offer-img-card');
    if (card) { card.parentNode.removeChild(card); }
  }
});
```

- [ ] **Step 7: Commit**

```bash
git add admin/view/template/extension/module/offer_manager_form.twig
git commit -m "feat(admin): redesign images tab as draggable card grid"
```

---

## Task 6: Products tab — Categories and Manufacturers chip display

**Files:**
- Modify: `admin/view/template/extension/module/offer_manager_form.twig`

- [ ] **Step 1: Find current categories section**

Lines ~225–237:
```twig
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-category">...
                  <input type="text" name="category_name" ... id="input-category" class="form-control" />
                  <div id="offer-category" class="well well-sm" style="height: 160px; overflow: auto;">
                    {% for category in offer_categories %}
                    <div id="offer-category{{ category.category_id }}"><i class="fa fa-minus-circle"></i> {{ category.name }}
                      <input type="hidden" name="offer_category[]" value="{{ category.category_id }}" />
                    </div>
                    {% endfor %}
                  </div>
```

- [ ] **Step 2: Replace categories section with chip display**

```twig
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-category"><span data-toggle="tooltip" title="{{ help_category }}">{{ entry_category }}</span></label>
                <div class="col-sm-10">
                  <input type="text" name="category_name" value="" placeholder="{{ entry_category }}" id="input-category" class="form-control" style="margin-bottom:6px;" />
                  <div id="offer-category" class="offer-tag-chip-container">
                    {% for category in offer_categories %}
                    <span class="offer-tag-chip" id="offer-category{{ category.category_id }}">{{ category.name }} <button type="button" class="offer-tag-chip__remove" data-id="{{ category.category_id }}" aria-label="Remove">&times;</button><input type="hidden" name="offer_category[]" value="{{ category.category_id }}" /></span>
                    {% endfor %}
                  </div>
                </div>
              </div>
```

- [ ] **Step 3: Replace manufacturers section with chip display**

```twig
              <div class="form-group">
                <label class="col-sm-2 control-label" for="input-manufacturer"><span data-toggle="tooltip" title="{{ help_manufacturer }}">{{ entry_manufacturer }}</span></label>
                <div class="col-sm-10">
                  <input type="text" name="manufacturer_name" value="" placeholder="{{ entry_manufacturer }}" id="input-manufacturer" class="form-control" style="margin-bottom:6px;" />
                  <div id="offer-manufacturer" class="offer-tag-chip-container">
                    {% for manufacturer in offer_manufacturers %}
                    <span class="offer-tag-chip" id="offer-manufacturer{{ manufacturer.manufacturer_id }}">{{ manufacturer.name }} <button type="button" class="offer-tag-chip__remove" data-id="{{ manufacturer.manufacturer_id }}" aria-label="Remove">&times;</button><input type="hidden" name="offer_manufacturer[]" value="{{ manufacturer.manufacturer_id }}" /></span>
                    {% endfor %}
                  </div>
                </div>
              </div>
```

- [ ] **Step 4: Add CSS for tag chip containers**

```css
.offer-tag-chip-container {
  min-height: 36px;
  padding: 4px 6px;
  border: 1px solid #ddd;
  border-radius: 4px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  background: #fafafa;
}
.offer-tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  background: #edf0ff;
  border: 1px solid #c5cef5;
  border-radius: 12px;
  padding: 2px 8px;
  font-size: 12px;
  color: #3749bb;
}
.offer-tag-chip__remove {
  border: none;
  background: none;
  color: #888;
  font-size: 14px;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}
```

- [ ] **Step 5: Update autocomplete select callbacks to add chips**

Find the `$('#input-category').autocomplete` block and update its `select` callback:
```javascript
$('#input-category').autocomplete({
  source: function(request, response) {
    $.ajax({
      url: 'index.php?route=catalog/category/autocomplete&user_token={{ user_token }}&filter_name=' + encodeURIComponent(request.term),
      dataType: 'json',
      success: function(json) {
        response($.map(json, function(item) {
          return { label: item['name'], value: item['category_id'] };
        }));
      }
    });
  },
  select: function(event, ui) {
    event.preventDefault();
    $('#input-category').val('');
    var id = ui.item.value;
    var name = ui.item.label;
    if ($('#offer-category' + id).length) { return; }
    var chip = $('<span class="offer-tag-chip" id="offer-category' + id + '">' +
      $('<div/>').text(name).html() +
      ' <button type="button" class="offer-tag-chip__remove" data-id="' + id + '" aria-label="Remove">&times;</button>' +
      '<input type="hidden" name="offer_category[]" value="' + id + '" /></span>');
    $('#offer-category').append(chip);
  }
});
```

Do the same for `$('#input-manufacturer').autocomplete` (use `offer_manufacturer` container, `offer_manufacturer[]` name, `manufacturer_id`).

Also update `$('#input-product').autocomplete` `source` function to use `request.term`:
```javascript
url: 'index.php?route=catalog/product/autocomplete&user_token={{ user_token }}&filter_name=' + encodeURIComponent(request.term),
```

- [ ] **Step 6: Add event delegation for chip remove buttons**

```javascript
$(document).on('click', '.offer-tag-chip__remove', function() {
  $(this).closest('.offer-tag-chip').remove();
});
```

- [ ] **Step 7: Commit**

```bash
git add admin/view/template/extension/module/offer_manager_form.twig
git commit -m "feat(admin): replace category/manufacturer well lists with chip tag display"
```

---

## Task 7: Group panel — inline search with dropdown + drag-to-reorder

This is the largest single task. Replace the current basic group panel with Option A from the spec.

**Files:**
- Modify: `admin/view/template/extension/module/offer_manager_form.twig`

- [ ] **Step 1: Replace existing group panel Twig markup**

Find the current group panel block (lines ~198–222):
```twig
                  <div id="offer-groups">
                    {% set group_row = 0 %}
                    {% for group in offer_groups %}
                    <div class="offer-group-panel" data-group-row="{{ group_row }}">
                      ...
                    </div>
                    {% endfor %}
                  </div>
```

Replace with:
```twig
                  <div id="offer-groups">
                    {% set group_row = 0 %}
                    {% for group in offer_groups %}
                    <div class="offer-group-panel" data-group-row="{{ group_row }}" draggable="true">
                      <div class="offer-group-panel__head">
                        <span class="offer-group-panel__drag" title="Drag to reorder">&#8286;</span>
                        <input type="text" name="offer_group[{{ group_row }}][name]" value="{{ group.name }}" placeholder="{{ entry_group_name }}" class="form-control offer-group-panel__name" />
                        <span class="offer-group-panel__sort-badge">Sort: {{ group.sort_order }}</span>
                        <button type="button" class="btn btn-danger btn-sm btn-remove-group">&#10005; Remove</button>
                      </div>
                      <div class="offer-group-panel__search-wrap">
                        <span class="offer-group-panel__search-icon">&#128269;</span>
                        <input type="text" value="" placeholder="{{ entry_group_products }}" class="offer-group-product-search" autocomplete="off" />
                        <div class="offer-group-panel__dropdown" hidden></div>
                      </div>
                      <div class="offer-group-products">
                        {% for product in group.products %}
                        <div class="offer-group-product-item" id="offer-group{{ group_row }}-product{{ product.product_id }}" draggable="true">
                          <span class="offer-group-product-item__drag">&#8286;</span>
                          <span class="offer-group-product-item__name">{{ product.name }}</span>
                          <span style="margin-left:auto;font-size:11px;color:#999">ID:{{ product.product_id }}</span>
                          <button type="button" class="offer-group-product-item__remove">&times;</button>
                          <input type="hidden" name="offer_group[{{ group_row }}][product][]" value="{{ product.product_id }}" />
                        </div>
                        {% endfor %}
                      </div>
                      <div class="offer-group-panel__footer">
                        <span class="offer-group-panel__product-count">{{ group.products|length }} products &bull; drag to reorder</span>
                        <input type="hidden" name="offer_group[{{ group_row }}][sort_order]" value="{{ group.sort_order }}" class="offer-group-sort-input" />
                      </div>
                    </div>
                    {% set group_row = group_row + 1 %}
                    {% endfor %}
                  </div>
```

- [ ] **Step 2: Add group panel CSS**

```css
.offer-group-panel {
  background: #fafafa;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-bottom: 10px;
  overflow: visible;
}
.offer-group-panel.drag-over { border: 2px dashed #3749bb; }
.offer-group-panel__head {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f4f6fb;
  border-bottom: 1px solid #e2e6ef;
  padding: 7px 10px;
}
.offer-group-panel__drag { cursor: move; color: #aaa; font-size: 16px; }
.offer-group-panel__name { flex: 1; }
.offer-group-panel__sort-badge {
  font-size: 11px;
  color: #888;
  background: #f0f2f8;
  padding: 2px 8px;
  border-radius: 3px;
  white-space: nowrap;
}
.offer-group-panel__search-wrap {
  position: relative;
  padding: 8px 10px 4px;
  display: flex;
  align-items: center;
}
.offer-group-panel__search-icon { position: absolute; left: 20px; color: #3749bb; }
.offer-group-product-search {
  width: 100%;
  border: 1px solid #3749bb;
  border-radius: 4px;
  padding: 5px 10px 5px 28px;
  font-size: 13px;
  outline: none;
}
.offer-group-panel__dropdown {
  position: absolute;
  top: 100%;
  left: 10px;
  right: 10px;
  background: #fff;
  border: 1px solid #c8d0e7;
  border-radius: 0 0 4px 4px;
  z-index: 100;
  box-shadow: 0 4px 12px rgba(0,0,0,.1);
  max-height: 180px;
  overflow-y: auto;
}
.offer-group-panel__dropdown-item {
  padding: 6px 10px;
  font-size: 12px;
  border-bottom: 1px solid #f0f2f8;
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}
.offer-group-panel__dropdown-item:last-child { border-bottom: none; }
.offer-group-panel__dropdown-item:hover { background: #f8f9ff; }
.offer-group-panel__dropdown-item--dimmed { opacity: 0.4; }
.offer-group-panel__dropdown-item__info { flex: 1; }
.offer-group-panel__dropdown-item__name { font-weight: 600; }
.offer-group-panel__dropdown-item__id { font-size: 10px; color: #999; }
.offer-group-products {
  max-height: 160px;
  overflow-y: auto;
  border: 1px solid #e2e6ef;
  border-radius: 4px;
  margin: 0 10px;
}
.offer-group-product-item {
  padding: 5px 8px;
  display: flex;
  align-items: center;
  gap: 6px;
  border-bottom: 1px solid #f0f2f8;
  background: #fff;
  font-size: 12px;
}
.offer-group-product-item:last-child { border-bottom: none; }
.offer-group-product-item.drag-over { border-top: 2px solid #3749bb; }
.offer-group-product-item__drag { cursor: move; color: #ccc; font-size: 14px; }
.offer-group-product-item__name { flex: 1; font-weight: 500; }
.offer-group-product-item__remove {
  border: none;
  background: none;
  color: #e74c3c;
  font-size: 16px;
  cursor: pointer;
  padding: 0;
  line-height: 1;
}
.offer-group-panel__footer {
  padding: 5px 10px 8px;
  font-size: 11px;
  color: #888;
}
```

- [ ] **Step 3: Replace initOfferGroupProductAutocomplete() with new inline-search implementation**

Replace the existing `initOfferGroupProductAutocomplete` function and `addOfferGroup` function entirely with:

```javascript
function getGroupProductIds(panel) {
  var ids = {};
  panel.querySelectorAll('.offer-group-product-item input[type="hidden"]').forEach(function(inp) {
    ids[String(inp.value)] = true;
  });
  return ids;
}

function buildGroupProductRow(groupRow, productId, productName) {
  var div = document.createElement('div');
  div.className = 'offer-group-product-item';
  div.id = 'offer-group' + groupRow + '-product' + productId;
  div.draggable = true;
  div.innerHTML =
    '<span class="offer-group-product-item__drag">&#8286;</span>' +
    '<span class="offer-group-product-item__name">' + $('<div/>').text(productName).html() + '</span>' +
    '<span style="margin-left:auto;font-size:11px;color:#999">ID:' + productId + '</span>' +
    '<button type="button" class="offer-group-product-item__remove">&times;</button>' +
    '<input type="hidden" name="offer_group[' + groupRow + '][product][]" value="' + productId + '" />';
  initGroupProductDrag(div);
  div.querySelector('.offer-group-product-item__remove').addEventListener('click', function() {
    div.parentNode.removeChild(div);
    updateGroupProductCount(div.closest('.offer-group-panel') || document.getElementById('offer-groups'));
  });
  return div;
}

function updateGroupProductCount(panel) {
  if (!panel) { return; }
  var count = panel.querySelectorAll('.offer-group-product-item').length;
  var footer = panel.querySelector('.offer-group-panel__product-count');
  if (footer) { footer.textContent = count + ' products • drag to reorder'; }
}

function initGroupProductDrag(item) {
  var dragSrc = null;
  item.addEventListener('dragstart', function(e) { dragSrc = item; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); });
  item.addEventListener('dragover', function(e) { e.preventDefault(); item.classList.add('drag-over'); });
  item.addEventListener('dragleave', function() { item.classList.remove('drag-over'); });
  item.addEventListener('drop', function(e) {
    e.preventDefault();
    e.stopPropagation();
    item.classList.remove('drag-over');
    if (dragSrc && dragSrc !== item) {
      var list = item.parentNode;
      var items = Array.from(list.querySelectorAll('.offer-group-product-item'));
      var from = items.indexOf(dragSrc), to = items.indexOf(item);
      if (from > to) { list.insertBefore(dragSrc, item); }
      else { list.insertBefore(dragSrc, item.nextSibling); }
    }
  });
}

function initGroupSearch(panel) {
  var input    = panel.querySelector('.offer-group-product-search');
  var dropdown = panel.querySelector('.offer-group-panel__dropdown');
  var list     = panel.querySelector('.offer-group-products');
  var groupRow = panel.getAttribute('data-group-row');
  var timer    = null;

  if (!input || !dropdown) { return; }

  input.addEventListener('input', function() {
    clearTimeout(timer);
    var term = input.value.trim();
    if (!term) { dropdown.hidden = true; dropdown.innerHTML = ''; return; }
    timer = setTimeout(function() {
      $.ajax({
        url: 'index.php?route=catalog/product/autocomplete&user_token={{ user_token }}&filter_name=' + encodeURIComponent(term),
        dataType: 'json',
        success: function(json) {
          var existing = getGroupProductIds(panel);
          dropdown.innerHTML = '';
          if (!json.length) {
            dropdown.innerHTML = '<div class="offer-group-panel__dropdown-item" style="color:#999">No products found</div>';
          } else {
            json.forEach(function(item) {
              var isDimmed = !!existing[String(item.product_id)];
              var row = document.createElement('div');
              row.className = 'offer-group-panel__dropdown-item' + (isDimmed ? ' offer-group-panel__dropdown-item--dimmed' : '');
              row.innerHTML =
                '<div class="offer-group-panel__dropdown-item__info">' +
                '  <div class="offer-group-panel__dropdown-item__name">' + $('<div/>').text(item.name).html() + '</div>' +
                '  <div class="offer-group-panel__dropdown-item__id">Product ID: ' + item.product_id + '</div>' +
                '</div>' +
                '<button type="button" class="btn btn-primary btn-xs">+ Add</button>';
              row.querySelector('button').addEventListener('click', function() {
                if (isDimmed) { return; }
                $('#offer-group' + groupRow + '-product' + item.product_id).remove();
                list.appendChild(buildGroupProductRow(groupRow, item.product_id, item.name));
                updateGroupProductCount(panel);
                var existingNow = getGroupProductIds(panel);
                row.classList.toggle('offer-group-panel__dropdown-item--dimmed', !!existingNow[String(item.product_id)]);
                isDimmed = true;
              });
              dropdown.appendChild(row);
            });
          }
          dropdown.hidden = false;
        },
        error: function() { dropdown.hidden = true; }
      });
    }, 200);
  });

  input.addEventListener('blur', function() {
    setTimeout(function() { dropdown.hidden = true; }, 200);
  });
  input.addEventListener('focus', function() {
    if (dropdown.innerHTML && input.value.trim()) { dropdown.hidden = false; }
  });
}

var groupDragSrc = null;

function initGroupPanelDrag(panel) {
  panel.addEventListener('dragstart', function(e) {
    if (e.target.closest('.offer-group-product-item') || e.target.closest('.offer-group-product-search')) { return; }
    groupDragSrc = panel;
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', '');
  });
  panel.addEventListener('dragover', function(e) {
    if (e.target.closest('.offer-group-products')) { return; }
    e.preventDefault();
    panel.classList.add('drag-over');
  });
  panel.addEventListener('dragleave', function() { panel.classList.remove('drag-over'); });
  panel.addEventListener('drop', function(e) {
    if (e.target.closest('.offer-group-products')) { return; }
    e.preventDefault();
    panel.classList.remove('drag-over');
    if (groupDragSrc && groupDragSrc !== panel) {
      var container = document.getElementById('offer-groups');
      var panels = Array.from(container.querySelectorAll('.offer-group-panel'));
      var from = panels.indexOf(groupDragSrc), to = panels.indexOf(panel);
      if (from > to) { container.insertBefore(groupDragSrc, panel); }
      else { container.insertBefore(groupDragSrc, panel.nextSibling); }
    }
  });
}

function initGroupPanel(panel) {
  initGroupSearch(panel);
  initGroupPanelDrag(panel);
  panel.querySelectorAll('.offer-group-product-item').forEach(initGroupProductDrag);
  panel.querySelectorAll('.offer-group-product-item__remove').forEach(function(btn) {
    btn.addEventListener('click', function() {
      var item = btn.closest('.offer-group-product-item');
      item.parentNode.removeChild(item);
      updateGroupProductCount(panel);
    });
  });
  panel.querySelector('.btn-remove-group').addEventListener('click', function() {
    panel.parentNode.removeChild(panel);
  });
}

document.querySelectorAll('.offer-group-panel').forEach(initGroupPanel);

function addOfferGroup() {
  var container = document.getElementById('offer-groups');
  var panel = document.createElement('div');
  panel.className = 'offer-group-panel';
  panel.draggable = true;
  panel.setAttribute('data-group-row', group_row);
  panel.innerHTML =
    '<div class="offer-group-panel__head">' +
    '  <span class="offer-group-panel__drag" title="Drag to reorder">&#8286;</span>' +
    '  <input type="text" name="offer_group[' + group_row + '][name]" value="" placeholder="{{ entry_group_name }}" class="form-control offer-group-panel__name" />' +
    '  <span class="offer-group-panel__sort-badge">Sort: ' + group_row + '</span>' +
    '  <button type="button" class="btn btn-danger btn-sm btn-remove-group">&#10005; Remove</button>' +
    '</div>' +
    '<div class="offer-group-panel__search-wrap">' +
    '  <span class="offer-group-panel__search-icon">&#128269;</span>' +
    '  <input type="text" value="" placeholder="{{ entry_group_products }}" class="offer-group-product-search" autocomplete="off" />' +
    '  <div class="offer-group-panel__dropdown" hidden></div>' +
    '</div>' +
    '<div class="offer-group-products"></div>' +
    '<div class="offer-group-panel__footer">' +
    '  <span class="offer-group-panel__product-count">0 products &bull; drag to reorder</span>' +
    '  <input type="hidden" name="offer_group[' + group_row + '][sort_order]" value="' + group_row + '" class="offer-group-sort-input" />' +
    '</div>';
  container.appendChild(panel);
  initGroupPanel(panel);
  group_row++;
}

document.getElementById('button-add-offer-group').addEventListener('click', addOfferGroup);
```

- [ ] **Step 4: Remove old initOfferGroupProductAutocomplete and old addOfferGroup functions**

Delete the following old functions from the script block:
- `initOfferGroupProductAutocomplete(context)` 
- Old `addOfferGroup()` function
- Old `buildOfferGroupProductRow(groupRow, productId, productName)` function
- The old `$('#button-add-offer-group').click(...)` or `onclick="addOfferGroup()"` call (now handled by event listener)

- [ ] **Step 5: Update reindex on form submit for groups**

Add to the submit handler:
```javascript
document.querySelectorAll('.offer-group-panel').forEach(function(panel, idx) {
  var sortInput = panel.querySelector('.offer-group-sort-input');
  if (sortInput) { sortInput.value = idx; }
  var badge = panel.querySelector('.offer-group-panel__sort-badge');
  if (badge) { badge.textContent = 'Sort: ' + idx; }
});
```

- [ ] **Step 6: Verify old autocomplete syntax replaced**

Run: `grep -n "encodeURIComponent(request)" /var/www/skyland/src/admin/view/template/extension/module/offer_manager_form.twig`
Expected: zero results (all now use `request.term` or direct term variable).

- [ ] **Step 7: Commit**

```bash
git add admin/view/template/extension/module/offer_manager_form.twig
git commit -m "feat(admin): redesign group panel with inline product search, dropdown, drag-to-reorder"
```
