--- 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 --- ## 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) ## 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=&id_product_attribute=` And response HTTP 200 z `id_customization > 0` And w DB tabela `customization` ma nowy wiersz z `id_cart=&id_product=202&in_cart=1` And w DB tabela `customized_data` ma wiersz z `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: '...'` 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) ``` 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 (`_` 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_`) 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 `` + 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. - 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). Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-03-SUMMARY.md`