Compare commits

..

2 Commits

Author SHA1 Message Date
Jacek
09f51be1c1 feat: edycja personalizacji produktu w koszyku
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>
2026-03-19 19:45:02 +01:00
Jacek
951a82a5b1 build: ver_0.343 - Custom fields: type + is_required + obsługa obrazków w koszyku
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:37:36 +01:00
14 changed files with 578 additions and 36 deletions

View File

@@ -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

View File

@@ -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: 9Apilo 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: 10Edycja 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*

View 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>

View 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*

View File

@@ -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;

View File

@@ -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`

View File

@@ -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)

View File

@@ -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; ?>

View File

@@ -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">

View File

@@ -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

Binary file not shown.

View 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": [
]
}

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<?
$current_ver = 342;
$current_ver = 343;
for ($i = 1; $i <= $current_ver; $i++)
{