Files
newwalls.pl/.paul/phases/02-product-actions-fixes/02-04-PLAN.md
Jacek Pyziak ac03f807c1 feat(02-product-actions-fixes): Phase 02 complete — customization, price label, structure fix
Plan 02-03: Customization save + success modal (5/5 AC)
- 26-field squaremeter POST payload (verbose PL dim, qty_alt/qty_alth)
- Chain POST /module/ps_shoppingcart/ajax -> Bootstrap #blockcart-modal
- Critical fix: moved {/block} so inline script actually renders
- __p02p02InFlight re-entrancy guard

Plan 02-04: Live cena per-sqm label obok "Dodaj do koszyka" (5/5 AC)
- .p02p04-total-price label, gorna .current-price static
- Separate __p02p04Bound + setInterval reconciliation
- Poll-retry prestashop.on registration

Plan 02-05: Struktura materialu w POST payload (4/4 AC)
- Enumerate [name^="group["] spoza formy, doklej do payload
- Fix: group_5 select w .product-bar-box nie trafial do koszyka

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:55:05 +02:00

14 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
phase plan type wave depends_on files_modified autonomous delegation
02-product-actions-fixes 04 execute 1
02-03
themes/ayon/assets/js/custom.js
themes/ayon/templates/catalog/product.tpl
false 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)

<acceptance_criteria>

AC-1: Initial price render on load

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

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

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

AC-4: Price re-renders after variant AJAX refresh

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

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

</acceptance_criteria>

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.

<success_criteria>

  • 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. </success_criteria>
Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-04-SUMMARY.md`