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>
This commit is contained in:
309
.paul/phases/02-product-actions-fixes/02-04-PLAN.md
Normal file
309
.paul/phases/02-product-actions-fixes/02-04-PLAN.md
Normal file
@@ -0,0 +1,309 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user