Nowa metoda basketUpdateCustomFields() w ShopBasketController — AJAX endpoint z walidacją required fields, przeliczaniem product_code (MD5 hash) i merge duplikatów. UI: przycisk "Edytuj personalizację" + formularz inline + JS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
222 lines
8.8 KiB
Markdown
222 lines
8.8 KiB
Markdown
---
|
|
phase: 10-basket-edit-custom-fields
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- autoload/front/Controllers/ShopBasketController.php
|
|
- templates/shop-basket/_partials/product-custom-fields.php
|
|
- templates/shop-basket/basket-details.php
|
|
- ajax.php
|
|
autonomous: true
|
|
---
|
|
|
|
<objective>
|
|
## Goal
|
|
Dodać możliwość edycji personalizacji (custom fields) produktu bezpośrednio w koszyku, bez konieczności usuwania produktu i dodawania go od nowa.
|
|
|
|
## Purpose
|
|
Klient, który pomyli się przy wpisywaniu personalizacji (np. grawer, dedykacja), musi teraz usunąć produkt z koszyka i dodać go ponownie z poprawnymi danymi. To frustrujące UX — edycja inline jest naturalnym oczekiwaniem.
|
|
|
|
## Output
|
|
- Przycisk "Edytuj" przy personalizacjach w koszyku
|
|
- Modal lub formularz inline do edycji wartości custom fields
|
|
- Endpoint AJAX do zapisania zmian w sesji koszyka
|
|
- Przeliczenie product_code (MD5 hash) po zmianie wartości
|
|
</objective>
|
|
|
|
<context>
|
|
## Project Context
|
|
@.paul/PROJECT.md
|
|
@.paul/ROADMAP.md
|
|
@.paul/STATE.md
|
|
|
|
## Source Files
|
|
@autoload/front/Controllers/ShopBasketController.php
|
|
@templates/shop-basket/_partials/product-custom-fields.php
|
|
@templates/shop-basket/basket-details.php
|
|
@templates/shop-product/_partial/product-custom-fields.php
|
|
@autoload/Domain/Product/ProductRepository.php
|
|
@ajax.php
|
|
</context>
|
|
|
|
<skills>
|
|
## Required Skills (from SPECIAL-FLOWS.md)
|
|
|
|
No specialized flows configured — /frontend-design jest optional.
|
|
|
|
</skills>
|
|
|
|
<acceptance_criteria>
|
|
|
|
## AC-1: Przycisk edycji widoczny w koszyku
|
|
```gherkin
|
|
Given produkt w koszyku ma wypełnione custom fields (personalizacje)
|
|
When klient widzi szczegóły koszyka (basket-details)
|
|
Then przy każdej pozycji z custom fields widnieje przycisk "Edytuj personalizację"
|
|
And przycisk NIE pojawia się gdy produkt nie ma custom fields
|
|
```
|
|
|
|
## AC-2: Formularz edycji wyświetla aktualne wartości
|
|
```gherkin
|
|
Given klient klika "Edytuj personalizację" przy pozycji koszyka
|
|
When otwiera się formularz edycji (modal lub inline)
|
|
Then formularz zawiera pola odpowiadające custom fields tego produktu
|
|
And pola są wypełnione aktualnymi wartościami z koszyka
|
|
And pola wymagane (is_required) są oznaczone jako wymagane
|
|
```
|
|
|
|
## AC-3: Zapis zmian aktualizuje koszyk
|
|
```gherkin
|
|
Given klient zmienił wartości custom fields w formularzu edycji
|
|
When klika "Zapisz"
|
|
Then wartości custom fields w sesji koszyka są zaktualizowane
|
|
And product_code (MD5 hash) jest przeliczony z nowymi wartościami
|
|
And strona koszyka odświeża się pokazując nowe wartości
|
|
And ilość produktu i inne atrybuty nie ulegają zmianie
|
|
```
|
|
|
|
## AC-4: Walidacja pól wymaganych
|
|
```gherkin
|
|
Given produkt ma custom field oznaczone jako is_required = 1
|
|
When klient próbuje zapisać formularz z pustym polem wymaganym
|
|
Then zapis jest blokowany
|
|
And wyświetlany jest komunikat o wymaganym polu
|
|
```
|
|
|
|
## AC-5: Obsługa konfliktu duplikatu
|
|
```gherkin
|
|
Given koszyk zawiera dwa egzemplarze tego samego produktu z różnymi personalizacjami
|
|
When klient edytuje personalizację jednego tak, że staje się identyczna z drugim
|
|
Then pozycje zostają scalone (ilości zsumowane)
|
|
And w koszyku pozostaje jedna pozycja z łączną ilością
|
|
```
|
|
|
|
</acceptance_criteria>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Endpoint AJAX do aktualizacji custom fields w koszyku</name>
|
|
<files>autoload/front/Controllers/ShopBasketController.php, ajax.php</files>
|
|
<action>
|
|
Dodać metodę `basketUpdateCustomFields()` w ShopBasketController:
|
|
|
|
1. Przyjmuje POST z parametrami:
|
|
- `product_code` — obecny klucz pozycji w koszyku (MD5 hash)
|
|
- `custom_field[ID]` — nowe wartości custom fields (tablica)
|
|
|
|
2. Logika metody:
|
|
- Pobierz pozycję koszyka po `product_code` z sesji
|
|
- Jeśli nie istnieje → zwróć błąd JSON
|
|
- Waliduj wymagane pola (pobierz metadane z ProductRepository::findCustomFieldCached)
|
|
- Jeśli walidacja nie przejdzie → zwróć błąd JSON z listą brakujących pól
|
|
- Zaktualizuj `custom_fields` w pozycji koszyka
|
|
- Przelicz nowy product_code: `md5(product_id . attributes . message . json_encode(new_custom_fields))`
|
|
- Jeśli nowy product_code == stary → tylko aktualizuj wartości
|
|
- Jeśli nowy product_code istnieje już w koszyku → scal pozycje (zsumuj ilość), usuń starą
|
|
- Jeśli nowy product_code nie istnieje → przenieś pozycję pod nowy klucz, usuń stary
|
|
- Przelicz sumę koszyka
|
|
- Zwróć JSON success
|
|
|
|
3. Zarejestruj endpoint w ajax.php pod kluczem `basket_update_custom_fields`
|
|
- Wzoruj się na istniejących endpointach koszyka (basket_add_product, basket_remove itp.)
|
|
|
|
Unikaj:
|
|
- NIE używaj match expressions (PHP < 8.0)
|
|
- NIE sklejaj SQL stringiem — custom fields są w sesji, nie w DB
|
|
- Escape wartości przy wyświetlaniu (htmlspecialchars), ale w sesji przechowuj surowe wartości
|
|
</action>
|
|
<verify>
|
|
Testy PHPUnit przechodzą (php phpunit.phar).
|
|
Endpoint odpowiada na POST z prawidłowym JSON response.
|
|
</verify>
|
|
<done>AC-3, AC-4, AC-5 satisfied: endpoint aktualizuje custom fields, waliduje required, obsługuje merge duplikatów</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: UI edycji personalizacji w szablonie koszyka</name>
|
|
<files>templates/shop-basket/_partials/product-custom-fields.php, templates/shop-basket/basket-details.php</files>
|
|
<action>
|
|
1. W `product-custom-fields.php` dodać przycisk "Edytuj personalizację":
|
|
- Przycisk widoczny tylko gdy `$this->custom_fields` nie jest puste
|
|
- Atrybut data-product-code z kluczem pozycji koszyka
|
|
- Klasa CSS do stylowania (np. `btn-edit-custom-fields`)
|
|
|
|
2. Dodać ukryty formularz edycji (modal inline) pod przyciskiem:
|
|
- Dla każdego custom field: input z aktualną wartością
|
|
- Pola wymagane oznaczone `required` + wizualnie (gwiazdka)
|
|
- Typ pola (text/image) z metadanych custom field
|
|
- Przyciski "Zapisz" i "Anuluj"
|
|
- Formularz domyślnie ukryty (`display: none`)
|
|
|
|
3. W `basket-details.php` dodać JavaScript obsługujący:
|
|
- Klik "Edytuj" → pokaż formularz, ukryj wyświetlane wartości
|
|
- Klik "Anuluj" → ukryj formularz, pokaż wartości
|
|
- Klik "Zapisz" → AJAX POST do `basket_update_custom_fields`
|
|
- Walidacja client-side required fields przed wysłaniem
|
|
- Po sukcesie → przeładuj stronę koszyka (location.reload)
|
|
- Po błędzie → pokaż komunikat
|
|
|
|
4. Przekazać `product_code` do szablonu custom fields:
|
|
- W `basket-details.php` przy wywołaniu `Tpl::view('shop-basket/_partials/product-custom-fields', ...)`
|
|
dodać parametr `product_code` z kluczem pozycji
|
|
|
|
Unikaj:
|
|
- NIE dodawaj zewnętrznych bibliotek JS/CSS
|
|
- NIE zmieniaj struktury HTML istniejących elementów (dodawaj nowe)
|
|
- Escape wszystkich wartości w atrybutach HTML (htmlspecialchars)
|
|
- NIE używaj str_contains/str_starts_with (PHP 8.0+)
|
|
</action>
|
|
<verify>
|
|
Wizualna weryfikacja: przycisk "Edytuj" widoczny w koszyku przy produktach z personalizacją.
|
|
Klik otwiera formularz z aktualnymi wartościami.
|
|
Zapis odświeża koszyk z nowymi wartościami.
|
|
</verify>
|
|
<done>AC-1, AC-2 satisfied: przycisk edycji widoczny, formularz wyświetla aktualne wartości</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<boundaries>
|
|
|
|
## DO NOT CHANGE
|
|
- autoload/Domain/Product/ProductRepository.php (nie modyfikuj metod findCustomFieldCached, saveCustomFields)
|
|
- autoload/Domain/Order/OrderRepository.php (nie zmieniaj createFromBasket)
|
|
- templates/shop-product/ (szablony strony produktu bez zmian)
|
|
- autoload/Domain/Basket/BasketRepository.php (jeśli istnieje — nie modyfikuj)
|
|
|
|
## SCOPE LIMITS
|
|
- Tylko koszyk (basket-details) — NIE podsumowanie zamówienia (summary-view)
|
|
- Tylko edycja wartości — NIE dodawanie/usuwanie pól custom fields
|
|
- Tylko pola typu text i image — nie dodawaj nowych typów pól
|
|
- NIE zmieniaj sposobu przechowywania custom fields w zamówieniach (pp_shop_order_products)
|
|
- NIE dodawaj testów PHPUnit dla warstwy widoku (templates) — testuj tylko logikę kontrolera
|
|
|
|
</boundaries>
|
|
|
|
<verification>
|
|
Before declaring plan complete:
|
|
- [ ] `php phpunit.phar` — wszystkie testy przechodzą (820+)
|
|
- [ ] Endpoint `basket_update_custom_fields` zwraca poprawny JSON
|
|
- [ ] Przycisk "Edytuj" widoczny w koszyku przy produktach z personalizacją
|
|
- [ ] Formularz edycji wyświetla aktualne wartości
|
|
- [ ] Zapis zmienia wartości w sesji i odświeża koszyk
|
|
- [ ] Pola required są walidowane (client + server side)
|
|
- [ ] Merge duplikatów działa poprawnie
|
|
- [ ] Brak regresji — istniejąca funkcjonalność koszyka działa bez zmian
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Wszystkie testy PHPUnit przechodzą
|
|
- AC-1 do AC-5 spełnione
|
|
- Kod zgodny z PHP < 8.0
|
|
- XSS protection (htmlspecialchars) na wszystkich outputach
|
|
- Brak nowych zależności zewnętrznych
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.paul/phases/10-basket-edit-custom-fields/10-01-SUMMARY.md`
|
|
</output>
|