Files
newwalls.pl/.paul/phases/02-product-actions-fixes/02-01-PLAN.md
Jacek Pyziak 7ac795ba3f feat(new-layout): add-to-cart handler + piece configurator (Phase 02 plans 01-02)
Plan 02-01 (piece/crop configurator, complete):
- #piece reuse z shared partial product-cover-thumbnails.tpl
- 8 hidden inputs (is_crop, crop_pos_x/y, crop_width/height, piece_bg_top/left, is_reflection) w formie #add-to-cart-or-refresh
- Defensive setup w custom.js: setTimeout(600) init, no-op override totalpriceinfospecific/prod, DOM stubs
- CSS scope pod body#product .product-size-data .product-size-data--new

Plan 02-02 (add-to-cart submission, PARTIAL):
- Capture-phase native addEventListener (useCapture=true) blokuje PS core crash
  (button poza formą w nowym layoucie — closest('form') zwracało 0)
- Manualny AJAX POST: form.serialize() + qty + add=1&action=update do /pl/koszyk
- Fancybox-blocker port z custom.js:327 (nie odpalał się bo selector 0 matches)
- Manual sync is_crop/crop_width/height przed POST (obejście crash checkedHandler)
- prestashop.emit('updatedCart') + defensive blockcart refresh fetch
- Loading spinner + success flash CSS
- Inline handler mirror w product.tpl z idempotency guard (window.__p02p02Bound)
  — cache-buster dla browser cachowanego custom.js

