--- phase: 02-product-actions-fixes plan: 04 type: execute wave: 1 depends_on: ["02-03"] files_modified: - themes/ayon/assets/js/custom.js - themes/ayon/templates/catalog/product.tpl autonomous: false delegation: off --- ## Goal Live cena per-sqm w UI nowego layoutu: zmiana `#piece-width` / `#piece-height` → natychmiastowe przeliczenie ceny w `.product-prices .current-price` (oraz `.product-prices-data` jeśli istnieje). Formula: `base_price × area_m²`, gdzie `base_price` czytane z `meta[property="product:price:amount"]` (fallback do text'u obecnej ceny jeśli meta brak). ## Purpose Po Plan 02-03 POST payload ma poprawnie policzoną cenę backend (`product_total_price_calc`), ale UI wyświetla statycznie "Od 239 zł / m²". User nie widzi konkretnej sumy zanim nie kliknie "Dodaj do koszyka" → modal. Live price daje klientowi konkretny koszt podczas konfiguracji — kluczowy UX step dla custom-sized products. ## Output - Nowa funkcja `recalcProductPrice()` w custom.js (i inline mirror w product.tpl). - Event listeners na `#piece-width` / `#piece-height` (input, change) + initial render na `DOMContentLoaded`. - Re-render price po AJAX variant refresh (Plan 01-01 `action=refresh` emituje `updatedProduct` — hook na ten event). - Zero zmian w POST payload / cart flow (Plan 02-03 już poprawny). - Summary: `.paul/phases/02-product-actions-fixes/02-04-SUMMARY.md`. ## Project Context @.paul/PROJECT.md @.paul/ROADMAP.md @.paul/STATE.md ## Prior Work (direct dependency) @.paul/phases/02-product-actions-fixes/02-03-SUMMARY.md # Formula i basePrice fallback ustalone w 02-03 (lines w custom.js po Plan 02-03 mają: # basePrice = meta[property="product:price:amount"] || .current-price text # areaM2 = (piece_w_cm/100) * (piece_h_cm/100) # totalPriceCalc = Math.round(basePrice × areaM2) # Reużywamy ten sam algorytm dla UI display. ## Source Files @themes/ayon/templates/catalog/product.tpl # Line 768-928: inline mirror NEW layout (po Plan 02-03 block move fix — teraz faktycznie renderuje) @themes/ayon/assets/js/custom.js # Line 1026+: Plan 02-02/02-03 add-to-cart handler — basePrice + area calc już istnieją # Line 988 (ok): $(document).ready — miejsce na price-recalc init ## Required Skills | Skill | Priority | When to Invoke | Loaded? | |-------|----------|----------------|---------| | Playwright MCP (`mcp__plugin_playwright_playwright__*`) | required | Task 3 live verification (live price update na dimension change) | ○ | **BLOCKING:** Playwright MCP wymagany w Task 3 (checkpoint) — weryfikacja że cena update'uje się dynamicznie w browser bez reload. ## Skill Invocation Checklist - [ ] Playwright MCP dostępny (custom widget, piece popup, dimension input UI) ## AC-1: Initial price render on load ```gherkin Given user otwiera stronę produktu w nowym layoucie (IP 89.69.31.86) And `#piece-width` = "50", `#piece-height` = "50" (default) And `meta[property="product:price:amount"]` = "239" When strona ładuje się Then `.product-prices .current-price` zawiera policzoną cenę (239 × (0.5 × 0.5) = 59.75 → "59,75 zł" w PL locale) Or jeśli niemożliwe precyzyjnie (rounding, locale), co najmniej nie "Od 239,00 zł / m²" tylko konkretna suma ``` ## AC-2: Price updates on dimension change ```gherkin Given user na stronie produktu w NEW layoucie And initial cena wyświetlona When user zmieni `#piece-width` z "50" na "200" And trigger `input` event Then `.product-prices .current-price` pokazuje new cena (239 × (2.0 × piece_h_m)) w ≤500ms And nie ma HTTP request (pure JS calc) ``` ## AC-3: Price updates on height change ```gherkin Given user na stronie produktu w NEW layoucie z width=200 When user zmieni `#piece-height` z "50" na "150" Then `.product-prices .current-price` pokazuje 239 × (2.0 × 1.5) = 717,00 zł ``` ## AC-4: Price re-renders after variant AJAX refresh ```gherkin Given user kliknie inny wariant kolorystyczny (Plan 01-01 flow) And `prestashop.emit('updatedProduct', resp)` firuje And nowy `meta[property="product:price:amount"]` ma inną wartość (lub ten sam, zależy od variant) And piece-width/height pozostają niezmienione When updatedProduct event fires Then `.current-price` re-renderuje się z nowym base_price × current area ``` ## AC-5: Zero regression OLD layout ```gherkin Given strona produktu poza IP 89.69.31.86 (OLD layout) When strona ładuje się Then cena wyświetlana jak przed Plan 02-04 (baseline — static z shared partial) And żadne nowe JS error nie pojawia się w konsoli And squaremeter native price calc działa bez regresji ``` Task 1: Extract recalcProductPrice() helper + bind na piece dimensions themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl W custom.js (po `$(document).ready` block ~line 988) dodaj funkcję + bindings: ```js // Phase 02 Plan 02-04: live price calc on piece dimension change. // NEW layout only (.product-variants-data--new marker). Algorytm identyczny // z add-to-cart handler (Plan 02-03) — single source of truth dla cena. function __p02p04RecalcPrice() { if (!document.querySelector('.product-variants-data--new')) return; var pwRaw = parseInt(($('#piece-width').val() || 0), 10) || 0; var phRaw = parseInt(($('#piece-height').val() || 0), 10) || 0; if (pwRaw <= 0 || phRaw <= 0) return; var wM = pwRaw / 100; var hM = phRaw / 100; var areaM2 = wM * hM; var basePrice = parseFloat($('meta[property="product:price:amount"]').attr('content')) || parseFloat(($('.product-prices .current-price, .current-price').first() .text() || '').replace(/[^\d.,]/g, '').replace(',', '.')) || 0; if (basePrice <= 0) return; var total = basePrice * areaM2; // Format PL locale: 717,00 zł (2 decimals, comma separator, zł suffix) var formatted = total.toFixed(2).replace('.', ',') + ' zł'; // Update primary price display (NEW layout) $('.product-prices .current-price, .product-prices-data .current-price').each(function() { // Zachowaj strukture (ewentualne span'y z unit / label) — podmień tylko text node var $el = $(this); // Usun "Od X zł / m²" label format, zastap konkretna suma $el.text(formatted); }); // Persist na data attr dla debug / testow $('.product-prices').first().attr('data-calculated-price', formatted); } // Event bindings (delegated — piece popup moze pojawiac sie pozniej): $(document).on('input change keyup', '#piece-width, #piece-height', function() { // Debounce: throttle do 100ms zeby nie palic CPU przy szybkim type clearTimeout(window.__p02p04RecalcT); window.__p02p04RecalcT = setTimeout(__p02p04RecalcPrice, 100); }); // Initial render on DOMContentLoaded $(document).ready(__p02p04RecalcPrice); // Re-render po AJAX variant refresh (Plan 01-01 flow) if (window.prestashop && typeof prestashop.on === 'function') { prestashop.on('updatedProduct', function() { // Delay 50ms: basePrice meta moze jeszcze nie byc zaaktualizowana w DOM setTimeout(__p02p04RecalcPrice, 50); }); } $(document).on('updatedProduct', function() { setTimeout(__p02p04RecalcPrice, 50); }); ``` Dopisz IDENTYCZNY blok w `product.tpl` inline mirror (wewnątrz istniejącego `(function() { ... })();` po `window.__p02p02Bound` guard, przed closing `})();`). Inline mirror używa `jQuery` zamiast `$`. **Unikaj:** - Modyfikacji istniejącego Plan 02-03 add-to-cart handler (re-use basePrice/area formula, nie duplikat). - Dodawania `.current-price` w OLD layout (guard przez `.product-variants-data--new` marker). - Touching `.product-prices-data` struktury — only text update. - Zmiany Plan 02-03 hidden inputs synchronizacji (to robi add-to-cart handler, nie price display). - Throw na missing `#piece-width` / `#piece-height` (graceful — early return). Jeśli piece popup nie jest jeszcze otwarty, `#piece-width`/`#piece-height` MIGHT not exist w DOM. W tym przypadku `$('#piece-width').val()` zwraca `undefined` → parseInt → NaN → 0 → early return. Cena wtedy zostaje statyczna (OK). Local syntax check: otwórz custom.js w browser devtools — brak SyntaxError. Bash: `node -c themes/ayon/assets/js/custom.js` (jeśli Node dostępne) — albo przez FTP upload + load strony i sprawdzenie konsoli na parse errors. AC-1 (initial render) + AC-2 (width change) + AC-3 (height change) spełnione po Task 3 live verify. Task 2: FTP deploy obu plików + basic sanity check themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl Upload via curl (jak w Plan 02-03, bo ftp-kr nie łapie Claude edycji): ```bash curl -s -T "themes/ayon/assets/js/custom.js" "ftp://projectpro:i6B.b5P%7Bd6@newwalls.pl/public_html/themes/ayon/assets/js/custom.js" -w "js:%{http_code}\n" curl -s -T "themes/ayon/templates/catalog/product.tpl" "ftp://projectpro:i6B.b5P%7Bd6@newwalls.pl/public_html/themes/ayon/templates/catalog/product.tpl" -w "tpl:%{http_code}\n" ``` Expected: `js:226` + `tpl:226` (FTP transfer complete). Następnie fetch server-side custom.js + product.tpl rendered HTML, grep na `__p02p04RecalcPrice` — confirm deploy. Via Playwright: 1. `fetch('/themes/ayon/assets/js/custom.js')` → text contains `__p02p04RecalcPrice` 2. `fetch('/pl/prestige/294-4181-rain-of-flowers.html?_bust=')` → HTML contains `__p02p04RecalcPrice` (inline mirror) Deploy confirmed. Task 3 checkpoint może się zacząć. Task 3: Live verification via Playwright — wszystkie 5 AC Live price calculation: - Initial render (AC-1) - Width change update (AC-2) - Height change update (AC-3) - Variant AJAX re-render (AC-4) - OLD layout regression (AC-5) Via Playwright MCP: **AC-1 (initial render):** 1. Navigate: `https://newwalls.pl/pl/prestige/294-4181-rain-of-flowers.html?_new=1` 2. Wait 1s for DOMContentLoaded 3. Evaluate: `$('.product-prices .current-price').text()` — should NOT be "Od 239,00 zł / m²" but konkretna wartość (np. "59,75 zł" dla default 50×50). 4. Check `data-calculated-price` attribute na `.product-prices`. **AC-2 + AC-3 (dimension change):** 5. Set `#piece-width` = 200, trigger input event. 6. Wait 150ms (debounce 100ms + buffer). 7. Evaluate `.current-price` text — should contain nowy total (np. 239 × 2.0 × 0.5 = 239,00 zł). 8. Set `#piece-height` = 150, trigger input. Wait 150ms. 9. `.current-price` should be 239 × 2.0 × 1.5 = 717,00 zł. **AC-4 (variant switch):** 10. Kliknij inny wariant kolorystyczny w `.product-variants`. 11. Wait for AJAX refresh to complete (prestashop emits `updatedProduct`). 12. Wait 200ms. 13. `.current-price` should re-calculate z nowym (lub tym samym) base_price × current area. **AC-5 (OLD layout regression):** 14. Tymczasowo flip IP gate (`89.69.31.86` → `255.255.255.255`) w product.tpl via Edit + curl FTP. 15. Navigate + reload. 16. Evaluate `$('.current-price').text()` — powinno wyglądać jak baseline OLD (static price z partial, bez dynamic re-calc). 17. Check console: zero JS errors. 18. RESTORE IP gate (`255.255.255.255` → `89.69.31.86`) + FTP upload. Jeśli któreś AC nie spełnione — opisz symptom, wstrzymaj, iteruj Task 1. Type "approved" LUB describe specific AC failure. ## DO NOT CHANGE - Plan 02-03 add-to-cart handler (custom.js lines 1026+) — reużywamy formułę basePrice/area, nie duplikat. Jeśli potrzebne, wydziel helper z add-to-cart handler (ale tylko jeśli tańsze niż duplicate). - POST payload construction (Plan 02-03 scope) — nie dotykamy `product_total_price_calc`, `calculated_total` etc. w payload. - `modules/squaremeter/*` — żaden edit ani override. - Stary layout (`{if REMOTE_ADDR != '89.69.31.86'}` block, line 53-525 w product.tpl) — brak zmian. - Shared partials (`_partials/*.tpl`) — niedotykalne. - `totalpriceinfospecific` / `prod` no-op override z Plan 02-01 — zostaje. ## SCOPE LIMITS - Ten plan NIE dodaje dimension input UI (input width/height niezależny od piece popup) — user podaje wymiar przez piece popup (obecny mechanizm). - Ten plan NIE synchronizuje ceny z backend rules (extra fees, waste value, promocje) — używa tylko `base_price × area`. Jeśli backend ma dopłaty, UI pokaże niższą cenę niż faktycznie w koszyku. **Trade-off akceptowalny** dla scope — klient widzi estimate, dokładna cena w cart. - Ten plan NIE dodaje currency formatting edge cases (inne lokale, waluty) — hardcode PL: `{X},{YY} zł`. - Ten plan NIE dodaje loading indicator / animation podczas recalc — 100ms debounce wystarczający. - Brak nowych dependencies. Przed declaration complete (UNIFY gate): - [ ] AC-1 verified Playwright: initial render pokazuje konkretną cenę (nie "Od Y zł / m²"). - [ ] AC-2 verified Playwright: width change → price update ≤500ms, brak HTTP request. - [ ] AC-3 verified Playwright: height change → correct formula 239 × 2.0 × 1.5 = 717,00 zł. - [ ] AC-4 verified Playwright: variant switch → re-render z nowym base_price. - [ ] AC-5 verified Playwright: OLD layout nie ma changes, zero JS errors. - [ ] Git diff clean: zero zmian w `modules/squaremeter/`, shared partials, OLD layout, Plan 02-03 add-to-cart handler. - [ ] Kod identyczny w OBU miejscach (custom.js + product.tpl inline mirror). - [ ] FTP deploy confirmed `226` dla obu plików. - Wszystkie 5 AC pass w live Playwright test. - Zero regresji w starym layoucie. - Zero regresji w Plan 02-03 add-to-cart + customization + modal flow (re-test kluczowych części). - SUMMARY.md dokumentuje deployed behavior. Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-04-SUMMARY.md`