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>
This commit is contained in:
2026-04-23 23:33:45 +02:00
parent 161c090ef0
commit 7ac795ba3f
24 changed files with 5447 additions and 2569 deletions

View File

@@ -0,0 +1,310 @@
---
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>

View File

@@ -0,0 +1,217 @@
---
phase: 02-product-actions-fixes
plan: 01
subsystem: ui
tags: [prestashop, smarty, jquery, scss, piece, crop, fancybox, module-hook-compat]
requires:
- phase: 01-product-variants-fix
provides:
- Form `<form id="add-to-cart-or-refresh">` w gałęzi nowego layoutu — Plan 02-01 dołącza do niej 8 hidden inputs dla crop/mirror
- Marker class `.product-variants-data--new` — Plan 02-01 wykorzystuje do scope'u JS init (`setTimeout` check)
- Własny AJAX refresh w custom.js (`action=refresh` + in-place DOM update) — Plan 02-01 dopina listener `prestashop.on('updatedProduct')` do re-bindowania dragElement po replace `.product_image_wrapper`
provides:
- Działający konfigurator „piece" (wybór fragmentu tapety) w nowym layoucie: popup trigger w `.product-size-data`, fancybox z input'ami Szerokość/Wysokość, piece overlay na zdjęciu
- 8 hidden inputów `is_crop`, `is_reflection`, `crop_pos_x/y`, `crop_width/height`, `piece_bg_top/left` w formie `#add-to-cart-or-refresh` (nowy layout)
- Defensive setup (no-op override `totalpriceinfospecific`/`prod` + DOM stubs w custom.js) — unblocks istniejące piece handlery w nowym layoucie bez modyfikacji inline-scriptów z module hooks
affects:
- Plan 02-02 (add-to-cart) — hidden inputs już w formie, gotowe do POST'owania do koszyka. Także no-op override `totalpriceinfospecific` oznacza że logika cen wymaga innego mechanizmu w nowym layoucie.
- Plan 02-03 (empty blocks) — pattern scope'owania CSS pod `.product-size-data .product-size-data--new` do reużycia dla `.product-protect`, `.product-installation`, `.product-order-sample`
tech-stack:
added: []
patterns:
- "Shared partial reuse: zamiast duplikować element między layoutami, reużyć istniejący (#piece z product-cover-thumbnails.tpl)"
- "No-op override globalnych funkcji z module hook (`totalpriceinfospecific`, `prod`) jako alternative do modyfikacji inline-scriptów których nie można zmienić"
- "DOM stubs injection via JS zamiast template — obchodzi Smarty cache + FTP sync delay"
- "setTimeout(600) init zamiast `$(document).ready` — defer poza queue ready callbacków które crash'ują na brakujących DOM elementach w nowym layoucie"
key-files:
created:
- .paul/phases/02-product-actions-fixes/02-01-PLAN.md
- .paul/phases/02-product-actions-fixes/02-01-SUMMARY.md
modified:
- themes/ayon/templates/catalog/product.tpl
- themes/ayon/assets/js/custom.js
- themes/ayon/assets/css/custom.scss
key-decisions:
- "`#piece` reużywany z product-cover-thumbnails.tpl — nie duplikujemy w product.tpl"
- "totalpriceinfospecific / prod override'owane na no-op w nowym layoucie (crash z powodu brakujących DOM deps)"
- "DOM stubs injection w JS zamiast template (cache/sync unreliability)"
- "Piece NIE auto-init'uje się — pojawia się dopiero po kliknięciu popup trigger (user feedback w checkpoint)"
- "Defensive guard `.pp_stick_parent` w click handler (element exists only w starym layoucie)"
patterns-established:
- "Scope'owanie CSS dla bloków nowego layoutu: `body#product .<block-class> .<block-class>--new {...}` — wzór do reużycia dla kolejnych bloków (product-protect, product-installation, etc.)"
- "Obrona przed cross-layout breakage module hooków: setTimeout init w custom.js z no-op override + DOM stubs; nie ruszamy inline-scriptów"
- "Reużywanie state holders jako hidden inputów: `<input type='checkbox' style='display:none'>` zamiast tworzenia osobnego state-management, istniejące handlery bindują się do ID identycznych ze starym layoutem"
duration: ~3.5h (z live debug'iem via Playwright)
started: 2026-04-23T19:30:00Z
completed: 2026-04-23T22:30:00Z
---
# Phase 02 Plan 01: Piece/crop configurator (nowy layout) — Summary
**Port konfiguratora „piece" (wybór fragmentu tapety + mirror) do nowego layoutu z zachowaniem pełnego kontraktu serializacji do koszyka. Popup trigger + fancybox — odzwierciedla user flow starego layoutu.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~3.5h |
| Started | 2026-04-23T19:30:00Z |
| Completed | 2026-04-23T22:30:00Z |
| Tasks | 3 auto + 1 checkpoint (approved po iteracji) |
| Files modified | 3 source + 2 PAUL meta |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Markup piece + mirror w nowym layoucie | Pass | `#piece` reużyty z shared partial (nie duplikowany), controls w `.product-size-data .product-box--data`, 8 hidden inputów w `#add-to-cart-or-refresh`. Initial text „Wybierz rozmiar" (zamiast stałego „100x100" per user feedback). |
| AC-2: Piece interaktywny | Pass | Popup otwiera się po kliknięciu `.piece-summary`, zmiana wymiarów w popup → piece rescale (100x100 → 200x150 = 40%×50% kontenera), hidden inputs `#product_crop_width/height` sync'ują. Drag działa (dragElement z istniejącego flow). Verified via Playwright. |
| AC-3: Form serialization | Pass | `$('#add-to-cart-or-refresh').serialize()` zwraca pełny ciąg: `token=...&id_product=...&id_customization=...&is_crop=1&is_reflection=0&crop_pos_x=200&crop_pos_y=100&crop_width=200&crop_height=150&piece_bg_top=-104.398&piece_bg_left=-208.804&group[4]=6`. Wszystkie pola aktualne. |
| AC-4: Re-init po zmianie wariantu | Not tested | AC zakładał re-bind dragElement po `updatedProduct`. Listener dodany (custom.js:681-692), ale live test zmiany wariantu nie przeprowadzony w tej sesji. Markowane jako "not verified" — do re-verify w Plan 02-02 lub osobno. |
| AC-5: Zero regresji w starym layoucie | Not tested | Stary layout (IP != 89.69.31.86) nie był testowany w tej sesji. Zmiany w custom.js (no-op override, setTimeout init, stubs injection) są scope'owane do nowego layoutu via `$('.product-variants-data--new').length` check — nie powinny dotknąć starego. Do weryfikacji przed commitowaniem phase. |
**Modyfikacja AC:** Plan pierwotnie zakładał piece auto-init'ujący się na load (inline `<style>` force visibility + `checkedHandler` w setTimeout). User feedback w trakcie checkpoint zmienił wymaganie: piece pojawia się dopiero po kliknięciu trigger. AC-2 satisfied zgodnie z tym zmodyfikowanym flow.
## Accomplishments
- **Port piece configurator z zachowaniem kontraktu** — identyczne ID i nazwy hidden inputów ze starym layoutem → istniejące JS handlers bindują się automatycznie, serwer-side logic nie wymaga zmian.
- **Popup match z Figma/screenshot** — istniejący fancybox flow (custom.js:535-557) działa w nowym layoucie po dodaniu trigger'a `<a class="fancybox-size-controls piece-summary">` i defensywnych guard'ów.
- **Rozwiązanie 5 ukrytych problemów wykrytych w live debug** (Playwright): duplikat `#piece`, CSS `:has()` edge case, ready queue fighting, inline-script crash, brakujący `.pp_stick_parent`. Każdy udokumentowany w Decisions.
- **Pattern defensywnego setup'u dla module hook compatibility** — no-op override + DOM stubs w JS zamiast template. Reużywalny przy naprawach innych funkcji nowego layoutu.
## Task Commits
Task commits nie zostały jeszcze utworzone — APPLY wykonywany inline (delegation: off) bez per-task commitów. Commit fazowy wykona `transition-phase` po zakończeniu całej fazy 02 (plany 02-01, 02-02, 02-03+).
| Task | Commit | Type | Description |
|------|--------|------|-------------|
| Task 1 (markup) | pending | feat | Piece trigger + controls + 8 hidden inputs w gałęzi nowego layoutu |
| Task 2 (JS) | pending | feat | setTimeout init z no-op override + stubs, `prestashop.on('updatedProduct')` listener, defensive fancybox guard |
| Task 3 (CSS) | pending | feat | Scoped styles `.product-size-data--new`, `.piece-summary`, `#button-mirror-reflection` |
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `themes/ayon/templates/catalog/product.tpl` | Modified | Gałąź `== '89.69.31.86'`: 8 hidden inputs w `#add-to-cart-or-refresh`, `.piece-summary` anchor w `.product-size-data .product-box--data`, wrapper `.product-size-data--new`, mirror button, hidden state (checkbox, width/height, position divs). Initial `#piece-size-view` text = „Wybierz rozmiar". |
| `themes/ayon/assets/js/custom.js` | Modified | +~60 linii: `prestashop.on('updatedProduct', ...)` listener do re-bindu dragElement po AJAX refresh, setTimeout(600) init w nowym layoucie (override totalpriceinfospecific/prod na no-op, wstrzyknięcie DOM stubs), defensive `.pp_stick_parent` guard w `.fancybox-size-controls` click handler (ok. linia 540). |
| `themes/ayon/assets/css/custom.scss` | Modified | +~60 linii na końcu pliku: Phase 02 scope block pod `body#product .product-size-data .product-size-data--new` (flex layout, piece-summary trigger styling, mirror button hover). Istniejące `.product-images .piece` ze starego layoutu applikują się automatycznie do `#piece` z shared partial. |
| `.paul/phases/02-product-actions-fixes/02-01-PLAN.md` | Created | Plan fazy 02 plan 01 |
| `.paul/phases/02-product-actions-fixes/02-01-SUMMARY.md` | Created | Ten plik |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Reuse `#piece` z `product-cover-thumbnails.tpl` (shared partial), NIE duplikować | Pierwotna próba dodania osobnego `<div id="piece">` jako sibling `.product_image_wrapper` tworzyła duplikat ID → two elements z tym samym ID → my element renderował biała pusta ramkę, oryginalny (z pattern bg) był niewidoczny gdzie indziej. | Mniej markup, reużycie istniejącego styling (`.product-images .piece` ze starego SCSS). |
| Override `totalpriceinfospecific` / `prod` na no-op w custom.js setTimeout | Inline-script z module hook (squaremeter) czyta `#totalpriceinfo.style`, `#product-details.data()`, robi `JSON.parse(undefined)` — wszystko null/undefined w nowym layoucie. Error propaguje przez `trigger('change')` na `#piece-width` i abortuje `checkedHandler` przed `is_crop=1`. | `checkedHandler` działa w nowym layoucie. Cena calculation wyłączona — trzeba zapewnić alternatywny mechanizm w Plan 02-02. |
| DOM stubs (`#totalpriceinfo`, etc.) wstrzykiwane przez JS, nie przez template | Smarty template cache + ręczny FTP sync powodował że stuby w `product.tpl` nie docierały do browsera (page wciąż używała starej skompilowanej wersji templatu). JS deploys są bardziej deterministyczne (pojedynczy plik, jasne cache semantics). | Stuby zawsze w DOM w nowym layoucie. Template pozostaje czysty. |
| Piece NIE auto-init'uje się na load (user feedback w checkpoint) | User: „kwadrat pojawia się dopiero po kliknieciu przycisku cm — kliknij aby zmienić". Zmiana zachowania vs pierwotny plan. | Load = placeholder + hidden piece. Click = popup + piece fadeIn. Oszczedność initial clutter na zdjęciu. |
| Defensive guard `.pp_stick_parent` w fancybox-size-controls click handler | Element istnieje tylko w starym layoucie (PS sidebar). Bez guard'a `$('.pp_stick_parent').offset().top` rzuca TypeError, aborts handler przed `$.fancybox()`. | Popup działa w obu layoutach. Old layout nieruszony (guard = if empty → skip). |
| `piece-width/height` hidden state jako `style="display:none;"` inputs w markup (nie w popup JS template) | Istniejące JS handlery (custom.js:275-300) bindują się do ID `#piece-width`/`#piece-height` przez direct binding. Muszą istnieć w DOM at ready time. Popup używa `#fancy-piece-width/height` i sync'uje do hidden state on confirm. | Zero zmian w istniejącej logice. Clean separation: popup UI × state inputs. |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 5 | Niezbędne — bez nich piece nie działał w nowym layoucie |
| Scope additions | 3 | Defensive setup (override + stubs + guard) — wymagane przez module hook compat |
| Deferred | 1 | AC-5 (stary layout regresja test) + AC-4 (variant AJAX re-init) nie zweryfikowane w tej sesji |
**Total impact:** Plan wykonany z istotnymi auto-fixami znalezionymi przez Playwright live debug. Zero scope creep poza niezbędnymi do działania. Zmiana user-facing behavior (piece auto-init → click-to-show) uzgodniona z userem w trakcie checkpoint.
### Auto-fixed Issues
**1. Duplikat `#piece` — dwa elementy z tym samym ID**
- **Found during:** Task 1 (markup) → live Playwright snapshot wykrył `pieceCount: 1` ale `pieceStyles.backgroundImage: url(".../birds-in-the-fog.jpg")` — element pochodzi ze shared partial `product-cover-thumbnails.tpl:53`.
- **Issue:** Dodałem własny `<div id="piece">` jako sibling `.product_image_wrapper`. Powstał duplikat ID (invalid HTML + konflikt JS/CSS).
- **Fix:** Usunięty własny `<div id="piece">`. Reużywamy istniejący z partial. Usunięty marker class `product_image--new` jako nieużywany.
- **Files:** `themes/ayon/templates/catalog/product.tpl`, `themes/ayon/assets/css/custom.scss`
- **Verification:** `document.querySelectorAll('#piece').length === 1` + `pieceParentClass: thumb-container (LI)`
**2. CSS `:has(> #piece)` selector — edge case w kontekście**
- **Found during:** Task 3 CSS + live test.
- **Issue:** `body#product .product_image:has(> #piece) { ... #piece { display: block } }` nie dawał expected override. Compiled CSS selector miał `display: block` ale niekoniecznie match'ował DOM tree (layout-dependent).
- **Fix:** Usunięty `:has()` selector. Reużywamy istniejące stylowanie `.product-images .piece` ze starego SCSS (linia 623) — applikuje się bo `#piece` jest w `.product-images` (z shared partial).
- **Files:** `themes/ayon/assets/css/custom.scss`
- **Verification:** Live test — `#piece` widoczny po kliknieciu popup trigger (display: block z fadeIn).
**3. Inline-script crash: `totalpriceinfospecific` TypeError `null.style`**
- **Found during:** Task 4 live verify (checkpoint).
- **Issue:** Module hook injectuje inline script z funkcją `totalpriceinfospecific` która robi `document.getElementById('totalpriceinfo').style.display = 'block'`. W nowym layoucie `#totalpriceinfo` nie istnieje. Funkcja wywoływana przez `#piece-width change handler` (custom.js:281). Error propaguje przez `trigger('change')` w checkedHandler (custom.js:183) — abortuje przed `is_crop=1`.
- **Fix:** W setTimeout init: `window.totalpriceinfospecific = function() {};` (no-op override). Plus DOM stubs dla pozostałych elementów (`#custom-wallpaper-price`, `#custom-wallpaper-price-label`, `#quantity_wanted*`).
- **Files:** `themes/ayon/assets/js/custom.js`
- **Verification:** Playwright: po init `totalpriceinfospecific.toString()` = no-op, `checkedHandler` dochodzi do `is_crop=1` bez error.
**4. Inline-script crash (wariant #2): `JSON.parse(undefined)` w `totalpriceinfospecific`**
- **Found during:** Task 4 live verify — po stub'owaniu `#totalpriceinfo` error zmienił się na `"undefined" is not valid JSON` przy `$('#product-details').data('product').quantity_discounts`.
- **Issue:** Funkcja ma WIELE DOM dependencies (nie tylko `#totalpriceinfo`). Stubowanie każdego to moving target.
- **Fix:** Override całej funkcji na no-op (zamiast stubowania jej DOM). W nowym layoucie price calc i tak musi przejść inną drogą (do zaplanowania w Plan 02-02).
- **Files:** `themes/ayon/assets/js/custom.js`
- **Verification:** Playwright: `checkedHandler` runs clean, żadnych JSON.parse errorów w console.
**5. Existing ready() fighting my init: checkbox unchecked + values=50**
- **Found during:** Task 4 live verify — po override totalpriceinfospecific state wciąż `checked: false`, `piece-width: 50`.
- **Issue:** `custom.js:209-214` ma `$(document).ready(function() { jQuery('#checkbox-piece').prop('checked', false); $('#piece-width').val(50); $('#piece-height').val(50); checkedHandler(...) })`. Default init dla starego layoutu. Moje setTimeout(600) odpalał się PO tym i wywoływał `checkedHandler($('#checkbox-piece'))` z **unchecked** checkbox → else branch (disabled state).
- **Fix (iteracja 1):** W setTimeout: `$('#checkbox-piece').prop('checked', true); $('#piece-width').val(100); $('#piece-height').val(100);` przed `checkedHandler`. Usunięty po user feedback (piece ma się nie pokazywać na load).
- **Fix (iteracja 2, finalny):** Usunięty `checkedHandler` call w ogóle — piece pojawia się tylko po klikn. trigger'a popupu przez użytkownika. Init tylko wstrzykuje stuby + override funkcji.
- **Files:** `themes/ayon/assets/js/custom.js`
- **Verification:** Playwright: po load `pieceVisible: false` ✓, po `$('.piece-summary')[0].click()``pieceVisible: true, productIsCrop: 1, fancyboxOpen: true` ✓.
### Scope Additions
**1. No-op override `totalpriceinfospecific` + `prod`** (nie w planie) — wymagane dla module hook compat, inaczej piece crash'uje.
**2. DOM stubs injection via JS** (nie w planie) — wymagane jako alternative do niezawodnego deploy'u template'u.
**3. Defensive `.pp_stick_parent` guard** (nie w planie) — wymagane żeby popup się otwierał w nowym layoucie.
### Deferred Items
- **AC-5 regression test starego layoutu** — nie przeprowadzony w tej sesji. Moje zmiany w custom.js są scope'owane pod `.product-variants-data--new` check (jest tylko w nowym layoucie), więc teoretycznie stary nietknięty. Do weryfikacji przed commitowaniem fazy 02 (transition).
- **AC-4 re-init po AJAX variant change** — listener dodany, ale live test zmiany wariantu (click na kafelek → AJAX refresh → `#piece` re-rendered → dragElement re-binds) nie przeprowadzony. Do weryfikacji w kolejnej sesji.
- **Mirror button end-to-end** — handler istnieje w custom.js, w nowym layoucie ma własny przycisk w markup, ale nie testowany klick → `#piece.mirrored` + `#product_is_reflection=1`. Do weryfikacji.
- **Piece-size-view wording** — po kliknieciu popup pokazuje „50x50" (default values) zamiast „Wybierz rozmiar". Cosmetic, opcjonalny future improvement.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Browser cache serwował stary `custom.js` mimo że server miał nowy | Force reload (Ctrl+Shift+Del + clear cache, lub DevTools Network → Disable cache + F5). Playwright: fetch z `?nc=<timestamp>` + dynamic eval init block aby zsynchronizować live state z latest JS. Rekomendacja: w produkcji dodać cache-buster `?v=<version>` do `<script src>` tag'a (Plan 02-02 lub osobno). |
| PrestaShop Smarty cache + FTP sync delay sprawiał że template edits nie dochodziły do browsera | Przeniesienie wszystkich stubs z template do JS (custom.js). Rekomendacja: po każdej zmianie w `.tpl` → Admin PS → Performance → Clear cache. |
| `dispatch`/`trigger` w jQuery forwardsem propagują błąd z nested handler do caller — cały flow abort'uje | Try/catch nie wystarczy (handler sam w sobie rzuca, nie ma sposobu catch poza patchowaniem). Override problematycznej funkcji (no-op) to jedyne czyste rozwiązanie bez modyfikacji nietuchable inline-scriptów. |
## Skill audit
SPECIAL-FLOWS.md nie skonfigurowane. W Plan 02-01 użyte:
- `mcp__plugin_playwright_playwright__*`**krytyczne**. Bez live debug (eval w page context, fetch current script, inspect runtime state) nie dalo by się znaleźć inline-script crashy. Multi-round testing po każdej poprawce.
- Context-mode `ctx_execute` + `ctx_batch_execute` — do eksploracji repo bez zanieczyszczania context window.
**Rekomendacja:** Dodać Playwright MCP jako required skill dla dalszych planów Phase 02 (add-to-cart, empty blocks) — live debug w działającej produkcji jest kluczowy.
## Next Phase Readiness
**Ready:**
- Hidden inputy crop/mirror w formie `#add-to-cart-or-refresh` → gotowe do POST'owania do koszyka przez Plan 02-02.
- Pattern defensywnego setup'u (no-op override + DOM stubs + setTimeout init) reużywalny dla kolejnych napraw module-hook-dependent funkcji.
- Pattern scope'owania CSS pod `.<block>-data--new` do reużycia dla pustych bloków.
- Playwright MCP workflow ustalony — potwierdzone że live debug jest niezbędny.
**Concerns:**
- `totalpriceinfospecific` no-op override wylacza cene calculation w nowym layoucie. Plan 02-02 musi zapewnić alternatywne mechanizm (prawdopodobnie przez PrestaShop `prestashop.on('updatedProduct', ...)` listener ktory czyta `resp.product_prices`).
- Stary layout nie zweryfikowany end-to-end po moich zmianach w custom.js. Ryzyko małe (scope'owane pod marker class), ale do weryfikacji przed commitem phase'a.
- Variant AJAX refresh + re-init piece dragElement — listener dodany ale nie przetestowany. Edge case'y (różne wymiary obrazu między wariantami, state preservation) do przejrzenia.
**Blockers:** None — Plan 02-02 (add-to-cart) może wejść do planowania.
---
*Phase: 02-product-actions-fixes, Plan: 01*
*Completed: 2026-04-23*

View File

@@ -0,0 +1,389 @@
---
phase: 02-product-actions-fixes
plan: 02
type: execute
wave: 1
depends_on: ["02-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
Uruchomić „Dodaj do koszyka" end-to-end w nowym layoucie strony produktu (IP 89.69.31.86): klik w przycisk → walidacja piece (wymiary wybrane) → POST do `/cart` z pełnym body (token + id_product + id_customization + qty + 8 hidden inputów crop/mirror) → success feedback + odświeżenie cart widget w headerze bez page reload.
## Purpose
Bez tej funkcjonalności nowy layout nie nadaje się do testów — klient nie zatwierdzi go jako alternatywy dla starego do czasu aż dodawanie do koszyka działa identycznie (feature parity). Plan 02-01 (piece) dostarczył hidden inputy gotowe do POST'owania; Plan 02-02 domyka kontrakt: od kliknięcia przycisku do potwierdzenia że produkt trafił do koszyka.
## Output
- Działający add-to-cart flow w nowym layoucie zweryfikowany live (Playwright).
- Listener `prestashop.on('updatedCart', ...)` lub równoważny mechanizm odświeżający cart widget (header counter / modal) po pomyślnym POST.
- Success/error UX zgodny ze starym layoutem (toast modal / redirect do koszyka — do ustalenia w Task 1).
- Regresja starego layoutu = zero (zmiany scope'owane pod marker class `.product-variants-data--new` albo gałąź `if REMOTE_ADDR == '89.69.31.86'`).
- Summary: `.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md`.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/02-product-actions-fixes/02-01-SUMMARY.md
# Plan 02-01 dostarczył 8 hidden inputów w `#add-to-cart-or-refresh`:
# is_crop, is_reflection, crop_pos_x, crop_pos_y, crop_width, crop_height,
# piece_bg_top, piece_bg_left. Gotowe do POST'owania. Również nałożył no-op
# override na `totalpriceinfospecific`/`prod` — to wpływa na kalkulację ceny,
# ale NIE na sam POST (to osobny plan).
## Source Files
@themes/ayon/templates/catalog/product.tpl
@themes/ayon/templates/catalog/_partials/product-add-to-cart.tpl
@themes/ayon/assets/js/custom.js
@themes/ayon/assets/css/custom.scss
## External Reference
# PrestaShop 1.7 core cart flow (dla orientacji — NIE modyfikujemy core):
# - Handler globalny: `$(document).on('click', '[data-button-action=add-to-cart]', ...)`
# - POST: `urls.pages.cart` z `add=1&action=update&id_product=...&qty=...&token=...`
# - Response JSON: { success, hasError, errors, cart, ... }
# - Eventy: `prestashop.emit('updatedCart', { resp, reason })` po sukcesie
</context>
<skills>
## Required Skills
<!-- SPECIAL-FLOWS.md jeszcze nie istnieje (rekomendacja z Plan 02-01 do utworzenia). -->
<!-- Dla Plan 02-02 skills section jest minimalna: Playwright MCP wymagany do Task 1 diagnosis. -->
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| Playwright MCP (`mcp__plugin_playwright_playwright__*`) | required | Task 1 live diagnosis, Task 3 regression test | ○ |
| context-mode (`mcp__plugin_context-mode_context-mode__*`) | optional | Exploracja kodu PS core / custom.js bez zanieczyszczania kontekstu | ○ |
**BLOCKING:** Playwright MCP wymagany w Task 1 — bez live inspection nie da się ustalić czy PS core handler przechwytuje click.
## Skill Invocation Checklist
- [ ] Playwright MCP dostępny (tools `mcp__plugin_playwright_playwright__browser_*`)
- [ ] Dostęp do środowiska z IP `89.69.31.86` (lub sposób na spoof'owanie — do ustalenia w Task 1)
</skills>
<acceptance_criteria>
## AC-1: Add-to-cart button submitowalny w nowym layoucie
```gherkin
Given strona produktu w nowym layoucie (REMOTE_ADDR == '89.69.31.86')
And piece został skonfigurowany (#checkbox-piece checked, width/height > 0)
When użytkownik klika przycisk "Dodaj do koszyka" (`[data-button-action=add-to-cart]`)
Then request POST wychodzi do urls.pages.cart
And body zawiera: token, id_product, id_customization, qty, is_crop=1, crop_pos_x/y, crop_width, crop_height, piece_bg_top/left, is_reflection
And response HTTP 200 z JSON { success: true, cart: {...} }
```
## AC-2: Walidacja "musi wybrać piece" — fancybox blokada nadal działa
```gherkin
Given strona produktu w nowym layoucie
And #checkbox-piece NIE jest zaznaczony (user nie otworzył popup'a piece)
When użytkownik klika "Dodaj do koszyka"
Then pojawia się fancybox z treścią "Proszę wybrać rozmiar i wycinek tapety..."
And POST do koszyka NIE wychodzi (network tab clean)
And stan koszyka niezmieniony
```
## AC-3: Cart widget odświeża się po sukcesie
```gherkin
Given udany POST z AC-1 (response success=true)
When backend zwraca response
Then nagłówek/cart widget pokazuje zaktualizowaną liczbę produktów (+1)
And (jeśli istnieje modal potwierdzający) wyświetla się success modal LUB redirect do koszyka zgodnie z konwencją starego layoutu
And event `prestashop.emit('updatedCart', resp)` został wyemitowany (verified via console listener injected w Playwright)
```
## AC-4: Błąd serwera prezentowany użytkownikowi
```gherkin
Given add-to-cart zwraca error (np. insufficient stock, invalid qty)
When response.hasError === true
Then użytkownik widzi czytelny komunikat błędu (modal lub inline message)
And stan formy zostaje zachowany (piece config nie zresetowany)
And przycisk "Dodaj do koszyka" wraca do stanu enabled (nie utknął w loading)
```
## AC-5: Zero regresji w starym layoucie
```gherkin
Given strona produktu poza IP 89.69.31.86 (REMOTE_ADDR != '89.69.31.86')
When użytkownik konfiguruje piece i klika "Dodaj do koszyka"
Then flow działa identycznie jak przed Plan 02-02 (baseline)
And network payload i sekwencja zdarzeń niezmienione
```
</acceptance_criteria>
<tasks>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 1: Live diagnosis — jaki jest aktualny stan add-to-cart w nowym layoucie</name>
<what-built>
Zanim cokolwiek napiszemy — musimy zobaczyć co SIĘ DZIEJE po kliknięciu przycisku
w nowym layoucie. Hipotezy do zweryfikowania:
H1: PS core handler (`[data-button-action=add-to-cart]`) przechwytuje click, wykonuje POST,
wszystko działa i potrzebujemy tylko cosmetic polish + cart widget listener.
H2: PS core handler nie działa (bo np. szuka `.product-actions` wrapper którego brak,
albo inline-script crash wywala listener przed ready). Trzeba wrapper dodać LUB
napisać własny submit handler.
H3: Handler działa, POST wychodzi, ale response nie triggeruje cart widget refresh.
H4: Handler działa ale fancybox-blocker (custom.js:327-341) nie działa poprawnie
w nowym layoucie (np. `#checkbox-piece` nigdy się nie zaznacza po user flow).
</what-built>
<how-to-verify>
Via Playwright MCP (live env, IP spoofowany do 89.69.31.86 — lub na czyjej maszynie
ma IP dopasowany):
1. Navigate: https://newwalls.pl/[dowolny-produkt] (przy zalogowanym IP 89.69.31.86)
2. Verify new layout loaded: `document.querySelector('.product-variants-data--new') !== null`
3. Simulate user flow:
a. Kliknąć `.piece-summary` → popup otwiera się
b. Ustawić szerokość/wysokość (np. 200x150) w fancybox
c. Kliknąć "Zatwierdź" (lub odpowiednik) w popup → sprawdzić `$('#checkbox-piece').is(':checked')` === true
d. Inject listener dla diagnostyki:
```js
window.__addToCartDiag = { clicks: 0, posts: [], events: [] };
$(document).on('click', '[data-button-action=add-to-cart]', function() {
window.__addToCartDiag.clicks++;
});
const origFetch = window.fetch;
window.fetch = function(...args) {
if (String(args[0]).includes('cart')) window.__addToCartDiag.posts.push(args);
return origFetch.apply(this, args);
};
if (window.prestashop) {
prestashop.on('updatedCart', r => window.__addToCartDiag.events.push(['updatedCart', r]));
}
```
e. Kliknąć "Dodaj do koszyka"
f. Odczekać 2s, odczytać `window.__addToCartDiag` + network tab
4. Dokumentacja wyników:
- Czy POST dotarł do `/cart`?
- Jaki status HTTP + schema response?
- Czy `updatedCart` event emitted?
- Czy cart header counter się zaktualizował?
- Jeśli nic się nie stało — czy jest error w console? Stack trace?
5. Resume-signal: Napisz do plan'u które hipotezy (H1H4) są prawdziwe i który scenariusz implementacji wybrać dla Task 2:
- S1: Tylko dodać listener cart widget (H1 prawda) — minimalna zmiana.
- S2: Wrapper `.product-actions` wokół bloku z buttonem (H2 częściowo) — small template change.
- S3: Własny AJAX submit handler w custom.js (H2 prawda / PS core nie-do-przywrócenia) — więcej kodu.
- S4: Fix fancybox-blocker flow (H4 prawda) — tweak custom.js:327 logic.
</how-to-verify>
<resume-signal>Wybierz: S1 / S2 / S3 / S4 (można łączyć, np. "S2+S1") — albo opisz własny scenariusz jeśli diagnoza ujawni coś spoza hipotez.</resume-signal>
</task>
<task type="auto">
<name>Task 2: Implementacja submit flow według wybranego scenariusza (S1/S2/S3)</name>
<files>themes/ayon/templates/catalog/product.tpl, themes/ayon/assets/js/custom.js</files>
<action>
Na podstawie decyzji z Task 1:
**Jeśli S1 (PS core działa, tylko brakuje cart refresh):**
- W custom.js dodać (lub rozszerzyć istniejący `prestashop.on` block) listener:
```js
if (window.prestashop && typeof prestashop.on === 'function') {
prestashop.on('updatedCart', function(params) {
// Refresh header cart widget if not auto-refreshed by core
if ($('.product-variants-data--new').length === 0) return; // scope: new layout only
// Trigger header cart update — PS core usually handles this, ale defensive:
if (window.prestashop.modules && window.prestashop.modules.blockcart) {
$(document).trigger('blockcart:update', params);
}
});
}
```
- Scope pod marker class żeby stary layout nietknięty.
**Jeśli S2 (wrapper `.product-actions` brakuje):**
- W product.tpl, w gałęzi `{if ... == '89.69.31.86'}`, znaleźć blok
`<div class="product-add-to-cart">` (ok. linia 719 w nowym layoucie)
i owinąć go w `<div class="product-actions">`:
```smarty
<div class="product-actions">
<div class="product-add-to-cart">
...istniejący content...
</div>
</div>
```
- UWAGA: sprawdzić czy klasa `.product-actions` nie wywołuje niechcianych stylów
ze starego SCSS (grep w custom.css). Jeśli tak — zmienić selector CSS na
scope'owany (`body#product .product-variants-data--new .product-actions`)
lub dodać marker class `.product-actions--new` i użyć jej w JS.
**Jeśli S3 (własny submit handler):**
- W custom.js, w obrębie istniejącego `setTimeout(..., 600)` init bloku
(dla nowego layoutu, gated przez `.product-variants-data--new` check),
dodać handler:
```js
$('#add-to-cart-or-refresh').on('submit', function(e) {
if ($('.product-variants-data--new').length === 0) return; // old layout: PS core handles
e.preventDefault();
if (!$('#checkbox-piece').is(':checked')) {
// fallback do istniejącej fancybox blokady — delegacja
$('#add-to-cart-or-refresh button').trigger('click');
return;
}
var $btn = $(this).find('[data-button-action=add-to-cart]');
$btn.prop('disabled', true).addClass('loading');
$.ajax({
url: this.action,
method: 'POST',
data: $(this).serialize() + '&add=1&action=update',
dataType: 'json',
headers: { 'Accept': 'application/json' },
success: function(resp) {
if (resp.hasError || resp.success === false) {
// show error (use existing fancybox or inline)
$.fancybox({ content: (resp.errors || ['Błąd dodawania do koszyka']).join('<br>') });
return;
}
if (window.prestashop && typeof prestashop.emit === 'function') {
prestashop.emit('updatedCart', { resp: resp, reason: { linkAction: 'add-to-cart' } });
}
},
error: function() {
$.fancybox({ content: 'Błąd połączenia. Spróbuj ponownie.' });
},
complete: function() {
$btn.prop('disabled', false).removeClass('loading');
}
});
});
```
- NIE dotykać istniejącego `$('#add-to-cart-or-refresh button').on('click')` z custom.js:327
(blokada "wybierz rozmiar przed add-to-cart") — nadal potrzebna dla AC-2.
- Uwaga: PS core używa `add=1&action=update` dla add-to-cart — sprawdzić empirycznie
(w Task 1 powinno być w network tab jeśli PS core działa gdziekolwiek).
Avoid:
- Modyfikowanie `product-add-to-cart.tpl` partial (współdzielony ze starym layoutem, risk regresji)
- Dodawanie globalnych handlerów `$(document).on(...)` bez scope'owania na new layout
- Nadpisywanie istniejącego click handler z custom.js:327 (blokada fancybox)
</action>
<verify>
Playwright live test:
1. Load produkt w nowym layoucie (IP match)
2. Piece config flow (width=200, height=150, position drag)
3. Click "Dodaj do koszyka"
4. Odczytać `window.__addToCartDiag` + network tab:
- `posts.length >= 1` z body zawierającym `is_crop=1&crop_width=200&crop_height=150&...`
- Response HTTP 200, `success: true`
- Event `updatedCart` emitted
- Header cart counter += 1
</verify>
<done>AC-1, AC-2, AC-3 satisfied (AC-2 nietknięta przez Task 2 — istniejąca blokada nadal działa).</done>
</task>
<task type="auto">
<name>Task 3: Error handling, loading state UX, cross-layout regression check</name>
<files>themes/ayon/assets/js/custom.js, themes/ayon/assets/css/custom.scss</files>
<action>
**Error UX (jeśli S3 w Task 2 — jeśli S1/S2 sprawdzić czy PS core natywnie pokazuje błędy):**
- W custom.js, w submit handler z Task 2, branch `hasError`:
- Wyciągnąć czytelny text z `resp.errors` (array) → `join('<br>')`
- Fallback: "Nie udało się dodać do koszyka. Spróbuj ponownie."
- Pokazać w `$.fancybox({ content: ... })` zgodnie z istniejącym wzorcem (custom.js:330)
**Loading state (wszystkie scenariusze):**
- W custom.scss, scope'owana reguła (~linia koniec pliku):
```scss
body#product .product-variants-data--new {
#add-to-cart-or-refresh button.add-to-cart {
&.loading {
opacity: 0.6;
pointer-events: none;
position: relative;
&::after {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 20px; height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin .6s linear infinite;
}
}
}
}
@keyframes spin { to { transform: rotate(360deg); } }
```
(Skip jeśli `@keyframes spin` już istnieje — grep sprawdzić.)
**Regression test — stary layout:**
- Playwright na URL z REMOTE_ADDR != 89.69.31.86 (np. przez VPN lub override header Host).
- Zweryfikować że flow add-to-cart w starym layoucie działa identycznie jak przed Plan 02-02:
- Klik → POST → cart update → success
- Network payload (pomijając cookies) identyczny z baseline'em (zarejestrować baseline przed Task 2 jako kontrolę)
- Jeśli regresja wykryta → zdiagnozować jaki selektor / listener konflict'uje → scope'ować ciaśniej.
Avoid:
- Dodawanie globalnych styles (wszystkie pod `.product-variants-data--new` scope)
- Zmiana istniejącego submit flow starego layoutu (nawet inline — stary działa, nie dotykamy)
</action>
<verify>
1. Playwright nowy layout: force error (np. Console `fetch('/cart', {...})` z złym tokenem) → fancybox z błędem pokazuje się, button wraca do enabled.
2. Playwright nowy layout: slow-3G throttling → spinner widoczny podczas pending request.
3. Playwright stary layout (IP != 89.69.31.86): add-to-cart flow identyczny z baseline; headless log network + compare.
</verify>
<done>AC-4, AC-5 satisfied.</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `themes/ayon/templates/catalog/_partials/product-add-to-cart.tpl` (współdzielony ze starym layoutem — modyfikacja = ryzyko regresji w produkcji)
- `themes/ayon/templates/catalog/_partials/product-variants.tpl` (shared, Plan 01 closed)
- `themes/ayon/templates/catalog/_partials/product-cover-thumbnails.tpl` (shared, używany przez piece z Plan 02-01)
- Starą gałąź `{if ... != '89.69.31.86'}` w `product.tpl` — całość strukturalnie nietknięta
- Istniejący handler `$('#add-to-cart-or-refresh button').on('click', ...)` z custom.js:327 (blokada fancybox — nadal pełni AC-2)
- Hidden inputy crop/mirror z Plan 02-01 (nazwy/ID są kontraktem)
- `custom.css` — edytujemy wyłącznie `custom.scss` (user ma watcher)
## SCOPE LIMITS
- Ten plan NIE rozwiązuje kalkulacji ceny per-sqm w nowym layoucie (`totalpriceinfospecific` no-op override z Plan 02-01) — to osobny plan (Plan 02-03 kandydat).
- Ten plan NIE wypełnia pustych bloków (`.product-protect`, `.product-installation`, `.product-order-sample`) — osobny plan (Plan 02-04+).
- Ten plan NIE dodaje "quick view" / product modalu — zakres strony produktu tylko.
- Ten plan NIE zmienia serwer-side logic (add-to-cart controller) — tylko client + template.
- Brak nowych zależności npm / composer — wykorzystujemy istniejący stack (jQuery, fancybox, PS core).
</boundaries>
<verification>
Przed declaration complete (UNIFY gate):
- [ ] AC-1 verified Playwright: POST wychodzi z pełnym body, success response.
- [ ] AC-2 verified Playwright: blokada fancybox gdy piece nie wybrany.
- [ ] AC-3 verified Playwright: cart counter w headerze aktualizuje się.
- [ ] AC-4 verified Playwright: error response → czytelny komunikat, button enabled.
- [ ] AC-5 verified Playwright: stary layout bez regresji (baseline diff clean).
- [ ] `custom.scss` → `custom.css` compile clean (user watcher lub manual check po commit).
- [ ] Grep: zero zmian w plikach starego layoutu / shared partial'ach (git diff).
- [ ] `$('#add-to-cart-or-refresh').serialize()` w nowym layoucie zawiera wszystkie 8 hidden input'ów + token + id_product + id_customization + qty.
</verification>
<success_criteria>
- Wszystkie 5 AC pass w live Playwright test.
- Zero regresji w starym layoucie (Playwright + git diff check).
- Zero nowych dependencies.
- `custom.js` zmiany dodatkowe (nie modyfikujące istniejących handlerów) — diff czysty, scope'owany.
- SUMMARY.md wystawiony z: co działa, co odroczone (cena = Plan 02-03), problemy napotkane.
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md`
</output>

View File

@@ -0,0 +1,143 @@
---
phase: 02-product-actions-fixes
plan: 02
subsystem: ui+backend-integration
tags: [prestashop, smarty, jquery, ajax, add-to-cart, squaremeter, customization, cache]
status: PARTIAL (core flow działa, ale end-to-end UX wymaga Plan 02-03 — customization nie zapisuje się i modal nie pojawia)
requires:
- phase: 02-product-actions-fixes
provides:
- Plan 02-01 form `#add-to-cart-or-refresh` z hidden inputs (crop/mirror) — wykorzystane jako baseline POST payload
- Plan 02-01 marker class `.product-variants-data--new` — użyty do scope'owania handler'a
provides:
- Capture-phase click handler na `[data-button-action=add-to-cart]` w `custom.js` (+inline mirror w `product.tpl` jako cache-buster)
- Blokuje PS core handler (ktory crash'owal bo button poza forma) przez `stopImmediatePropagation` na capture phase
- Manualny POST do form.action z `form.serialize() + qty + add=1&action=update`
- Fancybox-blocker port (walidacja ze piece wybrany przed submit)
- Sync is_crop/crop_width/crop_height przed POST (obejście crash'u checkedHandler)
- `prestashop.emit('updatedCart')` + blockcart refresh fetch
- Loading spinner + success flash animation
affects:
- Plan 02-03 (cena + customization + modal) — **BLOKER dla production readiness**:
- Customization nie zapisuje się bo `squaremeter::hookActionObjectCartUpdateBefore` wymaga `discretion=on` + squaremeter fields (`dim`, `qty`, `qty_alth`, `product_total_price_calc`, `extrafeevalue`, `wastevalue`, `calculated_total`, etc.)
- Plan 02-01 override `totalpriceinfospecific` wyłączył synchronizację tych pól w nowym layoucie
- Success modal (po add-to-cart) wymaga osobnego POST do `/module/ps_shoppingcart/ajax?action=add-to-cart` → renderowany przez `Ps_Shoppingcart::renderModal()`
- W koszyku brakuje "Szczegóły" button bo cart.id_customization = 0
tech-stack:
added: []
patterns:
- "Capture-phase native addEventListener z useCapture=true — jedyna metoda blokowania PS core delegated bubble handlers w jQuery.on()"
- "Inline script w product.tpl jako cache-buster dla handler'a, guard'owany `window.__<flag>Bound` — immune na browser cache statycznych assetow"
- "Stopniowa diagnoza structure-first (DOM traversal → event phases → POST payload) w Playwright przed zmianami kodu"
key-files:
created:
- .paul/phases/02-product-actions-fixes/02-02-PLAN.md
- .paul/phases/02-product-actions-fixes/02-02-SUMMARY.md
modified:
- themes/ayon/assets/js/custom.js (wrapped Plan 02-02 block w `if (!window.__p02p02Bound) { ... }` guard, ~115 linii)
- themes/ayon/assets/css/custom.scss (+loading spinner + added-flash, ~42 linie)
- themes/ayon/templates/catalog/product.tpl (+inline handler mirror przed `{/if}` new-layout, ~95 linii) — cache-buster
key-decisions:
- "Task 1 diagnoza via Playwright → S3 (własny AJAX submit) — struktura DOM: button+qty są poza forma, PS core `closest('form')` zwraca 0 elementów, POST nigdy nie wychodzi"
- "Capture-phase native addEventListener zamiast jQuery `.on()` — eliminuje double-POST (PS core registered first, jQuery stopPropagation nie mogło cofnąć wcześniejszej rejestracji)"
- "Manual sync is_crop/crop_width/crop_height w handlerze — obejście crash'u checkedHandler (totalpriceinfospecific override z 02-01 nie działa w produkcji)"
- "Inline script w product.tpl + idempotency guard — cache-buster dla <script src=custom.js> bez modyfikacji gdzieśtam rejestracji"
- "UNIFY jako PARTIAL — szczerze raportować że customization + modal wymagają Plan 02-03 z powodu zależności od squaremeter flow (ktory 02-01 wyłączyło no-op override'em)"
patterns-established:
- "Capture-phase handler na window events: `document.addEventListener('click', fn, true)` — dla blokowania globalnych delegated handlerów PS core bez modyfikacji core.js"
- "Idempotency guard flag `window.__<planId>Bound` — pozwala duplikować handler (custom.js + inline template) bez double-execution"
- "HTML response inline <script> jako cache-buster dla statycznych assetów — immune na browser cache + PS 1.7 Smarty cache invaliduje sie na kolejnej pełnej pre-renderyzacji"
duration: ~4h (Task 1 diagnoza 30min, Task 2 iteracje 2h, Task 3 CSS 15min, debugging cache+customization discovery 1.5h)
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: POST dociera do /koszyk z pełnym payloadem | **Pass** | Playwright verified: POST z is_crop=1, crop_width/height, qty, add=1, action=update. Response success:true. |
| AC-2: Fancybox-blocker bez piece config | **Pass (code review)** | Straight-line if-return logic w handler'ze. Live test session pollution blokowała izolowany test, ale logika prosta i sprawdzona w kodzie. |
| AC-3: Cart widget counter się odświeża | **Pass** | Cart-products-count aktualizowany z `"000"` na `"1"` po POST. Mechanizm: `prestashop.emit('updatedCart')` + manual blockcart fetch. |
| AC-4: Error response → czytelny komunikat | **Pass (code review)** | Handler parse'uje `resp.errors` (array lub object), pokazuje fancybox. Symmetric do success path. Nie live-tested bo wymagałoby symulacji error response. |
| AC-5: Zero regresji starego layoutu | **Pass (by design)** | Early return `if (!document.querySelector('.product-variants-data--new')) return` — stary layout nie wywołuje preventDefault/stop, PS core handler wykonuje się normalnie. |
## Accomplishments
- **Diagnoza struktury DOM** — ustalenie że forma i button są w rozdzielnych kontenerach Bootstrap (form w sidebar .col-md-6, button w szerokim .product-bar). PS core handler nie może dotrzeć do formy.
- **Capture-phase breakthrough** — pierwsza iteracja (jQuery `.on()`) powodowała double-POST. Refactor na native addEventListener z `useCapture=true` w pełni blokuje PS core. Verified `nativeClicks:1, delegated:0`.
- **Cache immunity** — identyfikacja browser cache jako root cause "niby zadziałało ale u użytkownika nie". Rozwiązanie: inline script w template + idempotency guard pozwala na parallel deploys (custom.js + inline) bez konfliktu.
- **Głęboka diagnoza customization flow** — rozpoznanie że PS 1.7 + squaremeter używa dwóch dróg: cart controller override + `hookActionObjectCartUpdateBefore` gated przez `discretion=on`. Success modal przez `/module/ps_shoppingcart/ajax?action=add-to-cart`. Oba wymagają pól których Plan 02-01 nie dostarcza.
## Deviations from Plan
### Auto-fixed Issues
**1. Double POST — PS core + nasz handler oba wysyłają**
- **Found during:** Task 2 first live test — `posts: [2 entries]`, oba identyczne.
- **Issue:** jQuery `$(document).on('click', ..., )` registered AFTER PS core → stopImmediatePropagation w naszym handlerze nie blokowało PS core (registered earlier).
- **Fix:** Refactor z jQuery `.on()` na native `document.addEventListener('click', fn, true)` (capture phase). Capture fires PRZED bubble, blokuje PS core całkowicie. Verified `nativeClicks:1, delegated:0`.
- **Files:** `themes/ayon/assets/js/custom.js`
**2. Cart widget counter nie odświeżał się po emit('updatedCart')**
- **Found during:** Task 2 live test — response success ale `.cart-products-count` pokazywał `"000"`.
- **Issue:** Natural PS blockcart listener nie jest zarejestrowany w nowym layoucie / nie reaguje.
- **Fix:** Dodany manual `$.get('/koszyk', {action:'refresh', ajax:1})` po successful POST → defensive replace `.blockcart` z response. W real teście counter zaktualizował się (`"000"``"1"`) — prawdopodobnie natural listener też odpalił + nasz backup.
- **Files:** `themes/ayon/assets/js/custom.js`
**3. Browser cache serwował starą wersję custom.js**
- **Found during:** User raport po FTP deploy — "klikam i nic się nie dzieje". Diagnoza Playwright: `perfEntries[0].decodedBodySize = 30.6 KB` vs server `fetch.size = 41.3 KB`, `transferSize: 0` (from cache).
- **Issue:** `<script src="custom.js">` rejestrowany bez query param (version). Browser cache serwuje stale.
- **Fix (częściowy):** Dodany inline mirror handler w product.tpl new-layout branch. HTML response zawsze zawiera świeży kod. Idempotency guard `window.__p02p02Bound` zapobiega double-register jeśli cached custom.js też zawiera handler.
- **Pozostało:** Systemowy cache-buster dla `custom.js` `<script>` tag — defer do Plan 02-03 razem z innymi cross-cutting concerns.
- **Files:** `themes/ayon/templates/catalog/product.tpl`
### Scope Additions
- **Inline script cache-buster w product.tpl** — nie planowane w Task 2, ale niezbędne po odkryciu że browser cache blokuje deploy.
- **Głęboka diagnoza squaremeter flow** — rozpoznanie że customization zapis wymaga `discretion=on` + dimension fields. Rezultat: identyfikacja zakresu Plan 02-03.
### Deferred Items → Plan 02-03
- **Customization save w koszyku** — crop/mirror data (+ dimension gdy zaimplementowane) musi utworzyć customization z in_cart=0 w DB żeby user widział "Szczegóły" button w cart. Wymaga POST z `discretion=on` + squaremeter fields.
- **Success modal po add-to-cart** — POST do `/module/ps_shoppingcart/ajax?action=add-to-cart` → render modal z "Kontynuuj zakupy / Przejdź do koszyka".
- **Cena per-sqm kalkulacja w nowym layoucie** — wymaga przywrócenia squaremeter dimension flow (zamiast no-op override z Plan 02-01). Powiązane z customization bo `product_total_price_calc` jest jednym z pól.
- **Systemowy cache-buster `custom.js`** — zamiast inline duplicate, dodać `?v={mtime}` do rejestracji script tag'a (znaleźć miejsce rejestracji: theme.yml, FrontController override, lub PS hook).
- **Plan 02-01 override `totalpriceinfospecific` live-debug** — w produkcji override nie jest aktywny (ORYGINAL function crashes na klik piece-summary). Root cause: setTimeout(600) timing vs inline ready callbacks. Do naprawy w Plan 02-03 razem z przywróceniem squaremeter flow.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| PS core handler registered before us → jQuery stopPropagation nie blokuje PS core | Capture-phase native addEventListener (useCapture=true) fires PRZED bubble phase PS core — całkowicie go blokuje. |
| Browser cache servuje starą wersję custom.js mimo że server ma nową (FTP sync zakończony, marker "Phase 02 Plan 02-02" obecny server-side) | Inline script mirror w product.tpl + idempotency guard. HTML response serwuje świeży kod z każdym request. |
| Playwright test session state leakage (poprzednie handlery persist'ują mimo navigate) | `page.goto()` wyglada jak full reload ale cached JS execution state może przetrwać przy hash-only URL changes. Rozwiązanie: bardziej aggressive cache busting przez navigate na URL bez fragmentu. |
| Customization nie zapisuje się mimo że is_crop/crop data są w POST payload | Squaremeter hook `hookActionObjectCartUpdateBefore` jest gate'owany przez `discretion=on` — brak tego pola w naszym POST. Delegowane do Plan 02-03. |
| Success modal nie pojawia się | PS core post-success flow oczekuje osobnego response z `modal` key z endpointa `/module/ps_shoppingcart/ajax`. Nasz POST idzie tylko do `/koszyk`. Delegowane do Plan 02-03. |
## Skill audit
Użyte w Plan 02-02:
- **Playwright MCP** — krytyczne. Bez live debug (event phase tracing, network capture, DOM inspection, iterowanie handlera bez FTP deploy cycle) nie dałoby się znaleźć capture-phase fix'u ani diagnoza customization flow. Wiele iteracji.
- **context-mode** — ctx_batch_execute/ctx_execute_file do eksploracji PS core (core.js), squaremeter source (override controllers + hooks) bez zanieczyszczania context.
Pattern established: **Structure-first diagnosis** — zanim jakakolwiek implementacja, sprawdzić DOM (form-button relationship), event flow (capture/bubble), POST payload (what fields). Oszczędza cykle debug.
## Next Phase Readiness
**Ready:**
- Capture-phase pattern reużywalny dla kolejnych globalnych event overrides (modal close, cart update, etc.)
- Inline template cache-buster pattern reużywalny dla wszystkich JS zmian które wymagają "always fresh" deploy
- Idempotency guard pattern dla duplikowanych handler'ów
**Concerns:**
- Plan 02-02 feature jest **PARTIAL**: add-to-cart technicznie działa (produkt w koszyku), ale **niezadowalające UX**: brak modal potwierdzenia, brak customization details → user nie widzi co zamówił w szczegółach.
- Plan 02-01 override `totalpriceinfospecific` aktywnie uszkadza squaremeter flow → cascade do cena + customization.
- Real user flow na produkcji nadal wymaga Plan 02-03 do "acceptable" state.
**Blockers dla publikacji nowego layoutu:** Plan 02-03 (modal + customization + cena) MUST be done przed umożliwieniem layout-u zwykłym użytkownikom (usunięcie IP gate).

View File

@@ -0,0 +1,340 @@
---
phase: 02-product-actions-fixes
plan: 03
type: execute
wave: 1
depends_on: ["02-01", "02-02"]
files_modified:
- themes/ayon/assets/js/custom.js
- themes/ayon/templates/catalog/product.tpl
autonomous: false
delegation: off
---
<objective>
## Goal
Dokończyć end-to-end add-to-cart UX w nowym layoucie: (1) customization crop/mirror/dimensions zapisuje się w DB i jest widoczna w koszyku jako "Szczegóły", (2) success modal z opcjami "Kontynuuj zakupy / Przejdź do koszyka" pokazuje się po udanym dodaniu. Parzystość funkcjonalna ze starym layoutem dla tych dwóch punktów.
## Purpose
Plan 02-02 uruchomił POST, ale bez tych dwóch punktów nowy layout jest nieakceptowalny dla klienta końcowego: nie widzi potwierdzenia akcji (modal) ani co zamówił (customization details w cart). Jest to **blocker dla usunięcia IP gate** (`REMOTE_ADDR == '89.69.31.86'`) i publikacji layout'u szerszemu audytorium.
## Output
- Wszystkie wymagane squaremeter hidden inputs w POST payload (`discretion=on`, `dim`, `qty`, `qty_alth`, `product_total_price_calc`, `id_product_attribute`, +opcjonalne) obliczone z piece state + product data.
- Po udanym cart update → drugi POST do `/module/ps_shoppingcart/ajax?action=add-to-cart` → render modal z response.modal HTML w fancybox (lub native PS modal container).
- Koszyk po reload pokazuje "Szczegóły" button przy produkcie, klik rozwija customization data (wymiary, is_crop, is_reflection).
- Plan 02-01 override `totalpriceinfospecific` POZOSTAJE (nie ruszamy — bypass, nie restore). Cena kalkulacja może być nieprecyzyjna w tym planie (defer do Plan 02-04).
- Summary: `.paul/phases/02-product-actions-fixes/02-03-SUMMARY.md`.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/02-product-actions-fixes/02-01-SUMMARY.md
@.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md
## Source Files
@themes/ayon/templates/catalog/product.tpl
@themes/ayon/assets/js/custom.js
@modules/squaremeter/squaremeter.php
# Line 843+: hookActionObjectCartUpdateBefore — gate'owany `discretion=on`, czyta:
# dim, qty, qty_alth, qty_alt, qty_altd, extrafeevalue, wastevalue,
# product_total_price_calc, calculated_total, grand_calculated_total,
# converted_ea, directinput
@modules/squaremeter/override/classes/Cart.php
# _addCustomization2(): INSERT INTO customization + customized_data
@modules/squaremeter/override/modules/ps_shoppingcart/controllers/front/ajax.php
# Endpoint dla `action=add-to-cart` → renderModal() zwraca modal HTML
@modules/squaremeter/views/templates/front/header_surface.tpl
# Stary flow squaremeter — inspiracja jakie fields + jak computed
</context>
<skills>
## Required Skills
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| Playwright MCP (`mcp__plugin_playwright_playwright__*`) | required | Task 1 (capture OLD layout POST), Task 3 regression | ○ |
| context-mode (`mcp__plugin_context-mode_context-mode__*`) | optional | Eksploracja squaremeter hooks + PS endpoints | ○ |
**BLOCKING:** Playwright MCP wymagany w Task 1 — musimy zobaczyć EXACT payload starego layoutu (fields + wartości) zanim zainżynierujemy mapowanie w nowym.
## Skill Invocation Checklist
- [ ] Playwright MCP dostępny
- [ ] Dostęp do testowego środowiska gdzie można przełączyć layouty (IP switch lub URL param)
</skills>
<acceptance_criteria>
## AC-1: Customization zapisuje się podczas add-to-cart
```gherkin
Given user skonfigurował piece (width=200, height=150) w nowym layoucie
And formularz ma is_crop=1, crop_width=200, crop_height=150, piece_bg_top/left
When user klika "Dodaj do koszyka"
Then POST payload zawiera `discretion=on&dim=200x150&qty=200&qty_alth=150&product_total_price_calc=<value>&id_product_attribute=<value>`
And response HTTP 200 z `id_customization > 0`
And w DB tabela `customization` ma nowy wiersz z `id_cart=<active>&id_product=202&in_cart=1`
And w DB tabela `customized_data` ma wiersz z `index=<WD_CUSTOMIZATION_INDEX>&value='200x150'`
```
## AC-2: Success modal pokazuje się po dodaniu
```gherkin
Given udany cart update z AC-1
When backend potwierdzi success
Then wychodzi drugi POST do `/module/ps_shoppingcart/ajax?action=add-to-cart` z `id_product`, `id_product_attribute`, `id_customization`
And response zawiera `modal: '<HTML>...'`
And modal renderuje się (w fancybox lub native container) z:
- Nazwą produktu
- Podziękowaniem / komunikatem success
- Dwoma przyciskami: "Kontynuuj zakupy" (zamyka modal) i "Przejdź do koszyka" (nawiguje do /pl/koszyk)
```
## AC-3: Koszyk pokazuje "Szczegóły" button + customization data
```gherkin
Given product dodany przez nowy layout z customization
When user nawiguje do /pl/koszyk
Then cart item ma button/link "Szczegóły" (lub zwinięty blok z danymi)
And kliknięcie rozwija customization:
- Wymiary: "200 x 150 cm"
- Czy fragment: tak/nie (is_crop)
- Czy odbicie lustrzane: tak/nie (is_reflection)
```
## AC-4: Brak double-add (handler idempotency po success)
```gherkin
Given user klika "Dodaj do koszyka" jedno klikniecie
When handler przetwarza
Then wychodzi dokładnie 1 POST do /koszyk + 1 POST do /module/ps_shoppingcart/ajax
And w koszyku jest 1 produkt (nie 2)
```
## AC-5: Zero regresji starego layoutu
```gherkin
Given strona produktu poza IP 89.69.31.86
When user konfiguruje piece + dimensions + klika "Dodaj do koszyka"
Then flow identyczny jak przed Plan 02-03 (baseline network capture)
And customization save nadal działa (existing feature)
And modal nadal działa (existing feature)
```
</acceptance_criteria>
<tasks>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 1: Capture OLD layout POST payload (ground truth)</name>
<what-built>
Nothing coded yet — live data capture. Musimy zobaczyć DOKŁADNIE jakie fields stary layout
wysyła przy add-to-cart, z jakimi wartościami, w jakiej kolejności. Bez tego ryzyko
przegapienia wymaganego pola (np. `directinput`, `qty_alt`, `calculated_total`).
</what-built>
<how-to-verify>
Via Playwright MCP — OLD layout (IP != 89.69.31.86, np. przez URL param `?_test_old=1`
jeśli jest albo tymczasowo komentować IP check w product.tpl dla tej sesji):
1. Navigate: dowolny produkt na starym layoucie
2. Install diagnostic:
```js
window.__oldPayload = null;
const _ajax = jQuery.ajax;
jQuery.ajax = function(cfg) {
const url = (typeof cfg === 'string') ? cfg : cfg && cfg.url;
if (String(url).match(/cart|koszyk|ps_shoppingcart/) && cfg.type === 'POST') {
window.__oldPayload = { url, data: cfg.data, headers: cfg.headers };
}
return _ajax.apply(jQuery, arguments);
};
```
3. Sformułuj pełny user flow: wybierz wariant kolorystyczny + materiał + wprowadź wymiary (np. 200×150) + ustaw piece position + kliknij "Dodaj do koszyka"
4. Odczytaj `window.__oldPayload` + sprawdź Network tab dla wszystkich XHR/fetch do `cart` i `ps_shoppingcart`
5. Dokumentacja wszystkich parametrów:
- Pierwszy POST (cart update): wszystkie name=value pairs
- Drugi POST (modal fetch if exists): endpoint + payload
- Suffix pattern (`_<id_product>` lub brak)
- Format `dim` (np. "200x150" vs "200X150" vs "200 x 150")
- Czy `product_total_price_calc` jest integer czy decimal
6. Zapisać w komentarzu checkpoint'a listę fields z przykładowymi wartościami
Resume-signal: wklej dokładne pole-wartość z OLD POST + zatwierdź że to jest wzór
który naśladujemy w Task 2.
</how-to-verify>
<resume-signal>Podaj fields z OLD POST payload (przykład: `discretion=on&dim=200x150&qty=200&qty_alth=150&product_total_price_calc=597.00&...`) LUB flag "use sensible defaults" jeśli nie masz dostępu do OLD layoutu na prod.</resume-signal>
</task>
<task type="auto">
<name>Task 2: Inject squaremeter fields do POST payload w nowym layoucie</name>
<files>themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl</files>
<action>
W OBU miejscach (handler w custom.js linie 993-1113 + inline mirror w product.tpl
linie 757+) zmodyfikuj payload construction:
Przed `$.ajax({ ... data: payload })`:
```js
// Phase 02 Plan 02-03: inject squaremeter fields for customization save
var pieceW = parseInt($('#piece-width').val(), 10) || 100;
var pieceH = parseInt($('#piece-height').val(), 10) || 100;
var idProductAttribute = parseInt($('input[name=id_product_attribute], #idCombination').val(), 10)
|| window.product_page_product_combination
|| 0;
// Price calc: use product base price × area (m²) as fallback.
// Exact price jest Plan 02-04 scope — tu chodzi tylko o niezero value żeby hook przeszedł.
var areaM2 = (pieceW / 100) * (pieceH / 100);
var basePrice = parseFloat($('#product_base_price, #product_fixed_price').val())
|| parseFloat($('.product-prices .current-price').first().text().replace(/[^\d.,]/g, '').replace(',', '.'))
|| 0;
var totalPriceCalc = (basePrice * areaM2).toFixed(2);
var sqFields = 'discretion=on'
+ '&dim=' + pieceW + 'x' + pieceH
+ '&qty=' + pieceW // squaremeter qty = width (NIE cart quantity!)
+ '&qty_alth=' + pieceH
+ '&product_total_price_calc=' + totalPriceCalc
+ '&id_product_attribute=' + idProductAttribute
+ '&calculated_total=' + areaM2.toFixed(4)
+ '&grand_calculated_total=' + areaM2.toFixed(4);
payload = sqFields + '&' + payload; // prepend, bo niektóre pola mogą się powtórzyć — ostatni wygrywa w PHP
```
**Adjust payload** żeby NASZ qty (cart quantity) nie był nadpisywany. PS uses `qty` jako
product quantity; squaremeter czyta `qty` jako dimension width. Konflikt!
Rozwiązanie: nie wysyłać cart `qty=1` bezpośrednio; zamiast tego dodać je jako
`quantity_wanted=1` lub upewnić się że nasz `qty` (dimension) jest the one in POST.
**Task 1 da odpowiedź co stary layout robi tutaj — może używa suffix** (`qty_<id_product>`)
dla dimension a `qty` dla cart quantity.
Avoid:
- Zmiana nazwy cart quantity field (PS core by tego nie zaakceptował)
- Hardcode basePrice (read dynamically)
- Zakładanie że `id_product_attribute` jest w formie (może być w innej formie lub
jako Smarty var — Task 1 pokaże)
Po Task 1 adjust field list do tego co stary POST zawiera — możliwe że są DODATKOWE
pola których tu nie wymieniliśmy (np. `directinput`, `converted_ea`).
</action>
<verify>
Playwright live test nowego layoutu:
1. Navigate + piece config
2. Click add-to-cart
3. Network tab: sprawdź POST do /koszyk — payload zawiera `discretion=on&dim=200x150&qty=200&qty_alth=150&product_total_price_calc=<non-zero>&id_product_attribute=<non-zero>`
4. Response: `success:true, id_customization: <non-zero>`
5. DB check (via PS admin SQL): `SELECT * FROM ps_customization WHERE id_cart = <cart_id>` → 1 wiersz, `in_cart=1` po finalizacji
</verify>
<done>AC-1 satisfied.</done>
</task>
<task type="auto">
<name>Task 3: Chain POST do ps_shoppingcart/ajax + render modal</name>
<files>themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl</files>
<action>
W success callback po udanym cart POST (w OBU miejscach: custom.js + inline product.tpl),
po `emit('updatedCart')`, przed lub zamiast blockcart refresh:
```js
// Phase 02 Plan 02-03: fetch success modal
if (resp && resp.success && resp.id_product) {
$.ajax({
url: '/module/ps_shoppingcart/ajax',
type: 'POST',
data: {
action: 'add-to-cart',
id_product: resp.id_product,
id_product_attribute: resp.id_product_attribute,
id_customization: resp.id_customization
},
dataType: 'json',
success: function(modalResp) {
if (modalResp && modalResp.modal) {
$.fancybox({
content: modalResp.modal,
minWidth: 400,
maxWidth: 800,
padding: 20,
autoSize: true
});
}
if (modalResp && modalResp.preview) {
$('.blockcart').replaceWith(modalResp.preview);
}
}
});
}
```
**Uwaga:** endpoint path `/module/ps_shoppingcart/ajax` może być w PL jako
`/module/ps_shoppingcart/ajax` (path nie tłumaczony) ale warto sprawdzić w Task 1
(zobaczyć w Network czy URL jest exactly ten).
Jeśli modal HTML używa custom CSS (PS blockcart modal), może być problem z
stylowaniem w fancybox. Alternatywa: append `modal` HTML do `<body>` + show jako
native PS modal (Bootstrap `.modal.show`). Task 1 pokaże jak stary layout renderuje.
Avoid:
- Wywołanie obu: modal fetch + blockcart refresh (redundantnie) — modal resp już ma `preview`
- Zakładanie `resp.id_product` — PS może używać `idProduct` (camelCase) w JSON response
Po modal render, zbędny staje się nasz custom `.added-flash` animation. Zostaw
jako fallback gdy modal się nie zarenderuje.
</action>
<verify>
Playwright live test:
1. Piece config + click add-to-cart
2. Network: 2 POSTy — pierwszy do /koszyk, drugi do /module/ps_shoppingcart/ajax
3. DOM: modal z response.modal HTML pokazuje się jako fancybox lub native modal
4. Modal zawiera:
- Nazwę produktu
- Link "Kontynuuj zakupy" (zamyka modal)
- Link "Przejdź do koszyka" (wskazuje na /pl/koszyk)
5. Navigate to /pl/koszyk → cart pokazuje "Szczegóły" button → klik rozwija customization data
</verify>
<done>AC-2, AC-3, AC-4 satisfied.</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- Plan 02-01 override `totalpriceinfospecific` / `prod` no-op — nadal potrzebne żeby piece popup nie crash'ował. NIE restaurować squaremeter flow tutaj (zbyt ryzykowne + duży scope).
- `modules/squaremeter/*` — nie modyfikować modułu, żaden override ani edit.
- `themes/ayon/templates/catalog/_partials/*` — shared partials, nie ruszać.
- Stara gałąź `{if ... != '89.69.31.86'}` w `product.tpl` — nietknięta.
- Istniejący `$('#add-to-cart-or-refresh button').on('click')` w custom.js:327 — zostaje (niedziała w nowym, ale aktywne w starym).
- Istniejący capture-phase handler z Plan 02-02 — rozbudowa w miejscu, nie rewrite.
## SCOPE LIMITS
- Ten plan NIE dodaje UI dla live price calculation — cena w modalu/cart może być pokazana jako calculated value (read-only) ale bez UI do zmiany dimensions po dodaniu do cart. **Plan 02-04 scope.**
- Ten plan NIE dodaje dimension UI w product page (dimension input niezależny od piece popup) — user podaje wymiar przez piece popup (obecny mechanizm).
- Ten plan NIE restauruje squaremeter JS flow (header_surface.tpl etc.) — używamy BYPASS podejścia (bezpośrednio injection squaremeter fields w POST).
- Ten plan NIE rozwiązuje systemowego cache-buster dla `custom.js` — inline mirror z Plan 02-02 pozostaje. **Plan 02-05 scope.**
- Brak nowych dependencies.
</boundaries>
<verification>
Przed declaration complete (UNIFY gate):
- [ ] AC-1 verified Playwright: POST zawiera wszystkie squaremeter fields, response `id_customization > 0`.
- [ ] AC-2 verified Playwright: modal pokazuje się po add-to-cart, ma nazwy produktu + linki.
- [ ] AC-3 verified Playwright: w /pl/koszyk widać "Szczegóły" button, klik rozwija dane.
- [ ] AC-4 verified Playwright: single add per click, cart qty = 1 (nie 2).
- [ ] AC-5 verified Playwright: stary layout bez regresji (baseline network capture identyczny).
- [ ] Git diff clean: zero zmian w `modules/squaremeter/`, shared partials, starym layoucie.
- [ ] Kod identyczny w OBU miejscach (custom.js + product.tpl inline mirror) — diff między nimi = zero funkcjonalnych różnic.
</verification>
<success_criteria>
- Wszystkie 5 AC pass w live Playwright test.
- Zero regresji w starym layoucie.
- Zero modyfikacji w module squaremeter.
- Plan 02-01 override nadal aktywny (nie przywracamy squaremeter JS).
- SUMMARY.md dokumentuje co działa, co deferred (Plan 02-04 cena UI, Plan 02-05 cache-buster).
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-03-SUMMARY.md`
</output>