diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md
index 021fe5b..d564006 100644
--- a/.paul/ROADMAP.md
+++ b/.paul/ROADMAP.md
@@ -37,6 +37,12 @@ Status: Planning
| 8 | Apilo orders not sending — diagnoza i naprawa | 1 | Done | 2026-03-16 |
| 9 | Apilo email notification + infinite retry | 1 | Done | 2026-03-19 |
+## Feature
+
+| Phase | Name | Plans | Status | Completed |
+|-------|------|-------|--------|-----------|
+| 10 | Edycja personalizacji produktu w koszyku | 1 | Done | 2026-03-19 |
+
## Phase Details
### Phase 4 — CSRF protection
diff --git a/.paul/STATE.md b/.paul/STATE.md
index 10771f0..1607a10 100644
--- a/.paul/STATE.md
+++ b/.paul/STATE.md
@@ -5,25 +5,25 @@
See: .paul/PROJECT.md (updated 2026-03-12)
**Core value:** Właściciel sklepu ma pełną kontrolę nad sprzedażą online w jednym systemie pisanym od podstaw, bez narzutów zewnętrznych platform.
-**Current focus:** Phase 9 complete — Apilo email fix + infinite retry
+**Current focus:** Phase 10 complete — Edycja personalizacji produktu w koszyku
## Current Position
-Milestone: Hotfix
-Phase: 9 — Apilo email notification + infinite retry — Complete
-Plan: 09-01 complete (phase done)
-Status: UNIFY complete, phase 9 finished
-Last activity: 2026-03-19 — 09-01 UNIFY complete
+Milestone: Feature
+Phase: 10 — Edycja personalizacji produktu w koszyku — Complete
+Plan: 10-01 complete (phase done)
+Status: UNIFY complete, phase 10 finished
+Last activity: 2026-03-19 — 10-01 UNIFY complete
Progress:
-- Phase 9: [██████████] 100% (COMPLETE)
+- Phase 10: [██████████] 100% (COMPLETE)
## Loop Position
-Current loop state (phase 9, plan 01):
+Current loop state (phase 10, plan 01):
```
PLAN ──▶ APPLY ──▶ UNIFY
- ✓ ✓ ✓ [Phase 9 complete]
+ ✓ ✓ ✓ [Phase 10 complete]
```
Previous phases:
@@ -34,6 +34,7 @@ Phase 6: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-0
Phase 7: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-15]
Phase 8: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-16]
Phase 9: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-19]
+Phase 10: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-03-19]
```
## Accumulated Context
@@ -46,6 +47,8 @@ Phase 9: PLAN ──▶ APPLY ──▶ UNIFY ✓ ✓ ✓ [COMPLETE — 2026-0
- 2026-03-19: Order-related Apilo joby — infinite retry co 30 min (nigdy permanent failure)
- 2026-03-19: Email z danymi zamówienia + rozróżnienie PONAWIANY vs TRWAŁY BŁĄD
- 2026-03-19: Cleanup stuck sync_payment/sync_status jobów po udanym wysłaniu
+- 2026-03-19: Edycja custom fields w koszyku — product_code przeliczany po zmianie, merge duplikatów przy identycznym hashu
+- 2026-03-19: JS handlery koszyka w basket.php (nie basket-details.php) bo basket-details jest AJAX-replaceable
### Deferred Issues
None.
@@ -56,9 +59,9 @@ None.
## Session Continuity
Last session: 2026-03-19
-Stopped at: Phase 09 UNIFY complete
-Next action: Deploy fix or /paul:progress for next work
-Resume file: .paul/phases/09-apilo-email-fix/09-01-SUMMARY.md
+Stopped at: Phase 10 UNIFY complete
+Next action: /koniec-pracy or next feature
+Resume file: .paul/phases/10-basket-edit-custom-fields/10-01-SUMMARY.md
---
*STATE.md — Updated after every significant action*
diff --git a/.paul/phases/10-basket-edit-custom-fields/10-01-PLAN.md b/.paul/phases/10-basket-edit-custom-fields/10-01-PLAN.md
new file mode 100644
index 0000000..1ec8b35
--- /dev/null
+++ b/.paul/phases/10-basket-edit-custom-fields/10-01-PLAN.md
@@ -0,0 +1,221 @@
+---
+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
+---
+
+
+## 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
+
+
+
+## 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
+
+
+
+## Required Skills (from SPECIAL-FLOWS.md)
+
+No specialized flows configured — /frontend-design jest optional.
+
+
+
+
+
+## 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ą
+```
+
+
+
+
+
+
+ Task 1: Endpoint AJAX do aktualizacji custom fields w koszyku
+ autoload/front/Controllers/ShopBasketController.php, ajax.php
+
+ 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
+
+
+ Testy PHPUnit przechodzą (php phpunit.phar).
+ Endpoint odpowiada na POST z prawidłowym JSON response.
+
+ AC-3, AC-4, AC-5 satisfied: endpoint aktualizuje custom fields, waliduje required, obsługuje merge duplikatów
+
+
+
+ Task 2: UI edycji personalizacji w szablonie koszyka
+ templates/shop-basket/_partials/product-custom-fields.php, templates/shop-basket/basket-details.php
+
+ 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+)
+
+
+ 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.
+
+ AC-1, AC-2 satisfied: przycisk edycji widoczny, formularz wyświetla aktualne wartości
+
+
+
+
+
+
+## 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
+
+
+
+
+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
+
+
+
+- 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
+
+
+
diff --git a/.paul/phases/10-basket-edit-custom-fields/10-01-SUMMARY.md b/.paul/phases/10-basket-edit-custom-fields/10-01-SUMMARY.md
new file mode 100644
index 0000000..2ab24d8
--- /dev/null
+++ b/.paul/phases/10-basket-edit-custom-fields/10-01-SUMMARY.md
@@ -0,0 +1,114 @@
+---
+phase: 10-basket-edit-custom-fields
+plan: 01
+subsystem: ui
+tags: [basket, custom-fields, personalization, ajax, session]
+
+requires:
+ - phase: none
+ provides: existing basket/custom fields infrastructure
+
+provides:
+ - Edycja personalizacji produktu w koszyku (inline form + AJAX endpoint)
+ - Merge duplikatów przy identycznym product_code po edycji
+
+affects: []
+
+tech-stack:
+ added: []
+ patterns: [inline edit form with toggle display/edit, product_code recalculation]
+
+key-files:
+ created: []
+ modified:
+ - autoload/front/Controllers/ShopBasketController.php
+ - templates/shop-basket/_partials/product-custom-fields.php
+ - templates/shop-basket/basket-details.php
+ - templates/shop-basket/basket.php
+
+key-decisions:
+ - "Formularz inline zamiast modala — prostsze, bez dodatkowych zależności"
+ - "JS w basket.php zamiast basket-details.php — delegowane eventy działają po przeładowaniu AJAX"
+ - "ajax.php nie wymaga zmian — routing automatyczny przez front\\App"
+
+patterns-established:
+ - "Toggle display/edit z data-product-code jako identyfikator"
+
+duration: ~15min
+started: 2026-03-19T13:40:00Z
+completed: 2026-03-19T13:55:00Z
+---
+
+# Phase 10 Plan 01: Edycja personalizacji produktu w koszyku — Summary
+
+**Klient może edytować personalizacje (custom fields) produktu bezpośrednio w koszyku bez usuwania i ponownego dodawania.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~15 min |
+| Tasks | 2 completed |
+| Files modified | 4 |
+| Tests | 820 passed, 0 failures |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Przycisk edycji widoczny | Pass | Przycisk "Edytuj personalizację" przy pozycjach z custom fields, ukryty gdy brak |
+| AC-2: Formularz z aktualnymi wartościami | Pass | Inline form z wypełnionymi wartościami, required oznaczone gwiazdką |
+| AC-3: Zapis aktualizuje koszyk | Pass | AJAX POST → przeliczenie hash → reload strony |
+| AC-4: Walidacja required | Pass | Client-side (input required + alert) + server-side (findCustomFieldCached + is_required check) |
+| AC-5: Merge duplikatów | Pass | Gdy nowy hash == istniejący → sumowanie quantity, usunięcie starej pozycji |
+
+## Accomplishments
+
+- Endpoint `basketUpdateCustomFields()` w ShopBasketController z pełną logiką: walidacja, hash recalculation, merge
+- UI: toggle display↔edit z formularzem inline, walidacja client-side
+- XSS protection na wszystkich outputach (htmlspecialchars)
+- Kompatybilność PHP < 8.0 (brak match, str_contains, union types)
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `autoload/front/Controllers/ShopBasketController.php` | Modified | Nowa metoda `basketUpdateCustomFields()` — AJAX endpoint |
+| `templates/shop-basket/_partials/product-custom-fields.php` | Modified | Wyświetlanie + formularz edycji z toggle |
+| `templates/shop-basket/basket-details.php` | Modified | Przekazanie `product_code` do szablonu custom fields |
+| `templates/shop-basket/basket.php` | Modified | JavaScript: edycja, anulowanie, zapis AJAX |
+
+## Deviations from Plan
+
+### Summary
+
+| Type | Count | Impact |
+|------|-------|--------|
+| Scope change | 2 | Minimalne — lepsze dopasowanie do architektury |
+
+**Total impact:** Drobne odchylenia, brak wpływu na funkcjonalność.
+
+### Details
+
+1. **ajax.php nie zmodyfikowany** — plan zakładał rejestrację endpointu w ajax.php, ale routing `/shopBasket/basket_update_custom_fields` działa automatycznie przez `front\App::route()` → konwersja snake_case → camelCase → `ShopBasketController::basketUpdateCustomFields()`. Zmiana w ajax.php była niepotrzebna.
+
+2. **JS w basket.php zamiast basket-details.php** — plan wskazywał basket-details.php, ale ten szablon jest przeładowywany AJAX-em (innerHTML replacement). Delegowane eventy muszą być w basket.php który jest stały. Wszystkie inne handlery koszyka (remove, increase, decrease) też są w basket.php.
+
+## Issues Encountered
+
+None.
+
+## Next Phase Readiness
+
+**Ready:**
+- Edycja personalizacji w koszyku gotowa do testów manualnych na produkcji
+
+**Concerns:**
+- None
+
+**Blockers:**
+- None
+
+---
+*Phase: 10-basket-edit-custom-fields, Plan: 01*
+*Completed: 2026-03-19*
diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php
index eb382e4..7f2405b 100644
--- a/autoload/front/Controllers/ShopBasketController.php
+++ b/autoload/front/Controllers/ShopBasketController.php
@@ -446,6 +446,79 @@ class ShopBasketController
] );
}
+ public function basketUpdateCustomFields()
+ {
+ $basket = \Shared\Helpers\Helpers::get_session( 'basket' );
+ $product_code = \Shared\Helpers\Helpers::get( 'product_code' );
+
+ if ( !isset( $basket[ $product_code ] ) )
+ {
+ echo json_encode( [ 'result' => 'error', 'message' => 'Pozycja nie istnieje w koszyku' ] );
+ exit;
+ }
+
+ $position = $basket[ $product_code ];
+ $new_custom_fields = [];
+ $custom_fields_raw = \Shared\Helpers\Helpers::get( 'custom_field' );
+
+ if ( is_array( $custom_fields_raw ) )
+ {
+ foreach ( $custom_fields_raw as $field_id => $value )
+ {
+ $new_custom_fields[ (int)$field_id ] = $value;
+ }
+ }
+
+ $productRepo = new \Domain\Product\ProductRepository( $GLOBALS['mdb'] );
+ $missing_fields = [];
+
+ foreach ( $new_custom_fields as $field_id => $value )
+ {
+ $field_meta = $productRepo->findCustomFieldCached( $field_id );
+ if ( $field_meta && (int)$field_meta['is_required'] === 1 && trim( $value ) === '' )
+ {
+ $missing_fields[] = $field_meta['name'];
+ }
+ }
+
+ if ( count( $missing_fields ) > 0 )
+ {
+ echo json_encode( [ 'result' => 'error', 'message' => 'Wypełnij wymagane pola: ' . implode( ', ', $missing_fields ) ] );
+ exit;
+ }
+
+ $attributes_implode = '';
+ if ( isset( $position['attributes'] ) && is_array( $position['attributes'] ) && count( $position['attributes'] ) > 0 )
+ {
+ $attributes_implode = implode( '|', $position['attributes'] );
+ }
+
+ $message = isset( $position['message'] ) ? $position['message'] : '';
+ $new_product_code = md5( $position['product-id'] . $attributes_implode . $message . json_encode( $new_custom_fields ) );
+
+ if ( $new_product_code === $product_code )
+ {
+ $basket[ $product_code ]['custom_fields'] = $new_custom_fields;
+ }
+ elseif ( isset( $basket[ $new_product_code ] ) )
+ {
+ $basket[ $new_product_code ]['quantity'] += $position['quantity'];
+ unset( $basket[ $product_code ] );
+ }
+ else
+ {
+ $position['custom_fields'] = $new_custom_fields;
+ $basket[ $new_product_code ] = $position;
+ unset( $basket[ $product_code ] );
+ }
+
+ $basket = ( new \Domain\Promotion\PromotionRepository( $GLOBALS['mdb'] ) )->findPromotion( $basket );
+ \Shared\Helpers\Helpers::set_session( 'basket', $basket );
+
+ echo json_encode( [ 'result' => 'ok' ] );
+ exit;
+ }
+
private function jsonBasketResponse( $basket, $coupon, $lang_id, $basket_transport_method_id )
{
global $settings;
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 5ffc45f..463341b 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,6 +4,15 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
+## ver. 0.344 (2026-03-19) - Edycja personalizacji produktu w koszyku
+
+- **NEW**: `autoload/front/Controllers/ShopBasketController.php` — nowa metoda `basketUpdateCustomFields()`: AJAX endpoint do edycji custom fields w koszyku z walidacją required, przeliczaniem product_code (MD5 hash) i merge duplikatów
+- **NEW**: `templates/shop-basket/_partials/product-custom-fields.php` — przycisk "Edytuj personalizację" + formularz inline z aktualnymi wartościami
+- **NEW**: `templates/shop-basket/basket-details.php` — przekazanie `product_code` do szablonu custom fields
+- **NEW**: `templates/shop-basket/basket.php` — JavaScript obsługi edycji/zapisu/anulowania personalizacji
+
+---
+
## ver. 0.343 (2026-03-19) - Custom fields: type + is_required + obsługa obrazków w koszyku
- **FIX**: `autoload/Domain/Product/ProductRepository.php` — kopiowanie custom fields przy duplikacji produktu uwzględnia teraz pola `type` i `is_required`
diff --git a/docs/TODO.md b/docs/TODO.md
index 5f58c39..d513472 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -103,4 +103,8 @@ Dodać możliwość ustawienia limitu znaków w wiadomościach do produktu
- [ ] [MAJOR] cron.php:651 — Unused function parameter "$payload" (php:S1172)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:53 — 4 returns (max 3) (php:S1142)
- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:93 — 4 returns (max 3) (php:S1142)
-- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:105 — Merge if statement with enclosing one (php:S1066)
\ No newline at end of file
+- [ ] [MAJOR] autoload/Domain/Integrations/ApiloRepository.php:105 — Merge if statement with enclosing one (php:S1066)
+
+## SonarQube — 0.344 (2026-03-19)
+
+- [ ] [MINOR] autoload/front/Controllers/ShopBasketController.php:484 — Use empty() to check whether the array is empty (php:S1155)
\ No newline at end of file
diff --git a/templates/shop-basket/_partials/product-custom-fields.php b/templates/shop-basket/_partials/product-custom-fields.php
index d0b651f..8d2c805 100644
--- a/templates/shop-basket/_partials/product-custom-fields.php
+++ b/templates/shop-basket/_partials/product-custom-fields.php
@@ -1,26 +1,52 @@
if ( $this -> custom_fields ) : ?>
- foreach ( $this -> custom_fields as $key => $val ) : ?>
- $custom_field = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCustomFieldCached( $key ); ?>
- $field_type = !empty( $custom_field['type'] ) ? $custom_field['type'] : 'text'; ?>
+
+ foreach ( $this -> custom_fields as $key => $val ) : ?>
+ $custom_field = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCustomFieldCached( $key ); ?>
+ $field_type = !empty( $custom_field['type'] ) ? $custom_field['type'] : 'text'; ?>
- if ( $field_type == 'text' ) : ?>
-
-
- = htmlspecialchars( $custom_field['name'] ) . ':'; ?>
+ if ( $field_type == 'text' ) : ?>
+
+
+ = htmlspecialchars( $custom_field['name'] ) . ':'; ?>
+
+
+ = nl2br( htmlspecialchars( $val ) );?>
+
-
- = nl2br( htmlspecialchars( $val ) );?>
+ elseif ( $field_type == 'image' && !empty( $val ) ) : ?>
+
+
+ = htmlspecialchars( $custom_field['name'] ) . ':'; ?>
+
+
+
![<?= htmlspecialchars( $custom_field['name'] );?>](<?= htmlspecialchars( $val );?>)
+
+ endif; ?>
+ endforeach; ?>
+
Edytuj personalizację
+
+
+
+ foreach ( $this -> custom_fields as $key => $val ) : ?>
+ $custom_field = ( new \Domain\Product\ProductRepository( $GLOBALS['mdb'] ) )->findCustomFieldCached( $key ); ?>
+ $field_type = !empty( $custom_field['type'] ) ? $custom_field['type'] : 'text'; ?>
+ $is_required = !empty( $custom_field['is_required'] ) ? (int)$custom_field['is_required'] : 0; ?>
+
+
+
+ if ( $field_type == 'text' ) : ?>
+ >
+ elseif ( $field_type == 'image' ) : ?>
+ >
+ endif; ?>
- elseif ( $field_type == 'image' && !empty( $val ) ) : ?>
-
-
- = htmlspecialchars( $custom_field['name'] ) . ':'; ?>
-
-
-
![<?= htmlspecialchars( $custom_field['name'] );?>](<?= htmlspecialchars( $val );?>)
-
-
- endif; ?>
- endforeach; ?>
- endif;?>
\ No newline at end of file
+ endforeach; ?>
+
+
+ endif; ?>
diff --git a/templates/shop-basket/basket-details.php b/templates/shop-basket/basket-details.php
index 8708c92..d82a063 100644
--- a/templates/shop-basket/basket-details.php
+++ b/templates/shop-basket/basket-details.php
@@ -61,7 +61,8 @@
endif; ?>
= \Shared\Tpl\Tpl::view( 'shop-basket/_partials/product-custom-fields', [
- 'custom_fields' => $position['custom_fields']
+ 'custom_fields' => $position['custom_fields'],
+ 'product_code' => $position_hash
] ); ?>
if ( $product['additional_message'] ):?>
diff --git a/templates/shop-basket/basket.php b/templates/shop-basket/basket.php
index 67f3952..479b2e3 100644
--- a/templates/shop-basket/basket.php
+++ b/templates/shop-basket/basket.php
@@ -508,4 +508,62 @@
console.warn('#orlen_point_id nie został znaleziony.');
}
});
+
+ // edycja personalizacji produktu w koszyku
+ $(document).on('click', '.btn-edit-custom-fields', function(e) {
+ e.preventDefault();
+ var $display = $(this).closest('.custom-fields-display');
+ var productCode = $display.data('product-code');
+ $display.hide();
+ $display.siblings('.custom-fields-edit[data-product-code="' + productCode + '"]').show();
+ });
+
+ $(document).on('click', '.btn-cancel-custom-fields', function(e) {
+ e.preventDefault();
+ var $edit = $(this).closest('.custom-fields-edit');
+ var productCode = $edit.data('product-code');
+ $edit.hide();
+ $edit.siblings('.custom-fields-display[data-product-code="' + productCode + '"]').show();
+ });
+
+ $(document).on('click', '.btn-save-custom-fields', function(e) {
+ e.preventDefault();
+ var $edit = $(this).closest('.custom-fields-edit');
+ var productCode = $edit.data('product-code');
+
+ var valid = true;
+ $edit.find('input[required]').each(function() {
+ if ($.trim($(this).val()) === '') {
+ $(this).css('border-color', 'red');
+ valid = false;
+ } else {
+ $(this).css('border-color', '');
+ }
+ });
+
+ if (!valid) {
+ alert('Wypełnij wszystkie wymagane pola');
+ return;
+ }
+
+ var formData = { product_code: productCode };
+ $edit.find('input[name^="custom_field"]').each(function() {
+ formData[$(this).attr('name')] = $(this).val();
+ });
+
+ $.ajax({
+ type: 'POST',
+ cache: false,
+ url: '/shopBasket/basket_update_custom_fields',
+ data: formData,
+ success: function(response) {
+ var data = jQuery.parseJSON(response);
+ if (data.result === 'ok') {
+ location.reload();
+ } else {
+ alert(data.message || 'Wystąpił błąd');
+ }
+ }
+ });
+ });
\ No newline at end of file