Files
newwalls.pl/.paul/phases/02-product-actions-fixes/02-02-PLAN.md
Jacek Pyziak 7ac795ba3f feat(new-layout): add-to-cart handler + piece configurator (Phase 02 plans 01-02)
Plan 02-01 (piece/crop configurator, complete):
- #piece reuse z shared partial product-cover-thumbnails.tpl
- 8 hidden inputs (is_crop, crop_pos_x/y, crop_width/height, piece_bg_top/left, is_reflection) w formie #add-to-cart-or-refresh
- Defensive setup w custom.js: setTimeout(600) init, no-op override totalpriceinfospecific/prod, DOM stubs
- CSS scope pod body#product .product-size-data .product-size-data--new

Plan 02-02 (add-to-cart submission, PARTIAL):
- Capture-phase native addEventListener (useCapture=true) blokuje PS core crash
  (button poza formą w nowym layoucie — closest('form') zwracało 0)
- Manualny AJAX POST: form.serialize() + qty + add=1&action=update do /pl/koszyk
- Fancybox-blocker port z custom.js:327 (nie odpalał się bo selector 0 matches)
- Manual sync is_crop/crop_width/height przed POST (obejście crash checkedHandler)
- prestashop.emit('updatedCart') + defensive blockcart refresh fetch
- Loading spinner + success flash CSS
- Inline handler mirror w product.tpl z idempotency guard (window.__p02p02Bound)
  — cache-buster dla browser cachowanego custom.js