Deferred do Plan 02-03 (customization + modal blocker dla production):
- Customization nie zapisuje się (squaremeter hook gate'owany discretion=on + brak dimension fields)
- Success modal (wymaga POST do /module/ps_shoppingcart/ajax)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:33:45 +02:00

311 lines
18 KiB
Markdown

---
phase: 02-product-actions-fixes
plan: 01
type: execute
wave: 1
depends_on: ["01-01"]
files_modified:
- themes/ayon/templates/catalog/product.tpl
- themes/ayon/assets/js/custom.js
- themes/ayon/assets/css/custom.scss
autonomous: false
delegation: off
---
<objective>
## Goal
Port konfiguratora "piece" (wybór fragmentu tapety + odbicie lustrzane) ze starego layoutu do nowego (IP `== '89.69.31.86'`), z zachowaniem pełnej zgodności DOM kontraktu dla serializacji do koszyka (hidden inputs `is_crop`, `crop_pos_x/y`, `crop_width/height`, `piece_bg_top/left` w formie `#add-to-cart-or-refresh`).
## Purpose
Użytkownik w nowym layoucie nie widzi i nie może ustawić wycinka tapety ani odbicia lustrzanego. W starym layoucie ten flow wysyłał wymiary fragmentu + pozycję + `is_crop` + mirror do koszyka. Bez portu — zamówienia z nowego layoutu nie mają informacji o kropie. Funkcja jest kluczowa dla produktu (sklep „na wymiar").
## Output
Działający piece configurator w nowym layoucie: draggable `#piece` na zdjęciu produktu + kontrolki w bloku „Rozmiar i dostosowanie" + button `#button-mirror-reflection`. Hidden inputs w formie `#add-to-cart-or-refresh` aktualizują się przy interakcji. Stary layout nietknięty.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/01-product-variants-fix/01-01-SUMMARY.md
Phase 01 wprowadziło form `<form id="add-to-cart-or-refresh">` w gałęzi nowego layoutu (ale owija tylko variants) oraz pattern `prestashop.emit('updatedProduct', resp)` po własnym AJAX refresh — piece musi umieć się re-inicjalizować po zmianie wariantu (bo `.product_image_wrapper` jest `.html()`-replace'owany).
## Source Files — stary layout (wzór implementacji)
@themes/ayon/templates/catalog/product.tpl (gałąź `!= '89.69.31.86'`, ok. linie 179-244 hidden inputs + `.product-block-piece` + `#button-mirror-reflection`, ok. linie 522-523 `.piece-left-positon` / `.piece-top-positon`)
@themes/ayon/assets/js/custom.js (ok. linie 100-330 — `dragElement`, `checkedHandler`, change-handlery `#piece-width` / `#piece-height`, mirror toggle, update `#piece_bg_top/left` + `#product_crop_pos_x/y`)
@themes/ayon/assets/css/custom.scss (reguły `.product-block-piece`, `#piece`, `#button-mirror-reflection`, `.piece-size-controls`, `.piece-size-values`)
## Source Files — nowy layout (cel modyfikacji)
@themes/ayon/templates/catalog/product.tpl (gałąź `== '89.69.31.86'` — blok `product_variants` z `<form id="add-to-cart-or-refresh">`, blok `product_size` z pustym `<div class="product-box--data">`, `<div class="product_image_wrapper">`)
</context>
<acceptance_criteria>
## AC-1: Markup piece + mirror w nowym layoucie
```gherkin
Given strona produktu jest renderowana dla IP 89.69.31.86 (nowy layout)
When DOM się wczytuje
Then istnieją: `#piece` jako child `.product_image_wrapper` (initially hidden),
`#button-mirror-reflection`, `.product-block-piece` (z `#checkbox-piece`, `#piece-width`, `#piece-height`, `#piece-size-view`) w `.product-size-data .product-box--data`,
`.piece-left-positon` i `.piece-top-positon` jako elementy pomocnicze,
hidden inputs (`is_crop`, `crop_pos_x`, `crop_pos_y`, `crop_width`, `crop_height`, `piece_bg_top`, `piece_bg_left`) jako dzieci formy `#add-to-cart-or-refresh`
```
## AC-2: Piece interaktywny — drag, checkbox, resize przez inputy, mirror
```gherkin
Given nowy layout jest załadowany
When użytkownik klika `#checkbox-piece`
Then `#piece` staje się widoczny (fadeIn), inputy `#piece-width` i `#piece-height` stają się edytowalne,
`$('#product_is_crop').val()` === '1'
When użytkownik przeciąga `#piece` po obrazie
Then pozycja `#piece` się aktualizuje,
`#product_crop_pos_x`, `#product_crop_pos_y`, `#piece_bg_left`, `#piece_bg_top` mają nowe wartości,
`.piece-left-positon` i `.piece-top-positon` mają pozycję piksel
When użytkownik zmienia `#piece-width` / `#piece-height`
Then wymiar `#piece` się aktualizuje, `#product_crop_width/height` też,
`#piece-size-view` pokazuje `WxH`
When użytkownik klika `#button-mirror-reflection`
Then `#piece` i `.product-images img.thumb` dostają klasę `.mirrored` (toggle)
```
## AC-3: Form serialization — wartości trafiają do formy
```gherkin
Given nowy layout, checkbox aktywny, piece przesunięty i zresize'owany
When wykonamy `$('#add-to-cart-or-refresh').serialize()` w konsoli
Then string zawiera: `is_crop=1`, `crop_pos_x=<N>`, `crop_pos_y=<N>`, `crop_width=<N>`, `crop_height=<N>`, `piece_bg_top=<N>`, `piece_bg_left=<N>` (obok istniejących `token`, `id_product`, `id_customization`, `group[...]=...` z Phase 01)
```
## AC-4: Re-init po zmianie wariantu (AJAX refresh)
```gherkin
Given piece jest włączony i ustawiony, użytkownik zmienia wariant kolorystyczny (click kafelka)
When Phase 01 AJAX refresh wykona `$('.product_image_wrapper').html(resp.product_cover_thumbnails)` + emit `updatedProduct`
Then `#piece` jest ponownie wstawiany do `.product_image_wrapper`, `dragElement` jest re-bound,
stan (checkbox aktywny, wymiary, pozycja, mirror) jest zachowany lub resetowany do spójnego defaultu jedno z, udokumentowane
```
## AC-5: Zero regresji w starym layoucie
```gherkin
Given użytkownik NIE jest na IP 89.69.31.86 (stary layout)
When strona produktu się ładuje
Then stary `.product-block-piece`, `#piece`, `#button-mirror-reflection` działają identycznie jak przed Phase 02
(drag, resize, mirror, hidden inputs serialize)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Markup piece/mirror + hidden inputs w gałęzi nowego layoutu product.tpl</name>
<files>themes/ayon/templates/catalog/product.tpl</files>
<action>
W gałęzi `{if $smarty.server.REMOTE_ADDR == '89.69.31.86'}`:
(a) Dodać 7 hidden inputs JAKO DZIECI formy `<form id="add-to-cart-or-refresh">` (obok variants grid, wewnątrz form):
- `<input type="hidden" name="is_crop" value="0" id="product_is_crop">`
- `<input type="hidden" name="crop_pos_x" value="0" id="product_crop_pos_x">`
- `<input type="hidden" name="crop_pos_y" value="0" id="product_crop_pos_y">`
- `<input type="hidden" name="crop_width" value="0" id="product_crop_width">`
- `<input type="hidden" name="crop_height" value="0" id="product_crop_height">`
- `<input type="hidden" name="piece_bg_top" id="piece_bg_top" value="">`
- `<input type="hidden" name="piece_bg_left" id="piece_bg_left" value="">`
(b) W `<div class="product_image_wrapper">` (gałąź nowego layoutu) dodać overlay:
- `<div id="piece" style="display:none;"></div>` jako rodzeństwo lub dziecko `.product-cover-thumbnails`
- `#piece` musi mieć `background-image` ustawiony inline z URL okładki produktu (to co stary layout robi — odszukaj w starym markup'ie dokładny sposób; zachowaj tę samą zmienną Smarty)
(c) W bloku `{block name='product_size'}` (nowa gałąź), w pustym `<div class="product-box--data">` w `.product-box.product-size-data`, dodać wrapper `.product-size-data--new` i umieścić:
- `<div class="product-block-piece">` z `<div class="product-bar-icon crop-icon">`, `<div class="piece-size-controls product-bar-box">` (zawiera `<span id="piece-size-view" class="strong">Wybierz rozmiar</span>`),
`<div class="piece-size-controls hidden"><input type="checkbox" id="checkbox-piece"><label for="checkbox-piece">Wymiary tapety</label></div>`,
`<div class="piece-size-values hidden"><input type="number" min="50" max="500" value="100" id="piece-width" readonly><input type="number" min="50" max="300" value="100" id="piece-height" readonly></div>`
- `<div id="button-mirror-reflection"><img src="/themes/ayon/assets/images/odbicie-iustrzane.png" alt=""><p class="button-mirror-reflection-label">Odbicie lustrzane</p></div>`
(d) Dodać rodzeństwem (poza image wrapperem, gdziekolwiek w kontenerze): `<div class="piece-left-positon hidden">10</div>` `<div class="piece-top-positon hidden">10</div>` — wymagane przez `custom.js`.
(e) Użyć DOKŁADNIE tych samych ID co w starym layoucie — dzięki temu handlery w `custom.js` (`jQuery("#piece-width").change(...)`, itd.) bind'ują się do elementów w nowym layoucie identycznie. DUPLIKATY ID ze starym layoutem są OK — tylko jedna gałąź renderuje się na raz (IP-gated).
**Avoid:**
- Nie zmieniać markup'u starej gałęzi (`!= '89.69.31.86'`) — `DO NOT CHANGE`.
- Nie dotykać partial'a `_partials/product-variants.tpl`.
- Nie zmieniać nazw `name="..."` hidden inputów — zmiana łamie zapis do koszyka po stronie serwera.
</action>
<verify>
Załaduj produkt na nowym layoucie i w DevTools:
- `document.getElementById('piece')` zwraca element
- `document.getElementById('product_is_crop').value === '0'` na starcie
- `document.getElementById('checkbox-piece')` istnieje
- `document.getElementById('button-mirror-reflection')` istnieje
- `$('#add-to-cart-or-refresh input[name="is_crop"]').length === 1`
- Stary layout (przez drugi browser/IP) — bez zmian
</verify>
<done>AC-1 spełnione + częściowo AC-3 (hidden inputs istnieją w formie).</done>
</task>
<task type="auto">
<name>Task 2: JS — piece init() + re-init po updatedProduct, zachowanie istniejących handlerów</name>
<files>themes/ayon/assets/js/custom.js</files>
<action>
Ponieważ ID elementów są identyczne, istniejące handlery (binding bezpośredni `jQuery("#piece-width").change(...)`, `jQuery("#checkbox-piece").change(...)`, itd.) ZAPINAJĄ się na starcie niezależnie od layoutu. Kluczowe dwa ryzyka:
(1) `dragElement(document.getElementById("piece"))` w `checkedHandler` — to zapina mousedown na `#piece`. Po `.product_image_wrapper.html(resp.product_cover_thumbnails)` w Phase 01 AJAX refresh, `#piece` może być:
- zniszczony (bo nie jest częścią `resp.product_cover_thumbnails`) → po AJAX trzeba go re-stworzyć lub przenieść przed replace'em
- zachowany (jeśli nie jest w `.product_image_wrapper` w nowym layoucie) → wtedy OK
Decyzja: `#piece` NIE JEST dzieckiem kontenera replace'owanego przez AJAX. Umieść go jako sibling do `.product-cover-thumbnails` ale WEWNĄTRZ `.product_image_wrapper`, I przenieś go przed replacem (detach → replace → re-append), ALBO: umieść `#piece` jako overlay nad `.product_image_wrapper` (sibling, absolutnie pozycjonowany) aby AJAX replace nie ruszał go. **Preferowane: overlay sibling** — prostsze i mniej edge case'ów.
(2) W custom.js (miejsce po obecnym handlerze `change` wariantu w Phase 01), zarejestruj listener:
```js
prestashop.on('updatedProduct', function(event){
// re-ensure #piece position sync po ew. resize wrappera
if ($('#product_is_crop').val() === '1') {
// re-trigger width/height change handlers to recompute background-position
$('#piece-width').trigger('change');
$('#piece-height').trigger('change');
}
});
```
Cel: piece pozostaje zsynchronizowany wizualnie po resize kontenera (różne warianty mogą mieć różne wymiary obrazu okładki).
(3) (Opcjonalnie, jeżeli inline background-image dla `#piece` jest ustawiany przez JS zamiast w Smarty) — w handlerze `updatedProduct` odświeżyć `#piece`'s `background-image` na bazie `resp.product_cover` lub nowego `img.thumb`.
**Avoid:**
- Nie refactoruj istniejących handlerów piece — dokładaj tylko to co potrzebne dla nowego layoutu.
- Nie duplikuj `dragElement` — użyj istniejącej funkcji.
- Nie dotykaj handlera variant-change z Phase 01 (ok. istniejąca sekcja AJAX refresh).
</action>
<verify>
W browserze (nowy layout):
1. `$('#checkbox-piece').click()` → `#piece` widoczny, `#product_is_crop` = '1'
2. Przeciągnij `#piece` myszką → `#product_crop_pos_x` i `#product_crop_pos_y` mają wartości != 0
3. Zmień `#piece-width` na 200, trigger change → `#product_crop_width` = 200, `.piece-width-px` text = '200'
4. Klik `#button-mirror-reflection` → `#piece.mirrored` true
5. Zmień wariant kolorystyczny (click kafelka) → po AJAX refresh `#piece` nadal istnieje, stan zachowany
</verify>
<done>AC-2 i AC-4 spełnione.</done>
</task>
<task type="auto">
<name>Task 3: CSS — scoped styles i wizualna warstwa piece dla nowego layoutu</name>
<files>themes/ayon/assets/css/custom.scss</files>
<action>
Reguły dla starego layoutu są już w `custom.scss` (`#piece`, `.product-block-piece`, `#button-mirror-reflection`, itd.). W nowym layoucie kontekst jest inny — `.product_image_wrapper` zamiast `.product-images`, osadzenie w `.product-size-data .product-box--data .product-size-data--new`.
Dopisać sekcję (na końcu pliku lub obok dotychczasowego scope Phase 01):
```scss
body#product .product-size-data {
.product-box--data { padding: 0; }
.product-size-data--new {
// layout wewnętrzny — piece controls + mirror side-by-side
display: flex; align-items: center; gap: 16px;
.product-block-piece { /* ... */ }
.piece-size-controls, .piece-size-values { /* ... */ }
#button-mirror-reflection { cursor: pointer; /* ... */ }
}
}
body#product .product_image_wrapper {
position: relative; // anchor dla #piece overlay
#piece {
position: absolute;
background-size: cover;
cursor: move;
z-index: 10;
// display:none default (JS fadeIn)
}
}
```
**NIE ustawiaj globalnie na `#piece` / `.product-block-piece`** — to zepsuje stary layout. Scope'uj pod `body#product .product-size-data .product-size-data--new` oraz `body#product .product_image_wrapper #piece`.
Edytuj TYLKO `custom.scss`. `custom.css` jest auto-generowany przez watcher user'a (feedback memory `.claude/memory/feedback_scss_only.md`).
Zadanie nie wymaga pixel-perfect fit — celem jest: piece widoczny, draggable, mieści się na obrazie; controls czytelne w panelu. Vizualny polish (pixel fit) odnotować jako Deferred jeżeli nie ma ref'u Figma.
**Avoid:**
- Nie usuwaj ani nie modyfikuj istniejących reguł (stary layout).
- Nie dodawaj `!important`.
</action>
<verify>
Po rekompilacji SCSS (watcher usera) w `custom.css` pojawia się nowy blok `body#product .product-size-data` i `body#product .product_image_wrapper #piece`. Stary layout w DevTools nadal ma działające stare reguły (brak konfliktów w DevTools Elements panel).
</verify>
<done>AC-1 spełnione wizualnie (kontrolki widoczne, piece z position/cursor), AC-5 zachowane.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- Markup piece + mirror + hidden inputs w gałęzi nowego layoutu `product.tpl`
- JS init + re-init na `updatedProduct` w `custom.js`
- Scoped SCSS w `custom.scss`
</what-built>
<how-to-verify>
1. Nowy layout (IP 89.69.31.86):
- Przejdź na dowolny produkt z wariantami
- W bloku „Rozmiar i dostosowanie" kliknij checkbox „Wymiary tapety" — `#piece` powinien się pokazać nad zdjęciem
- Przeciągnij `#piece` myszką — pozycja się zmienia
- Zmień wartość `#piece-width` i `#piece-height` — `#piece` się przeskalowuje, `#piece-size-view` pokazuje `WxH`
- Kliknij `#button-mirror-reflection` — piece i thumb dostają klasę `.mirrored`
- W DevTools: `$('#add-to-cart-or-refresh').serialize()` zawiera `is_crop=1&crop_pos_x=<N>&crop_pos_y=<N>&crop_width=<N>&crop_height=<N>&piece_bg_top=<N>&piece_bg_left=<N>`
- Kliknij inny wariant kolorystyczny — po AJAX refresh piece dalej istnieje i działa
2. Stary layout (dowolny inny IP):
- Otwórz produkt w zwykłej przeglądarce (IP != 89.69.31.86)
- Sprawdź że piece/mirror/resize/drag działają bez regresji
- Dodaj do koszyka — wartości crop zapisują się do customization (jak wcześniej)
3. Playwright (opcjonalnie jeżeli dostępny): zautomatyzuj kroki 1 i zaloguj wyniki.
</how-to-verify>
<resume-signal>Napisz „approved" aby zamknąć APPLY, lub opisz problemy (z dokładnymi krokami odtworzenia) do naprawy.</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Gałąź `{if $smarty.server.REMOTE_ADDR != '89.69.31.86'}` w `themes/ayon/templates/catalog/product.tpl` (stary layout, produkcja).
- Istniejące handlery piece w `themes/ayon/assets/js/custom.js` (linie ~100-330 stare binding'i — tylko dodawaj nowe listenery/inicjalizacje, nie modyfikuj).
- Handler variant-change z Phase 01 w `custom.js` — pattern ustalony, tylko dodaj listener `prestashop.on('updatedProduct', ...)`.
- Partial `themes/ayon/templates/catalog/_partials/product-variants.tpl` — shared, nie dotykać.
- Wszystkie istniejące reguły SCSS dla starego layoutu.
## SCOPE LIMITS
- Nie naprawiamy „Dodaj do koszyka" w tym planie (osobny plan fazy 02).
- Nie wypełniamy pustych bloków `.product-protect`, `.product-installation`, `.product-order-sample` (osobny plan fazy 02).
- Nie dodajemy resize handles na `#piece` („bonus" z rozmowy z userem) — deferred do ewentualnego Plan 02-02 jako feature add-on. W tym planie: drag + resize przez inputy, jak stary layout.
- Nie dodajemy nowych zależności (jquery-ui resizable, interact.js, itp.).
- Nie zmieniamy server-side logiki zapisu kropu do koszyka — polegamy na tym że nazwy hidden inputs są identyczne ze starym layoutem.
</boundaries>
<verification>
Before declaring plan complete:
- [ ] Hidden inputs obecne wewnątrz `#add-to-cart-or-refresh` w nowym layoucie (AC-1, AC-3)
- [ ] `#piece`, `.product-block-piece`, `#button-mirror-reflection` widoczne i interaktywne w nowym layoucie (AC-2)
- [ ] Drag + resize przez inputy + mirror aktualizują hidden inputs (AC-2, AC-3)
- [ ] Po zmianie wariantu kolorystycznego piece nadal działa (AC-4)
- [ ] Stary layout bez regresji w pełnym cyklu (drag → resize → mirror → add-to-cart → customization zapisuje się) (AC-5)
- [ ] `custom.scss` edytowany, `custom.css` nie tknięty ręcznie
- [ ] Brak duplikatów funkcji JS, brak konfliktów CSS
</verification>
<success_criteria>
- Wszystkie 3 auto taski ukończone + checkpoint human-verify z "approved"
- 5/5 AC satysfakcjonowane
- Zero regresji na starym layoucie (weryfikowane checkpoint'em)
- Dokumentacja decyzji i deviation'ów w SUMMARY
</success_criteria>
<output>
After completion, create `.paul/phases/02-product-actions-fixes/02-01-SUMMARY.md`
</output>