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,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).