Deferred do Plan 02-03 (customization + modal blocker dla production):
- Customization nie zapisuje się (squaremeter hook gate'owany discretion=on + brak dimension fields)
- Success modal (wymaga POST do /module/ps_shoppingcart/ajax)

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

390 lines
19 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: 02
type: execute
wave: 1
depends_on: ["02-01"]
files_modified:
- themes/ayon/templates/catalog/product.tpl
- themes/ayon/assets/js/custom.js
- themes/ayon/assets/css/custom.scss
autonomous: false
delegation: off
---
<objective>
## Goal
Uruchomić „Dodaj do koszyka" end-to-end w nowym layoucie strony produktu (IP 89.69.31.86): klik w przycisk → walidacja piece (wymiary wybrane) → POST do `/cart` z pełnym body (token + id_product + id_customization + qty + 8 hidden inputów crop/mirror) → success feedback + odświeżenie cart widget w headerze bez page reload.
## Purpose
Bez tej funkcjonalności nowy layout nie nadaje się do testów — klient nie zatwierdzi go jako alternatywy dla starego do czasu aż dodawanie do koszyka działa identycznie (feature parity). Plan 02-01 (piece) dostarczył hidden inputy gotowe do POST'owania; Plan 02-02 domyka kontrakt: od kliknięcia przycisku do potwierdzenia że produkt trafił do koszyka.
## Output
- Działający add-to-cart flow w nowym layoucie zweryfikowany live (Playwright).
- Listener `prestashop.on('updatedCart', ...)` lub równoważny mechanizm odświeżający cart widget (header counter / modal) po pomyślnym POST.
- Success/error UX zgodny ze starym layoutem (toast modal / redirect do koszyka — do ustalenia w Task 1).
- Regresja starego layoutu = zero (zmiany scope'owane pod marker class `.product-variants-data--new` albo gałąź `if REMOTE_ADDR == '89.69.31.86'`).
- Summary: `.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md`.
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/02-product-actions-fixes/02-01-SUMMARY.md
# Plan 02-01 dostarczył 8 hidden inputów w `#add-to-cart-or-refresh`:
# is_crop, is_reflection, crop_pos_x, crop_pos_y, crop_width, crop_height,
# piece_bg_top, piece_bg_left. Gotowe do POST'owania. Również nałożył no-op
# override na `totalpriceinfospecific`/`prod` — to wpływa na kalkulację ceny,
# ale NIE na sam POST (to osobny plan).
## Source Files
@themes/ayon/templates/catalog/product.tpl
@themes/ayon/templates/catalog/_partials/product-add-to-cart.tpl
@themes/ayon/assets/js/custom.js
@themes/ayon/assets/css/custom.scss
## External Reference
# PrestaShop 1.7 core cart flow (dla orientacji — NIE modyfikujemy core):
# - Handler globalny: `$(document).on('click', '[data-button-action=add-to-cart]', ...)`
# - POST: `urls.pages.cart` z `add=1&action=update&id_product=...&qty=...&token=...`
# - Response JSON: { success, hasError, errors, cart, ... }
# - Eventy: `prestashop.emit('updatedCart', { resp, reason })` po sukcesie
</context>
<skills>
## Required Skills
<!-- SPECIAL-FLOWS.md jeszcze nie istnieje (rekomendacja z Plan 02-01 do utworzenia). -->
<!-- Dla Plan 02-02 skills section jest minimalna: Playwright MCP wymagany do Task 1 diagnosis. -->
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| Playwright MCP (`mcp__plugin_playwright_playwright__*`) | required | Task 1 live diagnosis, Task 3 regression test | ○ |
| context-mode (`mcp__plugin_context-mode_context-mode__*`) | optional | Exploracja kodu PS core / custom.js bez zanieczyszczania kontekstu | ○ |
**BLOCKING:** Playwright MCP wymagany w Task 1 — bez live inspection nie da się ustalić czy PS core handler przechwytuje click.
## Skill Invocation Checklist
- [ ] Playwright MCP dostępny (tools `mcp__plugin_playwright_playwright__browser_*`)
- [ ] Dostęp do środowiska z IP `89.69.31.86` (lub sposób na spoof'owanie — do ustalenia w Task 1)
</skills>
<acceptance_criteria>
## AC-1: Add-to-cart button submitowalny w nowym layoucie
```gherkin
Given strona produktu w nowym layoucie (REMOTE_ADDR == '89.69.31.86')
And piece został skonfigurowany (#checkbox-piece checked, width/height > 0)
When użytkownik klika przycisk "Dodaj do koszyka" (`[data-button-action=add-to-cart]`)
Then request POST wychodzi do urls.pages.cart
And body zawiera: token, id_product, id_customization, qty, is_crop=1, crop_pos_x/y, crop_width, crop_height, piece_bg_top/left, is_reflection
And response HTTP 200 z JSON { success: true, cart: {...} }
```
## AC-2: Walidacja "musi wybrać piece" — fancybox blokada nadal działa
```gherkin
Given strona produktu w nowym layoucie
And #checkbox-piece NIE jest zaznaczony (user nie otworzył popup'a piece)
When użytkownik klika "Dodaj do koszyka"
Then pojawia się fancybox z treścią "Proszę wybrać rozmiar i wycinek tapety..."
And POST do koszyka NIE wychodzi (network tab clean)
And stan koszyka niezmieniony
```
## AC-3: Cart widget odświeża się po sukcesie
```gherkin
Given udany POST z AC-1 (response success=true)
When backend zwraca response
Then nagłówek/cart widget pokazuje zaktualizowaną liczbę produktów (+1)
And (jeśli istnieje modal potwierdzający) wyświetla się success modal LUB redirect do koszyka zgodnie z konwencją starego layoutu
And event `prestashop.emit('updatedCart', resp)` został wyemitowany (verified via console listener injected w Playwright)
```
## AC-4: Błąd serwera prezentowany użytkownikowi
```gherkin
Given add-to-cart zwraca error (np. insufficient stock, invalid qty)
When response.hasError === true
Then użytkownik widzi czytelny komunikat błędu (modal lub inline message)
And stan formy zostaje zachowany (piece config nie zresetowany)
And przycisk "Dodaj do koszyka" wraca do stanu enabled (nie utknął w loading)
```
## AC-5: Zero regresji w starym layoucie
```gherkin
Given strona produktu poza IP 89.69.31.86 (REMOTE_ADDR != '89.69.31.86')
When użytkownik konfiguruje piece i klika "Dodaj do koszyka"
Then flow działa identycznie jak przed Plan 02-02 (baseline)
And network payload i sekwencja zdarzeń niezmienione
```
</acceptance_criteria>
<tasks>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 1: Live diagnosis — jaki jest aktualny stan add-to-cart w nowym layoucie</name>
<what-built>
Zanim cokolwiek napiszemy — musimy zobaczyć co SIĘ DZIEJE po kliknięciu przycisku
w nowym layoucie. Hipotezy do zweryfikowania:
H1: PS core handler (`[data-button-action=add-to-cart]`) przechwytuje click, wykonuje POST,
wszystko działa i potrzebujemy tylko cosmetic polish + cart widget listener.
H2: PS core handler nie działa (bo np. szuka `.product-actions` wrapper którego brak,
albo inline-script crash wywala listener przed ready). Trzeba wrapper dodać LUB
napisać własny submit handler.
H3: Handler działa, POST wychodzi, ale response nie triggeruje cart widget refresh.
H4: Handler działa ale fancybox-blocker (custom.js:327-341) nie działa poprawnie
w nowym layoucie (np. `#checkbox-piece` nigdy się nie zaznacza po user flow).
</what-built>
<how-to-verify>
Via Playwright MCP (live env, IP spoofowany do 89.69.31.86 — lub na czyjej maszynie
ma IP dopasowany):
1. Navigate: https://newwalls.pl/[dowolny-produkt] (przy zalogowanym IP 89.69.31.86)
2. Verify new layout loaded: `document.querySelector('.product-variants-data--new') !== null`
3. Simulate user flow:
a. Kliknąć `.piece-summary` → popup otwiera się
b. Ustawić szerokość/wysokość (np. 200x150) w fancybox
c. Kliknąć "Zatwierdź" (lub odpowiednik) w popup → sprawdzić `$('#checkbox-piece').is(':checked')` === true
d. Inject listener dla diagnostyki:
```js
window.__addToCartDiag = { clicks: 0, posts: [], events: [] };
$(document).on('click', '[data-button-action=add-to-cart]', function() {
window.__addToCartDiag.clicks++;
});
const origFetch = window.fetch;
window.fetch = function(...args) {
if (String(args[0]).includes('cart')) window.__addToCartDiag.posts.push(args);
return origFetch.apply(this, args);
};
if (window.prestashop) {
prestashop.on('updatedCart', r => window.__addToCartDiag.events.push(['updatedCart', r]));
}
```
e. Kliknąć "Dodaj do koszyka"
f. Odczekać 2s, odczytać `window.__addToCartDiag` + network tab
4. Dokumentacja wyników:
- Czy POST dotarł do `/cart`?
- Jaki status HTTP + schema response?
- Czy `updatedCart` event emitted?
- Czy cart header counter się zaktualizował?
- Jeśli nic się nie stało — czy jest error w console? Stack trace?
5. Resume-signal: Napisz do plan'u które hipotezy (H1H4) są prawdziwe i który scenariusz implementacji wybrać dla Task 2:
- S1: Tylko dodać listener cart widget (H1 prawda) — minimalna zmiana.
- S2: Wrapper `.product-actions` wokół bloku z buttonem (H2 częściowo) — small template change.
- S3: Własny AJAX submit handler w custom.js (H2 prawda / PS core nie-do-przywrócenia) — więcej kodu.
- S4: Fix fancybox-blocker flow (H4 prawda) — tweak custom.js:327 logic.
</how-to-verify>
<resume-signal>Wybierz: S1 / S2 / S3 / S4 (można łączyć, np. "S2+S1") — albo opisz własny scenariusz jeśli diagnoza ujawni coś spoza hipotez.</resume-signal>
</task>
<task type="auto">
<name>Task 2: Implementacja submit flow według wybranego scenariusza (S1/S2/S3)</name>
<files>themes/ayon/templates/catalog/product.tpl, themes/ayon/assets/js/custom.js</files>
<action>
Na podstawie decyzji z Task 1:
**Jeśli S1 (PS core działa, tylko brakuje cart refresh):**
- W custom.js dodać (lub rozszerzyć istniejący `prestashop.on` block) listener:
```js
if (window.prestashop && typeof prestashop.on === 'function') {
prestashop.on('updatedCart', function(params) {
// Refresh header cart widget if not auto-refreshed by core
if ($('.product-variants-data--new').length === 0) return; // scope: new layout only
// Trigger header cart update — PS core usually handles this, ale defensive:
if (window.prestashop.modules && window.prestashop.modules.blockcart) {
$(document).trigger('blockcart:update', params);
}
});
}
```
- Scope pod marker class żeby stary layout nietknięty.
**Jeśli S2 (wrapper `.product-actions` brakuje):**
- W product.tpl, w gałęzi `{if ... == '89.69.31.86'}`, znaleźć blok
`<div class="product-add-to-cart">` (ok. linia 719 w nowym layoucie)
i owinąć go w `<div class="product-actions">`:
```smarty
<div class="product-actions">
<div class="product-add-to-cart">
...istniejący content...
</div>
</div>
```
- UWAGA: sprawdzić czy klasa `.product-actions` nie wywołuje niechcianych stylów
ze starego SCSS (grep w custom.css). Jeśli tak — zmienić selector CSS na
scope'owany (`body#product .product-variants-data--new .product-actions`)
lub dodać marker class `.product-actions--new` i użyć jej w JS.
**Jeśli S3 (własny submit handler):**
- W custom.js, w obrębie istniejącego `setTimeout(..., 600)` init bloku
(dla nowego layoutu, gated przez `.product-variants-data--new` check),
dodać handler:
```js
$('#add-to-cart-or-refresh').on('submit', function(e) {
if ($('.product-variants-data--new').length === 0) return; // old layout: PS core handles
e.preventDefault();
if (!$('#checkbox-piece').is(':checked')) {
// fallback do istniejącej fancybox blokady — delegacja
$('#add-to-cart-or-refresh button').trigger('click');
return;
}
var $btn = $(this).find('[data-button-action=add-to-cart]');
$btn.prop('disabled', true).addClass('loading');
$.ajax({
url: this.action,
method: 'POST',
data: $(this).serialize() + '&add=1&action=update',
dataType: 'json',
headers: { 'Accept': 'application/json' },
success: function(resp) {
if (resp.hasError || resp.success === false) {
// show error (use existing fancybox or inline)
$.fancybox({ content: (resp.errors || ['Błąd dodawania do koszyka']).join('<br>') });
return;
}
if (window.prestashop && typeof prestashop.emit === 'function') {
prestashop.emit('updatedCart', { resp: resp, reason: { linkAction: 'add-to-cart' } });
}
},
error: function() {
$.fancybox({ content: 'Błąd połączenia. Spróbuj ponownie.' });
},
complete: function() {
$btn.prop('disabled', false).removeClass('loading');
}
});
});
```
- NIE dotykać istniejącego `$('#add-to-cart-or-refresh button').on('click')` z custom.js:327
(blokada "wybierz rozmiar przed add-to-cart") — nadal potrzebna dla AC-2.
- Uwaga: PS core używa `add=1&action=update` dla add-to-cart — sprawdzić empirycznie
(w Task 1 powinno być w network tab jeśli PS core działa gdziekolwiek).
Avoid:
- Modyfikowanie `product-add-to-cart.tpl` partial (współdzielony ze starym layoutem, risk regresji)
- Dodawanie globalnych handlerów `$(document).on(...)` bez scope'owania na new layout
- Nadpisywanie istniejącego click handler z custom.js:327 (blokada fancybox)
</action>
<verify>
Playwright live test:
1. Load produkt w nowym layoucie (IP match)
2. Piece config flow (width=200, height=150, position drag)
3. Click "Dodaj do koszyka"
4. Odczytać `window.__addToCartDiag` + network tab:
- `posts.length >= 1` z body zawierającym `is_crop=1&crop_width=200&crop_height=150&...`
- Response HTTP 200, `success: true`
- Event `updatedCart` emitted
- Header cart counter += 1
</verify>
<done>AC-1, AC-2, AC-3 satisfied (AC-2 nietknięta przez Task 2 — istniejąca blokada nadal działa).</done>
</task>
<task type="auto">
<name>Task 3: Error handling, loading state UX, cross-layout regression check</name>
<files>themes/ayon/assets/js/custom.js, themes/ayon/assets/css/custom.scss</files>
<action>
**Error UX (jeśli S3 w Task 2 — jeśli S1/S2 sprawdzić czy PS core natywnie pokazuje błędy):**
- W custom.js, w submit handler z Task 2, branch `hasError`:
- Wyciągnąć czytelny text z `resp.errors` (array) → `join('<br>')`
- Fallback: "Nie udało się dodać do koszyka. Spróbuj ponownie."
- Pokazać w `$.fancybox({ content: ... })` zgodnie z istniejącym wzorcem (custom.js:330)
**Loading state (wszystkie scenariusze):**
- W custom.scss, scope'owana reguła (~linia koniec pliku):
```scss
body#product .product-variants-data--new {
#add-to-cart-or-refresh button.add-to-cart {
&.loading {
opacity: 0.6;
pointer-events: none;
position: relative;
&::after {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 20px; height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin .6s linear infinite;
}
}
}
}
@keyframes spin { to { transform: rotate(360deg); } }
```
(Skip jeśli `@keyframes spin` już istnieje — grep sprawdzić.)
**Regression test — stary layout:**
- Playwright na URL z REMOTE_ADDR != 89.69.31.86 (np. przez VPN lub override header Host).
- Zweryfikować że flow add-to-cart w starym layoucie działa identycznie jak przed Plan 02-02:
- Klik → POST → cart update → success
- Network payload (pomijając cookies) identyczny z baseline'em (zarejestrować baseline przed Task 2 jako kontrolę)
- Jeśli regresja wykryta → zdiagnozować jaki selektor / listener konflict'uje → scope'ować ciaśniej.
Avoid:
- Dodawanie globalnych styles (wszystkie pod `.product-variants-data--new` scope)
- Zmiana istniejącego submit flow starego layoutu (nawet inline — stary działa, nie dotykamy)
</action>
<verify>
1. Playwright nowy layout: force error (np. Console `fetch('/cart', {...})` z złym tokenem) → fancybox z błędem pokazuje się, button wraca do enabled.
2. Playwright nowy layout: slow-3G throttling → spinner widoczny podczas pending request.
3. Playwright stary layout (IP != 89.69.31.86): add-to-cart flow identyczny z baseline; headless log network + compare.
</verify>
<done>AC-4, AC-5 satisfied.</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `themes/ayon/templates/catalog/_partials/product-add-to-cart.tpl` (współdzielony ze starym layoutem — modyfikacja = ryzyko regresji w produkcji)
- `themes/ayon/templates/catalog/_partials/product-variants.tpl` (shared, Plan 01 closed)
- `themes/ayon/templates/catalog/_partials/product-cover-thumbnails.tpl` (shared, używany przez piece z Plan 02-01)
- Starą gałąź `{if ... != '89.69.31.86'}` w `product.tpl` — całość strukturalnie nietknięta
- Istniejący handler `$('#add-to-cart-or-refresh button').on('click', ...)` z custom.js:327 (blokada fancybox — nadal pełni AC-2)
- Hidden inputy crop/mirror z Plan 02-01 (nazwy/ID są kontraktem)
- `custom.css` — edytujemy wyłącznie `custom.scss` (user ma watcher)
## SCOPE LIMITS
- Ten plan NIE rozwiązuje kalkulacji ceny per-sqm w nowym layoucie (`totalpriceinfospecific` no-op override z Plan 02-01) — to osobny plan (Plan 02-03 kandydat).
- Ten plan NIE wypełnia pustych bloków (`.product-protect`, `.product-installation`, `.product-order-sample`) — osobny plan (Plan 02-04+).
- Ten plan NIE dodaje "quick view" / product modalu — zakres strony produktu tylko.
- Ten plan NIE zmienia serwer-side logic (add-to-cart controller) — tylko client + template.
- Brak nowych zależności npm / composer — wykorzystujemy istniejący stack (jQuery, fancybox, PS core).
</boundaries>
<verification>
Przed declaration complete (UNIFY gate):
- [ ] AC-1 verified Playwright: POST wychodzi z pełnym body, success response.
- [ ] AC-2 verified Playwright: blokada fancybox gdy piece nie wybrany.
- [ ] AC-3 verified Playwright: cart counter w headerze aktualizuje się.
- [ ] AC-4 verified Playwright: error response → czytelny komunikat, button enabled.
- [ ] AC-5 verified Playwright: stary layout bez regresji (baseline diff clean).
- [ ] `custom.scss` → `custom.css` compile clean (user watcher lub manual check po commit).
- [ ] Grep: zero zmian w plikach starego layoutu / shared partial'ach (git diff).
- [ ] `$('#add-to-cart-or-refresh').serialize()` w nowym layoucie zawiera wszystkie 8 hidden input'ów + token + id_product + id_customization + qty.
</verification>
<success_criteria>
- Wszystkie 5 AC pass w live Playwright test.
- Zero regresji w starym layoucie (Playwright + git diff check).
- Zero nowych dependencies.
- `custom.js` zmiany dodatkowe (nie modyfikujące istniejących handlerów) — diff czysty, scope'owany.
- SUMMARY.md wystawiony z: co działa, co odroczone (cena = Plan 02-03), problemy napotkane.
</success_criteria>
<output>
Po zakończeniu: `.paul/phases/02-product-actions-fixes/02-02-SUMMARY.md`
</output>