update
This commit is contained in:
@@ -71,6 +71,7 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [x] Automatyzacja: event `order.status_aged` (cron) + warunek `days_in_status` — Phase 60
|
- [x] Automatyzacja: event `order.status_aged` (cron) + warunek `days_in_status` — Phase 60
|
||||||
- [x] Aktywacja przycisku Platnosc w headerze zamowienia + poprawa odstepu w formularzu platnosci — Phase 61
|
- [x] Aktywacja przycisku Platnosc w headerze zamowienia + poprawa odstepu w formularzu platnosci — Phase 61
|
||||||
- [x] Ochrona danych lokalnych przy re-imporcie + rozroznienie import/aktualizacja w activity log shopPRO — Phase 62
|
- [x] Ochrona danych lokalnych przy re-imporcie + rozroznienie import/aktualizacja w activity log shopPRO — Phase 62
|
||||||
|
- [x] Import i wyswietlanie personalizacji produktow z shopPRO (custom_fields) + naprawa daty zamowienia — Phase 63
|
||||||
|
|
||||||
### Active (In Progress)
|
### Active (In Progress)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
|
|||||||
| 60 | Order Status Aged Event | 1/1 | Complete |
|
| 60 | Order Status Aged Event | 1/1 | Complete |
|
||||||
| 61 | Payment Button Activation | 1/1 | Complete |
|
| 61 | Payment Button Activation | 1/1 | Complete |
|
||||||
| 62 | Import Re-import Safety | 1/1 | Complete |
|
| 62 | Import Re-import Safety | 1/1 | Complete |
|
||||||
|
| 63 | Order Item Personalization | 1/1 | Complete |
|
||||||
| TBD | Mobile Orders List | - | Not started |
|
| TBD | Mobile Orders List | - | Not started |
|
||||||
| TBD | Mobile Order Details | - | Not started |
|
| TBD | Mobile Order Details | - | Not started |
|
||||||
| TBD | Mobile Settings | - | Not started |
|
| TBD | Mobile Settings | - | Not started |
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
See: .paul/PROJECT.md (updated 2026-03-31)
|
See: .paul/PROJECT.md (updated 2026-03-31)
|
||||||
|
|
||||||
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
|
||||||
**Current focus:** Milestone v3.0 — Phase 62 complete, ready for next PLAN
|
**Current focus:** Milestone v3.0 — Phase 63 complete, ready for next PLAN
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v3.0 Mobile Responsive — In progress
|
Milestone: v3.0 Mobile Responsive — In progress
|
||||||
Phase: 11 of N (62 - Import Re-import Safety) — Complete
|
Phase: 12 of N (63 - Order Item Personalization) — Complete
|
||||||
Plan: 62-01 complete
|
Plan: 63-01 complete
|
||||||
Status: Loop complete — phase 62 done, ready for next PLAN
|
Status: Loop complete — phase 63 done, ready for next PLAN
|
||||||
Last activity: 2026-03-31 — UNIFY closed for 62-01
|
Last activity: 2026-04-01 — UNIFY closed for 63-01
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone: [######░░░░] ~58%
|
- Milestone: [######░░░░] ~62%
|
||||||
- Phase 62: [##########] 100%
|
- Phase 63: [##########] 100%
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
@@ -29,13 +29,13 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-31
|
Last session: 2026-04-01
|
||||||
Stopped at: Phase 62 complete
|
Stopped at: Phase 63 complete
|
||||||
Next action: /paul:plan dla kolejnego modulu
|
Next action: /paul:plan dla kolejnego modulu
|
||||||
Resume file: .paul/phases/62-import-reimport-safety/62-01-SUMMARY.md
|
Resume file: .paul/phases/63-order-item-personalization/63-01-SUMMARY.md
|
||||||
|
|
||||||
## Git State
|
## Git State
|
||||||
|
|
||||||
Last commit: af48e84
|
Last commit: 34b0a2b
|
||||||
Branch: main
|
Branch: main
|
||||||
Feature branches merged: none
|
Feature branches merged: none
|
||||||
|
|||||||
164
.paul/phases/63-order-item-personalization/63-01-PLAN.md
Normal file
164
.paul/phases/63-order-item-personalization/63-01-PLAN.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
phase: 63-order-item-personalization
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260401_000063_add_personalization_to_order_items.sql
|
||||||
|
- src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
- src/Modules/Orders/OrderImportRepository.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- resources/scss/orders/_show.scss
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Pobieranie danych personalizacji produktów z shopPRO i wyświetlanie ich w szczegółach zamówienia orderPRO.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Sprzedawcy widzą personalizacje klientów (np. tekst grawerunku, link Spotify, dedykacja) bezpośrednio w orderPRO bez konieczności logowania do shopPRO. Kluczowe dla realizacji zamówień z personalizowanymi produktami.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Migracja DB: kolumna `personalization` w `order_items`
|
||||||
|
- Mapper: ekstrakcja `custom_fields` z odpowiedzi shopPRO API
|
||||||
|
- UI: wyświetlanie personalizacji pod nazwą produktu w widoku zamówienia
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Settings/ShopproOrderMapper.php — mapItems() linia 527, mapuje produkty z API shopPRO
|
||||||
|
@src/Modules/Orders/OrderImportRepository.php — replaceItems() wstawia pozycje do DB
|
||||||
|
@resources/views/orders/show.php — widok szczegółów zamówienia, sekcja produktów
|
||||||
|
@database/migrations/20260302_000018_create_orders_tables_and_schedule.sql — schemat order_items
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
No specialized flows configured.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Personalizacja zapisana w bazie danych
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie z shopPRO zawiera produkty z custom_fields
|
||||||
|
When import zamówienia się wykonuje (cron lub ręczny)
|
||||||
|
Then kolumna personalization w order_items zawiera dane personalizacji jako czysty tekst (bez HTML)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Personalizacja wyświetlana w szczegółach zamówienia
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie w orderPRO ma pozycje z wypełnioną personalizacją
|
||||||
|
When użytkownik otwiera szczegóły zamówienia (/orders/{id})
|
||||||
|
Then pod nazwą produktu wyświetla się sekcja personalizacji z etykietami i wartościami
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Brak personalizacji nie powoduje błędów
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie z shopPRO zawiera produkty BEZ custom_fields (puste lub null)
|
||||||
|
When import się wykonuje i użytkownik otwiera szczegóły
|
||||||
|
Then kolumna personalization jest NULL i w UI nie pojawia się sekcja personalizacji
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Migracja DB + mapping personalizacji</name>
|
||||||
|
<files>database/migrations/20260401_000063_add_personalization_to_order_items.sql, src/Modules/Settings/ShopproOrderMapper.php, src/Modules/Orders/OrderImportRepository.php</files>
|
||||||
|
<action>
|
||||||
|
1. Utworzyć migrację dodającą kolumnę `personalization TEXT NULL` do tabeli `order_items` (po kolumnie `payload_json`).
|
||||||
|
|
||||||
|
2. W `ShopproOrderMapper::mapItems()` (linia 561-580):
|
||||||
|
- Wyciągnąć `custom_fields` z `$row` za pomocą `$this->readPath($row, ['custom_fields'])`
|
||||||
|
- Przekonwertować HTML na czysty tekst: zamienić `<br>` na `\n`, usunąć tagi HTML (`strip_tags`), `html_entity_decode`, `trim`
|
||||||
|
- Zapisać jako klucz `personalization` w tablicy wynikowej (obok `payload_json`)
|
||||||
|
- Jeśli wynik jest pustym stringiem, ustawić `null`
|
||||||
|
|
||||||
|
3. W `OrderImportRepository::replaceItems()`:
|
||||||
|
- Dodać kolumnę `personalization` do INSERT query
|
||||||
|
- Bindować wartość z tablicy item
|
||||||
|
|
||||||
|
Avoid: Nie zmieniać formatu `payload_json` — surowe dane API muszą zostać nienaruszone.
|
||||||
|
Avoid: Nie parsować HTML do JSON — czysty tekst z zachowanymi newlines jest wystarczający.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Migracja wykonuje się bez błędów: `php database/migrate.php`
|
||||||
|
- Po re-imporcie zamówienia z shopPRO kolumna `personalization` zawiera tekst
|
||||||
|
- Zamówienia bez personalizacji mają NULL w kolumnie
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 i AC-3 satisfied: personalizacja zapisywana w DB, brak personalizacji = NULL</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Wyświetlanie personalizacji w widoku zamówienia</name>
|
||||||
|
<files>resources/views/orders/show.php, resources/scss/orders/_show.scss</files>
|
||||||
|
<action>
|
||||||
|
1. W `resources/views/orders/show.php`, w sekcji renderującej produkty zamówienia:
|
||||||
|
- Pod nazwą produktu (`original_name`) dodać blok warunkowy: jeśli `$item['personalization']` nie jest puste
|
||||||
|
- Wyświetlić personalizację w `<div class="item-personalization">` 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.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Otworzyć zamówienie z personalizacją w przeglądarce — personalizacja widoczna pod nazwą produktu
|
||||||
|
- Otworzyć zamówienie bez personalizacji — brak dodatkowej sekcji
|
||||||
|
- Sprawdzić XSS: wpisać `<script>alert(1)</script>` w custom_fields shopPRO — powinno być escaped
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 satisfied: personalizacja wyświetlana w UI pod nazwą produktu</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
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
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Wszystkie taski zakończone
|
||||||
|
- Wszystkie weryfikacje przechodzą
|
||||||
|
- Brak błędów PHP/SCSS
|
||||||
|
- Personalizacja widoczna w UI dla produktów z custom_fields
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/63-order-item-personalization/63-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
127
.paul/phases/63-order-item-personalization/63-01-SUMMARY.md
Normal file
127
.paul/phases/63-order-item-personalization/63-01-SUMMARY.md
Normal file
@@ -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*
|
||||||
@@ -158,6 +158,7 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
|||||||
- schema neutralna wzgledem dostawcy API (pola `source_*`, `external_*`),
|
- schema neutralna wzgledem dostawcy API (pola `source_*`, `external_*`),
|
||||||
- kolekcje zamowienia rozdzielone na osobne tabele 1:N,
|
- kolekcje zamowienia rozdzielone na osobne tabele 1:N,
|
||||||
- `payload_json` dostepne dla diagnostyki/replay,
|
- `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`.
|
- historia zmian statusow utrzymywana w `order_status_history`.
|
||||||
|
|
||||||
### `integration_order_sync_state`
|
### `integration_order_sync_state`
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# Tech Changelog
|
# 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)
|
## 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.
|
- 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`.
|
- `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`.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE order_items ADD COLUMN personalization TEXT NULL AFTER payload_json;
|
||||||
@@ -1099,6 +1099,7 @@ h4.section-title {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
color: var(--c-primary, #2563eb);
|
color: var(--c-primary, #2563eb);
|
||||||
}
|
}
|
||||||
@@ -2313,6 +2314,27 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
|
|||||||
color: #0f172a;
|
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 {
|
.order-grid-2 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -1099,6 +1099,7 @@ h4.section-title {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
color: var(--c-primary, #2563eb);
|
color: var(--c-primary, #2563eb);
|
||||||
}
|
}
|
||||||
@@ -1909,6 +1910,7 @@ h4.section-title::before {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.order-tag.is-info {
|
.order-tag.is-info {
|
||||||
border-color: #bfdbfe;
|
border-color: #bfdbfe;
|
||||||
@@ -2312,6 +2314,27 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
|
|||||||
color: #0f172a;
|
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 {
|
.order-grid-2 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
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 {
|
.payment-add-form__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-kv dt {
|
.order-kv dt {
|
||||||
|
|||||||
@@ -359,6 +359,7 @@ h4.section-title {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
color: var(--c-primary, #2563eb);
|
color: var(--c-primary, #2563eb);
|
||||||
|
|
||||||
@@ -1602,6 +1603,29 @@ details[open] > .order-statuses-side__title .order-statuses-side__arrow {
|
|||||||
color: #0f172a;
|
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 {
|
.order-grid-2 {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -169,7 +169,17 @@ foreach ($addressesList as $address) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div>
|
<div>
|
||||||
<div class="order-item-name"><?= $e((string) ($item['original_name'] ?? '')) ?></div>
|
<div class="order-item-name"><?= $e((string) ($item['original_name'] ?? '')) ?></div>
|
||||||
<div class="muted"><?= $e((string) ($item['item_type'] ?? '')) ?></div>
|
<?php $personalization = trim((string) ($item['personalization'] ?? '')); ?>
|
||||||
|
<?php if ($personalization !== ''): ?>
|
||||||
|
<div class="item-personalization">
|
||||||
|
<span class="item-personalization__label">Personalizacja:</span>
|
||||||
|
<?php foreach (explode("\n", $personalization) as $line): ?>
|
||||||
|
<?php if (trim($line) !== ''): ?>
|
||||||
|
<div class="item-personalization__line"><?= $e(trim($line)) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -193,7 +203,6 @@ foreach ($addressesList as $address) {
|
|||||||
<dl class="order-kv mt-12">
|
<dl class="order-kv mt-12">
|
||||||
<dt><?= $e($t('orders.details.fields.status')) ?></dt><dd><?= $e((string) ($statusLabel ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.status')) ?></dt><dd><?= $e((string) ($statusLabel ?? '-')) ?></dd>
|
||||||
<dt>Nr zamowienia</dt><dd><strong><?= $e((string) ($orderRow['internal_order_number'] ?? '-')) ?></strong></dd>
|
<dt>Nr zamowienia</dt><dd><strong><?= $e((string) ($orderRow['internal_order_number'] ?? '-')) ?></strong></dd>
|
||||||
<dt><?= $e($t('orders.details.fields.source_order_id')) ?></dt><dd><?= $e((string) ($orderRow['source_order_id'] ?? '-')) ?></dd>
|
|
||||||
<dt><?= $e($t('orders.details.fields.external_order_id')) ?></dt><dd><?= $e((string) ($orderRow['external_order_id'] ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.external_order_id')) ?></dt><dd><?= $e((string) ($orderRow['external_order_id'] ?? '-')) ?></dd>
|
||||||
<dt><?= $e($t('orders.details.fields.ordered_at')) ?></dt><dd><?= $e((string) ($orderRow['ordered_at'] ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.ordered_at')) ?></dt><dd><?= $e((string) ($orderRow['ordered_at'] ?? '-')) ?></dd>
|
||||||
<dt><?= $e($t('orders.details.fields.customer_login')) ?></dt><dd><?= $e((string) ($orderRow['customer_login'] ?? '-')) ?></dd>
|
<dt><?= $e($t('orders.details.fields.customer_login')) ?></dt><dd><?= $e((string) ($orderRow['customer_login'] ?? '-')) ?></dd>
|
||||||
|
|||||||
@@ -256,11 +256,11 @@ final class OrderImportRepository
|
|||||||
'INSERT INTO order_items (
|
'INSERT INTO order_items (
|
||||||
order_id, source_item_id, external_item_id, ean, sku, original_name, original_code,
|
order_id, source_item_id, external_item_id, ean, sku, original_name, original_code,
|
||||||
original_price_with_tax, original_price_without_tax, media_url, quantity, tax_rate, item_status,
|
original_price_with_tax, original_price_without_tax, media_url, quantity, tax_rate, item_status,
|
||||||
unit, item_type, source_product_id, source_product_set_id, sort_order, payload_json
|
unit, item_type, source_product_id, source_product_set_id, sort_order, payload_json, personalization
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:order_id, :source_item_id, :external_item_id, :ean, :sku, :original_name, :original_code,
|
:order_id, :source_item_id, :external_item_id, :ean, :sku, :original_name, :original_code,
|
||||||
:original_price_with_tax, :original_price_without_tax, :media_url, :quantity, :tax_rate, :item_status,
|
:original_price_with_tax, :original_price_without_tax, :media_url, :quantity, :tax_rate, :item_status,
|
||||||
:unit, :item_type, :source_product_id, :source_product_set_id, :sort_order, :payload_json
|
:unit, :item_type, :source_product_id, :source_product_set_id, :sort_order, :payload_json, :personalization
|
||||||
)'
|
)'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -285,6 +285,7 @@ final class OrderImportRepository
|
|||||||
'source_product_set_id' => $row['source_product_set_id'] ?? null,
|
'source_product_set_id' => $row['source_product_set_id'] ?? null,
|
||||||
'sort_order' => (int) ($row['sort_order'] ?? 0),
|
'sort_order' => (int) ($row['sort_order'] ?? 0),
|
||||||
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
'payload_json' => $this->encodeJson($row['payload_json'] ?? null),
|
||||||
|
'personalization' => $row['personalization'] ?? null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ final class ShopproOrderMapper
|
|||||||
$sourceOrderId = $fallbackOrderId;
|
$sourceOrderId = $fallbackOrderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
$sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add']));
|
$sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add', 'date_order']));
|
||||||
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
|
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
|
||||||
if ($sourceUpdatedAt === null) {
|
if ($sourceUpdatedAt === null) {
|
||||||
$sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
|
$sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
|
||||||
@@ -577,12 +577,30 @@ final class ShopproOrderMapper
|
|||||||
'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
|
'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
|
||||||
'sort_order' => $sort++,
|
'sort_order' => $sort++,
|
||||||
'payload_json' => $row,
|
'payload_json' => $row,
|
||||||
|
'personalization' => $this->extractPersonalization($row),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $row
|
||||||
|
*/
|
||||||
|
private function extractPersonalization(array $row): ?string
|
||||||
|
{
|
||||||
|
$raw = $this->readPath($row, ['custom_fields']);
|
||||||
|
if ($raw === null || $raw === '' || $raw === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = str_replace(['<br>', '<br/>', '<br />'], "\n", (string) $raw);
|
||||||
|
$text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
$text = trim($text);
|
||||||
|
|
||||||
|
return $text !== '' ? $text : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $payload
|
* @param array<string, mixed> $payload
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
|
|||||||
Reference in New Issue
Block a user