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

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

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

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

19 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
phase plan type wave depends_on files_modified autonomous delegation
02-product-actions-fixes 02 execute 1
02-01
themes/ayon/templates/catalog/product.tpl
themes/ayon/assets/js/custom.js
themes/ayon/assets/css/custom.scss
false 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)

<acceptance_criteria>

AC-1: Add-to-cart button submitowalny w nowym layoucie

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

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

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

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

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>

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 (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.
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
  `<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)
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.

<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>
Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md`