# Offer Manager Redesign — Design Spec
Date: 2026-05-29

## Scope

Three parallel workstreams:
1. **Bug fixes** — 7 confirmed issues from code review
2. **Admin UI redesign** — group management UX + product search fix
3. **Frontend redesign** — offer detail page sidebar layout + category page with filters

---

## 1. Bug Fixes

### 1.1 Products beyond initial_limit permanently invisible
**Fix:** Remove `initial_limit` concept entirely. Show all products on offer detail page. The sidebar category navigation replaces the need for pagination — users click a category to get the full filtered page. Delete `offer-product-layout--hidden` CSS class and the `{% if loop.index > initial_limit %}` conditional from both theme templates. Remove `$data['initial_limit']` from the controller.

### 1.2 XHR error leaves "Searching…" forever
**Fix:** Add `else` branch to `onreadystatechange` handlers in `offers_info.twig` and `offers_category.twig` (both themes). On non-2xx status: clear `status.textContent`, call `renderProducts([])`.

### 1.3 Category page accessible for expired/upcoming offers; search returns empty
**Fix:** Add state guard in `category()` controller. If `getOfferState() !== 'active'`, return 404. Consistent with `search()` which already guards on active state.

### 1.4 Hardcoded English strings
**Fix:** Add language keys:
- `text_load_more` → "Load More"
- `text_products_found` → "{{count}} product(s) found" (JS replaces `{{count}}` with actual number)
Both in `catalog/language/en-gb/extension/module/offers.php`. Injected via Twig `json_encode|raw` like `textSearching`.

### 1.5 Gallery/description render before "coming soon" block on upcoming offers
**Fix:** Wrap gallery `{% if offer_images %}` and description `{% if offer.description %}` blocks in `{% if offer.state != 'upcoming' %}` guard in both theme `offers_info.twig` templates.

### 1.6 Global badges missing from offer product cards
**Fix:** `buildOfferProducts()` to attach `global_badges` array from `getProductBadges()`. Render badge stack in product card markup in both `offers_info.twig` and `offers_category.twig` templates. Same rendering as `product.twig`.

### 1.7 Group products no drag-to-reorder
**Fix:** Absorbed into admin UI redesign (section 2).

---

## 2. Admin UI Redesign

### 2.1 Autocomplete search fixes (all fields)

**Two bugs affect all autocomplete inputs (products, categories, manufacturers, group products):**

1. **Wrong term extraction** — `encodeURIComponent(request)` in every `source` function should be `encodeURIComponent(request.term)`. jQuery UI passes `request` as `{term: "..."}` not a string. Current code sends `%5Bobject%20Object%5D` → zero results.
2. **Prefix-only match** — `admin/model/catalog/product.php` lines 388–389, 667–668 use `LIKE 'name%'` → change to `LIKE '%name%'`. Same fix applies wherever category/manufacturer models do name LIKE searches.

Fix both in: `admin/view/template/extension/module/offer_manager_form.twig` (all `source:` functions) + admin product/category/manufacturer models.

### 2.2 Group panel — new design (Option A)

Each group panel has:

**Header row:**
- Drag handle (⠿ icon, `cursor:move`)
- Text input for group name
- Sort order badge (small, secondary)
- Red "✕ Remove" button

**Product search:**
- Full-width search input with search icon
- On `input` (debounced 200ms): AJAX to `catalog/product/autocomplete` with `filter_name=%term%`
- Results dropdown below input: each row = thumbnail placeholder + product name + product ID + blue "+ Add" button
- Clicking "+ Add" appends product to the list below WITHOUT clearing search input (so user can keep adding)
- Already-added products are visually dimmed or hidden from dropdown

**Product list:**
- Scrollable container (max-height 160px)
- Each row: drag handle + thumbnail + product name + ✕ remove
- Full HTML5 drag-and-drop sortable (same pattern as existing `offer-product` list)
- Product count label below: "N products • drag to reorder"

**Group-level drag:**
- Each `.offer-group-panel` gets `draggable="true"`
- Implement same HTML5 dragstart/dragover/drop pattern as product rows
- `data-group-row` attribute preserved through drag

### 2.3 Visual style
Match Bootstrap 3 admin theme. Use existing `form-control`, `btn-danger`, `btn-primary` classes where appropriate. Inline CSS only for layout not covered by Bootstrap. Panel background `#fafafa`, border `1px solid #ddd`, border-radius `4px`.

### 2.4 General tab — field redesigns

**Terms field** (currently `<textarea>` one-per-line):
- Replace with tag-chip input: visible chip pills with ×, plus an inline text input
- Enter key or comma adds new chip; × removes it
- Hidden `<input type="hidden">` per term synced on change (same name structure: `terms[]`)
- Chips display inline-flex with `background:#edf0ff; border-radius:12px`

