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

310 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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
---
<objective>
## 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`.
</objective>
<context>
## 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
</context>
<skills>
## 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)
</skills>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Extract recalcProductPrice() helper + bind na piece dimensions</name>
<files>themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl</files>
<action>
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).
</action>
<verify>
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.
</verify>
<done>AC-1 (initial render) + AC-2 (width change) + AC-3 (height change) spełnione po Task 3 live verify.</done>
</task>
<task type="auto">
<name>Task 2: FTP deploy obu plików + basic sanity check</name>
<files>themes/ayon/assets/js/custom.js, themes/ayon/templates/catalog/product.tpl</files>
<action>
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.
</action>
<verify>
Via Playwright:
1. `fetch('/themes/ayon/assets/js/custom.js')` → text contains `__p02p04RecalcPrice`
2. `fetch('/pl/prestige/294-4181-rain-of-flowers.html?_bust=<ts>')` → HTML contains `__p02p04RecalcPrice` (inline mirror)
</verify>
<done>Deploy confirmed. Task 3 checkpoint może się zacząć.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Live verification via Playwright — wszystkie 5 AC</name>
<what-built>
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)
</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Type "approved" LUB describe specific AC failure.</resume-signal>
</task>
</tasks>
<boundaries>
## 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.
</boundaries>
<verification>
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.
</verification>
<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>
<output>
Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-04-SUMMARY.md`
</output>