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