Files
newwalls.pl/.paul/phases/02-product-actions-fixes/02-03-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

16 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 03 execute 1
02-01
02-02
themes/ayon/assets/js/custom.js
themes/ayon/templates/catalog/product.tpl
false off
## 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.
## 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

## 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)

<acceptance_criteria>

AC-1: Customization zapisuje się podczas add-to-cart

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

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

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)

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

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>

Task 1: Capture OLD layout POST payload (ground truth) 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`). 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.
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. Task 2: Inject squaremeter fields do POST payload w nowym layoucie themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl 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`).
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=&id_product_attribute=` 4. Response: `success:true, id_customization: ` 5. DB check (via PS admin SQL): `SELECT * FROM ps_customization WHERE id_cart = ` → 1 wiersz, `in_cart=1` po finalizacji AC-1 satisfied. Task 3: Chain POST do ps_shoppingcart/ajax + render modal themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl 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.
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 AC-2, AC-3, AC-4 satisfied.

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

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