--- 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 --- ## 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`. ## 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 ## Required Skills | 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) ## 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 ``` Task 1: Live diagnosis — jaki jest aktualny stan add-to-cart w nowym layoucie 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). 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 (H1–H4) 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. Wybierz: S1 / S2 / S3 / S4 (można łączyć, np. "S2+S1") — albo opisz własny scenariusz jeśli diagnoza ujawni coś spoza hipotez. Task 2: Implementacja submit flow według wybranego scenariusza (S1/S2/S3) themes/ayon/templates/catalog/product.tpl, themes/ayon/assets/js/custom.js 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 `
` (ok. linia 719 w nowym layoucie) i owinąć go w `
`: ```smarty
...istniejący content...
``` - 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('
') }); 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) 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 AC-1, AC-2, AC-3 satisfied (AC-2 nietknięta przez Task 2 — istniejąca blokada nadal działa). Task 3: Error handling, loading state UX, cross-layout regression check themes/ayon/assets/js/custom.js, themes/ayon/assets/css/custom.scss **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('
')` - 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)
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. AC-4, AC-5 satisfied.
## 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). 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. - 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. Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md`