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

341 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>