Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09f51be1c1 | ||
|
|
951a82a5b1 |
@@ -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
|
||||
|
||||
@@ -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*
|
||||
|
||||
221
.paul/phases/10-basket-edit-custom-fields/10-01-PLAN.md
Normal file
221
.paul/phases/10-basket-edit-custom-fields/10-01-PLAN.md
Normal file
@@ -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
|
||||
---
|
||||
|
||||
<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>
|
||||
114
.paul/phases/10-basket-edit-custom-fields/10-01-SUMMARY.md
Normal file
114
.paul/phases/10-basket-edit-custom-fields/10-01-SUMMARY.md
Normal file
@@ -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*
|
||||
@@ -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;
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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)
|
||||
- [ ] [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)
|
||||
@@ -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'; ?>
|
||||
<div class="custom-fields-display" data-product-code="<?= htmlspecialchars( $this->product_code ); ?>">
|
||||
<? 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' ) : ?>
|
||||
<div class="custom-field">
|
||||
<div class="_name">
|
||||
<?= htmlspecialchars( $custom_field['name'] ) . ':'; ?>
|
||||
<? if ( $field_type == 'text' ) : ?>
|
||||
<div class="custom-field">
|
||||
<div class="_name">
|
||||
<?= htmlspecialchars( $custom_field['name'] ) . ':'; ?>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<?= nl2br( htmlspecialchars( $val ) );?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_text">
|
||||
<?= nl2br( htmlspecialchars( $val ) );?>
|
||||
<? elseif ( $field_type == 'image' && !empty( $val ) ) : ?>
|
||||
<div class="custom-field">
|
||||
<div class="_name">
|
||||
<?= htmlspecialchars( $custom_field['name'] ) . ':'; ?>
|
||||
</div>
|
||||
<div class="_image">
|
||||
<img src="<?= htmlspecialchars( $val );?>" alt="<?= htmlspecialchars( $custom_field['name'] );?>">
|
||||
</div>
|
||||
</div>
|
||||
<? endif; ?>
|
||||
<? endforeach; ?>
|
||||
<a href="#" class="btn btn-sm btn-default btn-edit-custom-fields">Edytuj personalizację</a>
|
||||
</div>
|
||||
|
||||
<div class="custom-fields-edit" data-product-code="<?= htmlspecialchars( $this->product_code ); ?>" style="display: none;">
|
||||
<? 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; ?>
|
||||
|
||||
<div class="custom-field-edit-row" style="margin-bottom: 5px;">
|
||||
<label>
|
||||
<?= htmlspecialchars( $custom_field['name'] ); ?><?= $is_required ? ' <span style="color:red;">*</span>' : ''; ?>
|
||||
</label>
|
||||
<? if ( $field_type == 'text' ) : ?>
|
||||
<input type="text" class="form-control form-control-sm" name="custom_field[<?= (int)$key; ?>]" value="<?= htmlspecialchars( $val ); ?>" <?= $is_required ? 'required' : ''; ?>>
|
||||
<? elseif ( $field_type == 'image' ) : ?>
|
||||
<input type="text" class="form-control form-control-sm" name="custom_field[<?= (int)$key; ?>]" value="<?= htmlspecialchars( $val ); ?>" placeholder="URL obrazka" <?= $is_required ? 'required' : ''; ?>>
|
||||
<? endif; ?>
|
||||
</div>
|
||||
<? elseif ( $field_type == 'image' && !empty( $val ) ) : ?>
|
||||
<div class="custom-field">
|
||||
<div class="_name">
|
||||
<?= htmlspecialchars( $custom_field['name'] ) . ':'; ?>
|
||||
</div>
|
||||
<div class="_image">
|
||||
<img src="<?= htmlspecialchars( $val );?>" alt="<?= htmlspecialchars( $custom_field['name'] );?>">
|
||||
</div>
|
||||
</div>
|
||||
<? endif; ?>
|
||||
<? endforeach; ?>
|
||||
<? endif;?>
|
||||
<? endforeach; ?>
|
||||
<div style="margin-top: 5px;">
|
||||
<a href="#" class="btn btn-sm btn-primary btn-save-custom-fields">Zapisz</a>
|
||||
<a href="#" class="btn btn-sm btn-default btn-cancel-custom-fields">Anuluj</a>
|
||||
</div>
|
||||
</div>
|
||||
<? endif; ?>
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
<hr>
|
||||
<? 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'] ):?>
|
||||
<div class="basket-product-message">
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
BIN
updates/0.30/ver_0.343.zip
Normal file
BIN
updates/0.30/ver_0.343.zip
Normal file
Binary file not shown.
24
updates/0.30/ver_0.343_manifest.json
Normal file
24
updates/0.30/ver_0.343_manifest.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"changelog": "Custom fields: type + is_required + obsługa obrazków w koszyku",
|
||||
"version": "0.343",
|
||||
"files": {
|
||||
"added": [
|
||||
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"autoload/Domain/Product/ProductRepository.php",
|
||||
"templates/shop-basket/_partials/product-custom-fields.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:bd3b968a4b389c0c3fd00a511e5a3ef0405d4b60079d6b460335f93b8faa92d2",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-03-19",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
<b>ver. 0.343 - 19.03.2026</b><br />
|
||||
Custom fields: type + is_required + obsługa obrazków w koszyku
|
||||
<hr>
|
||||
<b>ver. 0.342 - 19.03.2026</b><br />
|
||||
Apilo: email z danymi zamówienia + infinite retry co 30 min dla order jobów
|
||||
<hr>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?
|
||||
$current_ver = 342;
|
||||
$current_ver = 343;
|
||||
|
||||
for ($i = 1; $i <= $current_ver; $i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user