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>
341 lines
16 KiB
Markdown
341 lines
16 KiB
Markdown
---
|
||
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>
|