**Hero Badges field** (currently `<textarea>` one-per-line):
- Same tag-chip pattern as Terms
- Each chip = one badge pill shown on offer hero section
- Hidden inputs: `hero_badges[]`

**Slug field:**
- Add "⚡ Auto-generate" button inline with input
- JS: strips title to lowercase slug on click (`title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')`)
- Only fills if slug field is currently empty or user confirms overwrite

**Status field** (currently `<select>`):
- Replace with Bootstrap segmented toggle: two `<button type="button">` styled active/inactive
- Hidden `<input name="status">` synced on click
- Active=`btn-primary`, inactive=`btn-default`

**Hero Image field:**
- No functional change, keep existing `data-toggle="image"` picker
- Minor: show current filename below thumbnail if set

### 2.5 Images tab — card grid redesign

Replace `<table>` with CSS grid of image cards (4-per-row):

Each card:
- Image thumbnail (clickable, triggers existing `data-toggle="image"` picker)
- Link URL text input below thumbnail (no separate column)
- Footer: drag handle (⠿) + ✕ remove button
- Sort order determined by DOM position (drag-to-reorder), no explicit sort_order input

Add card (last cell): dashed border, "+" icon, calls `addOfferImage()`.

Drag-to-reorder: HTML5 dragstart/dragover/drop on `#offer-images-grid .offer-image-card`, same pattern as product rows. On drop, re-index hidden `offer_image[N][...]` input names.

Hidden inputs per card: `offer_image[N][image]`, `offer_image[N][link]`, `offer_image[N][sort_order]` (set to DOM index on submit via JS).

### 2.6 Products tab — categories and manufacturers redesign

**Both fields** (currently: text autocomplete input + scrollable `<div>` with `<div>` children):

Replace the `<div>` container with a tag-chip display:
- Selected items shown as removable pill chips: `<span class="offer-tag-chip">Name <button>×</button></span>`
- Autocomplete input stays above the chip area
- Chips container: `min-height:36px; padding:4px 6px; border:1px solid #ddd; border-radius:4px; display:flex; flex-wrap:wrap; gap:4px`
- Hidden `<input type="hidden">` per chip (existing `offer_category[]` / `offer_manufacturer[]` names)
- Removing chip removes its hidden input

---

## 3. Frontend Redesign

### 3.1 Offer Detail Page — Sidebar Layout

**Layout:** Two-column. Left: sticky sidebar (220px). Right: main content area.

**Sidebar content (top to bottom):**
1. "Shop by Category" heading
2. Category list items — each shows name + product count badge. "All Products" first, then offer groups ordered by `sort_order`. Active category highlighted with accent color.
3. Coupon code block (if offer has coupon) — dashed border, click-to-copy.
4. Offer dates (start → end) in small text.

**Sidebar behavior:**
- `position: sticky; top: 0` so it stays in view while scrolling product grid
- Clicking "All Products" → no navigation, scrolls to product grid (AJAX reset to all products is removed; sidebar just keeps all visible)
- Clicking any group category → **navigates to `/offers/{slug}/category/{group_id}`** (full page)
- Active item = "All Products" on initial load

**Main content area:**
- Header bar (back link + offer title + countdown pill) — full width above the two-column split
- Meta row (dates, tag) — full width
- Coupon + terms row (if present) — full width
- Hero countdown block (if active) — full width
- Gallery strip (if images, only when state != upcoming) — full width
- Description (only when state != upcoming) — full width
- Then two-column split begins: sidebar left, products right
- Products section: search bar at top + 4-col product grid (all products, no hidden ones)

**Responsive:**
- `>= 992px`: sidebar visible, 220px fixed
- `< 992px`: sidebar collapses to horizontal scrollable pill list above product grid (same position as current pill nav but styled consistently)
- `< 576px`: 2-col product grid

### 3.2 Category Page — Filter Sidebar Layout

**URL:** `?route=extension/module/offers/category&offer_slug={slug}&category_id={group_id}&price_min=X&price_max=Y&manufacturer_id[]=N&attribute[attr_id][]=value`  
No clean SEO URL for category pages (seo_url.php currently treats path depth >2 as 404 for offers; extending it is out of scope).  
Filter state stored in URL query params. AJAX applies filters via pushState so URL stays shareable/bookmarkable.

**Page structure (top to bottom):**
1. Breadcrumb: Home > Offers > {Offer Title} > {Category Name}
2. Banner strip: gradient background, offer name + category name (left), countdown pill + product count (right)
3. Two-column: left filter sidebar (220px) + right product area

