` z ikoną lub etykietą "Personalizacja:"
+ - Każda linia personalizacji (split po `\n`) jako osobna linia w UI
+ - Escape HTML: użyć `e()` helpera na każdej linii
+
+ 2. W SCSS dodać style:
+ - `.item-personalization` — mały font (0.85em), kolor muted, lekki padding-top
+ - Etykiety (tekst przed `:`) mogą być pogrubione via CSS lub pozostawione jako plain text
+ - Kompaktowy układ, bez nadmiernych marginesów
+
+ Avoid: Nie renderować surowego HTML z custom_fields — zawsze escape.
+ Avoid: Nie dodawać nowych zależności JS — to statyczny tekst.
+
+
+ - Otworzyć zamówienie z personalizacją w przeglądarce — personalizacja widoczna pod nazwą produktu
+ - Otworzyć zamówienie bez personalizacji — brak dodatkowej sekcji
+ - Sprawdzić XSS: wpisać `` w custom_fields shopPRO — powinno być escaped
+
+
AC-2 satisfied: personalizacja wyświetlana w UI pod nazwą produktu
+
+
+
+
+
+
+## DO NOT CHANGE
+- shopPRO codebase (API już zwraca custom_fields — zero zmian po stronie shopPRO)
+- payload_json format i zawartość w order_items
+- Istniejące kolumny i indeksy order_items
+- Logika importu zamówień poza mapowaniem items (OrderImportRepository::upsertOrderAggregate flow)
+
+## SCOPE LIMITS
+- Tylko personalizacja z shopPRO (nie Allegro, nie Erli)
+- Tylko widok szczegółów zamówienia — bez zmian na liście zamówień
+- Bez edycji personalizacji w orderPRO — read-only display
+- Bez parsowania personalizacji do struktury klucz-wartość (JSON) — plain text wystarczy
+
+
+
+
+Before declaring plan complete:
+- [ ] Migracja wykonuje się bez błędów
+- [ ] SCSS kompiluje się bez błędów
+- [ ] Zamówienie z personalizacją — dane widoczne w UI
+- [ ] Zamówienie bez personalizacji — brak sekcji w UI
+- [ ] Brak regresji w istniejącym wyświetlaniu produktów
+- [ ] XSS escape działa poprawnie
+- [ ] Aktualizacja DOCS/DB_SCHEMA.md i DOCS/ARCHITECTURE.md
+
+
+
+- Wszystkie taski zakończone
+- Wszystkie weryfikacje przechodzą
+- Brak błędów PHP/SCSS
+- Personalizacja widoczna w UI dla produktów z custom_fields
+
+
+
diff --git a/.paul/phases/63-order-item-personalization/63-01-SUMMARY.md b/.paul/phases/63-order-item-personalization/63-01-SUMMARY.md
new file mode 100644
index 0000000..f6bc20b
--- /dev/null
+++ b/.paul/phases/63-order-item-personalization/63-01-SUMMARY.md
@@ -0,0 +1,127 @@
+---
+phase: 63-order-item-personalization
+plan: 01
+subsystem: orders
+tags: [personalization, shopPRO, import, custom_fields]
+
+requires:
+ - phase: none
+ provides: n/a
+provides:
+ - Kolumna personalization w order_items
+ - Ekstrakcja custom_fields z shopPRO API przy imporcie
+ - Wyswietlanie personalizacji w widoku zamowienia
+affects: []
+
+tech-stack:
+ added: []
+ patterns: [extractPersonalization HTML-to-text w mapperze]
+
+key-files:
+ created:
+ - database/migrations/20260401_000075_add_personalization_to_order_items.sql
+ modified:
+ - src/Modules/Settings/ShopproOrderMapper.php
+ - src/Modules/Orders/OrderImportRepository.php
+ - resources/views/orders/show.php
+ - resources/scss/app.scss
+ - public/assets/css/app.css
+
+key-decisions:
+ - "Personalizacja jako plain text (nie JSON) — prostosc, wystarczajaca dla wyswietlania"
+ - "date_order dodane do readPath mapper — naprawa brakujacej daty zamowienia"
+
+patterns-established:
+ - "extractPersonalization: HTML strip + br-to-newline dla danych z shopPRO"
+
+duration: ~45min
+started: 2026-04-01T19:00:00Z
+completed: 2026-04-01T19:45:00Z
+---
+
+# Phase 63 Plan 01: Order Item Personalization Summary
+
+**Import i wyswietlanie danych personalizacji produktow z shopPRO (custom_fields) w szczegolach zamowienia orderPRO.**
+
+## Performance
+
+| Metric | Value |
+|--------|-------|
+| Duration | ~45min |
+| Started | 2026-04-01 |
+| Completed | 2026-04-01 |
+| Tasks | 2 completed |
+| Files modified | 7 |
+
+## Acceptance Criteria Results
+
+| Criterion | Status | Notes |
+|-----------|--------|-------|
+| AC-1: Personalizacja zapisana w DB | Pass | custom_fields ekstrakcja z HTML na tekst, zapis w kolumnie personalization |
+| AC-2: Personalizacja wyswietlana w UI | Pass | Blok pod nazwa produktu z etykieta i liniami tekstu |
+| AC-3: Brak personalizacji nie powoduje bledow | Pass | NULL w kolumnie, brak sekcji w UI |
+
+## Accomplishments
+
+- Kolumna `personalization TEXT NULL` w `order_items` + ekstrakcja `custom_fields` z shopPRO API
+- Wyswietlanie personalizacji w widoku zamowienia z escape XSS
+- Naprawa brakujacej daty zamowienia (`date_order` dodane do mapper readPath)
+- Usuniecie zbednego pola `item_type` z widoku i duplikatu `source_order_id`
+- Dodanie `margin-bottom` do section-title dla lepszego odstepy pod kreska
+
+## Files Created/Modified
+
+| File | Change | Purpose |
+|------|--------|---------|
+| `database/migrations/20260401_000075_add_personalization_to_order_items.sql` | Created | Kolumna personalization w order_items |
+| `src/Modules/Settings/ShopproOrderMapper.php` | Modified | extractPersonalization() + date_order w readPath |
+| `src/Modules/Orders/OrderImportRepository.php` | Modified | personalization w INSERT query |
+| `resources/views/orders/show.php` | Modified | Wyswietlanie personalizacji, usuniecie item_type i source_order_id |
+| `resources/scss/app.scss` | Modified | Style .item-personalization + section-title margin-bottom |
+| `public/assets/css/app.css` | Modified | Skompilowany CSS |
+| `DOCS/DB_SCHEMA.md` | Modified | Dokumentacja kolumny personalization |
+| `DOCS/TECH_CHANGELOG.md` | Modified | Wpis Phase 63 |
+
+## Decisions Made
+
+| Decision | Rationale | Impact |
+|----------|-----------|--------|
+| Plain text zamiast JSON dla personalizacji | Prostosc — dane sa read-only, nie wymagaja struktury klucz-wartosc | Wystarczajace dla wyswietlania |
+| date_order w readPath mappera | shopPRO API zwraca date_order, mapper nie mial tego klucza | Naprawia brakujaca date zamowienia |
+| Usuniecie item_type z widoku | Zawsze "product", zero informacji | Czystszy UI |
+| Usuniecie source_order_id z widoku | Duplikat external_order_id | Czystszy UI |
+
+## Deviations from Plan
+
+### Summary
+
+| Type | Count | Impact |
+|------|-------|--------|
+| Scope additions | 3 | Drobne poprawki UI wykryte przy testowaniu |
+| Deferred | 0 | - |
+
+**Total impact:** Drobne poprawki UI (date_order mapping, item_type/source_order_id cleanup, section-title spacing) — naturalne odkrycia przy testowaniu na zywo.
+
+## Issues Encountered
+
+| Issue | Resolution |
+|-------|------------|
+| Lokalna DB offline | Migracja uruchomiona na zdalnej DB przez mysql CLI |
+| Cron nie pobral zamowienia (stary updated_at) | Touch updated_at w shopPRO DB + reset kursora sync |
+| marianek.pl to osobna instancja shopPRO | Znaleziony config.php z danymi DB |
+
+## Next Phase Readiness
+
+**Ready:**
+- Personalizacja importowana i wyswietlana
+- Mapper rozszerzalny o kolejne pola shopPRO API
+
+**Concerns:**
+- Brak
+
+**Blockers:**
+- None
+
+---
+*Phase: 63-order-item-personalization, Plan: 01*
+*Completed: 2026-04-01*
diff --git a/DOCS/DB_SCHEMA.md b/DOCS/DB_SCHEMA.md
index f5aa93b..6f6a42a 100644
--- a/DOCS/DB_SCHEMA.md
+++ b/DOCS/DB_SCHEMA.md
@@ -158,6 +158,7 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
- schema neutralna wzgledem dostawcy API (pola `source_*`, `external_*`),
- kolekcje zamowienia rozdzielone na osobne tabele 1:N,
- `payload_json` dostepne dla diagnostyki/replay,
+ - `personalization` (TEXT, nullable) w `order_items` — dane personalizacji produktu z shopPRO (custom_fields), przechowywane jako czysty tekst,
- historia zmian statusow utrzymywana w `order_status_history`.
### `integration_order_sync_state`
diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md
index 994efad..4c7bbf6 100644
--- a/DOCS/TECH_CHANGELOG.md
+++ b/DOCS/TECH_CHANGELOG.md
@@ -1,5 +1,13 @@
# Tech Changelog
+## 2026-04-01 (Phase 63 - Order Item Personalization, Plan 01)
+- Migracja `20260401_000075_add_personalization_to_order_items.sql`: kolumna `personalization TEXT NULL` w `order_items`.
+- `ShopproOrderMapper::extractPersonalization()`: ekstrakcja `custom_fields` z odpowiedzi shopPRO API, konwersja HTML na czysty tekst (strip_tags, html_entity_decode, br->newline).
+- `ShopproOrderMapper::mapItems()`: dodanie klucza `personalization` do mapowanego itemu.
+- `OrderImportRepository::replaceItems()`: zapis `personalization` do INSERT query.
+- `resources/views/orders/show.php`: warunkowe wyswietlanie personalizacji pod nazwa produktu (div.item-personalization z etykieta i liniami tekstu, escape via `e()`).
+- `resources/scss/app.scss`: style `.item-personalization` (kompaktowy blok, border-left, muted colors).
+
## 2026-03-31 (Phase 60 - Order Status Aged Event, Plan 01)
- Migracja `20260331_000074_seed_order_status_aged_cron.sql`: seed cron schedule `order_status_aged` co 3600s.
- `OrderStatusAgedService`: skanuje zamowienia w danym statusie od X dni (query HAVING MAX(changed_at) na `order_status_history`), limit 100/regule, trigger `order.status_aged`.
diff --git a/database/migrations/20260401_000075_add_personalization_to_order_items.sql b/database/migrations/20260401_000075_add_personalization_to_order_items.sql
new file mode 100644
index 0000000..6fa698b
--- /dev/null
+++ b/database/migrations/20260401_000075_add_personalization_to_order_items.sql
@@ -0,0 +1 @@
+ALTER TABLE order_items ADD COLUMN personalization TEXT NULL AFTER payload_json;
diff --git a/public/assets/css/app.css b/public/assets/css/app.css
index 36cbc5a..a102637 100644
--- a/public/assets/css/app.css
+++ b/public/assets/css/app.css
@@ -1099,6 +1099,7 @@ h4.section-title {
gap: 6px;
font-weight: 600;
padding: 6px 0;
+ margin-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
color: var(--c-primary, #2563eb);
}
@@ -2313,6 +2314,27 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
color: #0f172a;
}
+.item-personalization {
+ margin-top: 4px;
+ padding: 4px 8px;
+ background: #f8fafc;
+ border-left: 2px solid #cbd5e1;
+ border-radius: 2px;
+ font-size: 0.92em;
+ color: #475569;
+ line-height: 1.4;
+}
+.item-personalization__label {
+ font-weight: 600;
+ color: #64748b;
+ display: block;
+ margin-bottom: 2px;
+}
+.item-personalization__line {
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
.order-grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
diff --git a/resources/scss/app.css b/resources/scss/app.css
index 677dd06..a102637 100644
--- a/resources/scss/app.css
+++ b/resources/scss/app.css
@@ -1099,6 +1099,7 @@ h4.section-title {
gap: 6px;
font-weight: 600;
padding: 6px 0;
+ margin-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
color: var(--c-primary, #2563eb);
}
@@ -1909,6 +1910,7 @@ h4.section-title::before {
font-size: 12px;
font-weight: 700;
line-height: 1.1;
+ white-space: nowrap;
}
.order-tag.is-info {
border-color: #bfdbfe;
@@ -2312,6 +2314,27 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
color: #0f172a;
}
+.item-personalization {
+ margin-top: 4px;
+ padding: 4px 8px;
+ background: #f8fafc;
+ border-left: 2px solid #cbd5e1;
+ border-radius: 2px;
+ font-size: 0.92em;
+ color: #475569;
+ line-height: 1.4;
+}
+.item-personalization__label {
+ font-weight: 600;
+ color: #64748b;
+ display: block;
+ margin-bottom: 2px;
+}
+.item-personalization__line {
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
.order-grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -2393,6 +2416,7 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
.payment-add-form__actions {
display: flex;
gap: 8px;
+ margin-top: 12px;
}
.order-kv dt {
diff --git a/resources/scss/app.scss b/resources/scss/app.scss
index 98ab0d8..da933b0 100644
--- a/resources/scss/app.scss
+++ b/resources/scss/app.scss
@@ -359,6 +359,7 @@ h4.section-title {
gap: 6px;
font-weight: 600;
padding: 6px 0;
+ margin-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
color: var(--c-primary, #2563eb);
@@ -1602,6 +1603,29 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
color: #0f172a;
}
+.item-personalization {
+ margin-top: 4px;
+ padding: 4px 8px;
+ background: #f8fafc;
+ border-left: 2px solid #cbd5e1;
+ border-radius: 2px;
+ font-size: 0.92em;
+ color: #475569;
+ line-height: 1.4;
+
+ &__label {
+ font-weight: 600;
+ color: #64748b;
+ display: block;
+ margin-bottom: 2px;
+ }
+
+ &__line {
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+}
+
.order-grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
diff --git a/resources/views/orders/show.php b/resources/views/orders/show.php
index 97d24d7..e5f5e80 100644
--- a/resources/views/orders/show.php
+++ b/resources/views/orders/show.php
@@ -169,7 +169,17 @@ foreach ($addressesList as $address) {
= $e((string) ($item['original_name'] ?? '')) ?>
-
= $e((string) ($item['item_type'] ?? '')) ?>
+
+
+
+
Personalizacja:
+
+
+
= $e(trim($line)) ?>
+
+
+
+
@@ -193,7 +203,6 @@ foreach ($addressesList as $address) {