**Filter sidebar:**
- **Price Range:** Two number inputs (Min ৳ / Max ৳). No external slider library. "Apply" button triggers AJAX. Initial values from `getOfferGroupFilters()` min/max.
- **Brand (Manufacturer):** checkbox list, brand name + count. Only brands present in this group's products.
- **Attributes:** One section per attribute group that has >1 distinct value across group products. Checkbox per unique value. E.g. "Socket: [ ] AM4 (3) [ ] AM5 (4)".
- "Apply Filters" button + "Clear All" link.

**Product area:**
- Sort dropdown (Default / Price Low→High / Price High→Low / Name A→Z)
- Product count label: "Showing X of Y products"
- 4-col grid (3-col at `<860px`, 2-col at `<540px`)
- Each product card: standard `product-thumb` structure + sale badge + price

**Filter mechanics:**
- Filter state stored in URL query params (pushState on AJAX apply)
- AJAX endpoint: `extension/module/offers/search` — extend to accept `price_min`, `price_max`, `manufacturer_id[]`, `attribute[id][]` params
- On first load: server-renders products with active filters from URL params
- On filter change: AJAX replaces product grid + updates count label

### 3.3 New/Modified Backend

**`catalog/controller/extension/module/offers.php`:**
- `detail()`: remove `initial_limit`; add `shop_categories` data with correct hrefs; keep product search URL
- `category()`: add active-state guard (404 if not active); pass filter params from `$_GET` to `buildFilteredGroupProducts()`; pass `filters` data to view for sidebar rendering
- `search()`: accept `price_min`, `price_max`, `manufacturer_id[]`, `attribute[attr_id][]` params; pass to model filter method

**`catalog/model/extension/module/offers.php`:**
- Add `getOfferGroupFilters($offer_group_id)`: returns `['price_min', 'price_max', 'manufacturers' => [{manufacturer_id, name, count},...], 'attribute_groups' => [{attribute_group_id, name, values:[{text,count},...]},...]]`. Queries products in the group using same active/status/store filters as `getResolvedProductIds`.
- Add `getFilteredGroupProducts($offer_group_id, $filters)`: filters is array with optional keys `price_min`, `price_max`, `manufacturer_id` (array), `attribute` (array keyed by attribute_id, values array). Returns product ID list.

**`catalog/view/theme/dreamer/template/extension/module/offers_category.twig`** (and `default`):
- Full redesign: banner + two-column layout + filter sidebar + product grid + sort

**`catalog/view/theme/dreamer/template/extension/module/offers_info.twig`** (and `default`):
- Two-column layout with sticky sidebar. Remove initial_limit logic. Remove Load More button.

**`catalog/view/theme/dreamer/stylesheet/offers.css`** (and `default`):
- Add `.offer-layout` (two-col flex), `.offer-sidebar` (sticky), `.offer-main` styles
- Add filter sidebar styles
- Remove `offer-product-layout--hidden` and `offer-products-more` styles

---

## File Change Summary

| File | Change |
|------|--------|
| `admin/model/catalog/product.php` | `LIKE '%name%'` substring search (lines 388–389, 667–668) |
| `admin/model/catalog/category.php` | `LIKE '%name%'` substring search for category autocomplete |
| `admin/model/catalog/manufacturer.php` | `LIKE '%name%'` substring search for manufacturer autocomplete |
| `admin/view/template/extension/module/offer_manager_form.twig` | Full redesign: group panel, terms/badges chips, images grid, slug auto-gen, status toggle, category/manufacturer chips; fix `request.term` |
| `catalog/controller/extension/module/offers.php` | detail() + category() + search() updates |
| `catalog/model/extension/module/offers.php` | getOfferGroupFilters() + getFilteredGroupProducts() |
| `catalog/language/en-gb/extension/module/offers.php` | text_load_more + text_products_found |
| `catalog/view/theme/dreamer/template/extension/module/offers_info.twig` | Sidebar layout |
| `catalog/view/theme/default/template/extension/module/offers_info.twig` | Sidebar layout |
| `catalog/view/theme/dreamer/template/extension/module/offers_category.twig` | Filter sidebar + banner |
| `catalog/view/theme/default/template/extension/module/offers_category.twig` | Filter sidebar + banner |
| `catalog/view/theme/dreamer/stylesheet/offers.css` | Sidebar + filter styles |
| `catalog/view/theme/default/stylesheet/offers.css` | Sidebar + filter styles |

---

## Out of Scope
- Price range slider library (use two number inputs + native range; no jQuery UI dependency)
- Pagination (all products shown; filters reduce the set naturally)
- Search within category page (filter sidebar replaces need; global site search handles general product lookup)
