update
This commit is contained in:
@@ -78,6 +78,14 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów
|
|||||||
- [x] Zapamiętywanie wybranej liczby wierszy na stronie (per_page) w localStorage — Phase 72
|
- [x] Zapamiętywanie wybranej liczby wierszy na stronie (per_page) w localStorage — Phase 72
|
||||||
- [x] Wyszukiwanie zamowien po nazwie produktu (EXISTS subquery) — Phase 73
|
- [x] Wyszukiwanie zamowien po nazwie produktu (EXISTS subquery) — Phase 73
|
||||||
- [x] Odwrocenie mapowania statusow: orderPRO po lewej, zewnetrzne po prawej (shopPRO + Allegro) — Phase 74
|
- [x] Odwrocenie mapowania statusow: orderPRO po lewej, zewnetrzne po prawej (shopPRO + Allegro) — Phase 74
|
||||||
|
- [x] Rozdzielenie mapowania push/pull statusow shopPRO + ochrona statusu przy re-imporcie — Phase 75
|
||||||
|
- [x] Fallback danych odbiorcy z customer gdy delivery nie ma adresu (shopPRO) — Phase 76
|
||||||
|
- [x] Naprawa auto-fill kwoty pobrania (COD) dla zamowien shopPRO — Phase 77
|
||||||
|
- [x] Presety przesylek: auto-submit formularza po autofill — Phase 78
|
||||||
|
- [x] Import pola message z shopPRO do personalizacji pozycji i notatek zamowienia — Phase 79
|
||||||
|
- [x] Przeladowanie listy zamowien po zmianie statusu inline (location.reload) — Phase 80
|
||||||
|
- [x] Globalna wyszukiwarka zamowien w topbarze (AJAX search, dropdown, nawigacja klawiaturowa) — Phase 81
|
||||||
|
- [x] Tooltip z pelna nazwa produktu na liscie zamowien (natywny title attribute) — Phase 82
|
||||||
- [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68
|
- [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68
|
||||||
|
|
||||||
### Active (In Progress)
|
### Active (In Progress)
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel
|
|||||||
| 72 | Per Page Persistence | 1/1 | Complete |
|
| 72 | Per Page Persistence | 1/1 | Complete |
|
||||||
| 73 | Search by Product | 1/1 | Complete |
|
| 73 | Search by Product | 1/1 | Complete |
|
||||||
| 74 | Reverse Status Mapping | 1/1 | Complete |
|
| 74 | Reverse Status Mapping | 1/1 | Complete |
|
||||||
|
| 75 | Pull Status Mapping | 1/1 | Complete |
|
||||||
|
| 76 | Shipment Receiver Fallback | 1/1 | Complete |
|
||||||
|
| 77 | COD Amount Fix | 1/1 | Complete |
|
||||||
|
| 78 | Preset Auto Submit | 1/1 | Complete |
|
||||||
|
| 79 | Personalization Message Field | 1/1 | Complete |
|
||||||
|
| 80 | Status Change Reload | 1/1 | Complete |
|
||||||
|
| 81 | Global Search Bar | 1/1 | Complete |
|
||||||
|
| 82 | Product Title Tooltip | 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,36 +5,36 @@
|
|||||||
See: .paul/PROJECT.md (updated 2026-04-07)
|
See: .paul/PROJECT.md (updated 2026-04-07)
|
||||||
|
|
||||||
**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 74 complete, ready for next PLAN
|
**Current focus:** Milestone v3.0 - Phase 82 complete, ready for next PLAN
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v3.0 Mobile Responsive - In progress
|
Milestone: v3.0 Mobile Responsive - In progress
|
||||||
Phase: 74 (Reverse Status Mapping) — Complete
|
Phase: 82 (Product Title Tooltip) — Complete
|
||||||
Plan: 74-01 unified
|
Plan: 82-01 unified
|
||||||
Status: Loop complete, ready for next PLAN
|
Status: Loop complete, ready for next PLAN
|
||||||
Last activity: 2026-04-07 — Unified .paul/phases/74-reverse-status-mapping/74-01-PLAN.md
|
Last activity: 2026-04-07 — Unified .paul/phases/82-product-title-tooltip/82-01-PLAN.md
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone: [########..] ~78%
|
- Milestone: [########..] ~89%
|
||||||
- Phase 74: [##########] 100%
|
- Phase 82: [##########] 100%
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN --> APPLY --> UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
✓ ✓ ✓ [Loop complete - ready for next PLAN]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-07
|
Last session: 2026-04-07
|
||||||
Stopped at: Plan 74-01 unified
|
Stopped at: Plan 82-01 unified
|
||||||
Next action: Run /paul:plan for the next prioritized phase
|
Next action: Run /paul:plan for the next prioritized phase
|
||||||
Resume file: .paul/phases/74-reverse-status-mapping/74-01-SUMMARY.md
|
Resume file: .paul/phases/82-product-title-tooltip/82-01-SUMMARY.md
|
||||||
|
|
||||||
## Git State
|
## Git State
|
||||||
|
|
||||||
Last commit: aadf98b
|
Last commit: 1933c74
|
||||||
Branch: main
|
Branch: main
|
||||||
|
|||||||
254
.paul/phases/75-pull-status-mapping/75-01-PLAN.md
Normal file
254
.paul/phases/75-pull-status-mapping/75-01-PLAN.md
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
---
|
||||||
|
phase: 75-pull-status-mapping
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260407_000079_pull_status_mappings.sql
|
||||||
|
- src/Modules/Settings/ShopproPullStatusMappingRepository.php
|
||||||
|
- src/Modules/Settings/ShopproIntegrationsController.php
|
||||||
|
- src/Modules/Settings/ShopproOrdersSyncService.php
|
||||||
|
- resources/views/settings/shoppro.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- routes/web.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Rozdzielenie mapowania statusów shopPRO na dwa niezależne kierunki: PUSH (orderPRO→shopPRO, istniejąca tabela) i PULL (shopPRO→orderPRO, nowa tabela). Naprawa bugu Phase 74 gdzie wiele statusów orderPRO mapuje na ten sam kod shopPRO, a pull direction bierze alfabetycznie pierwszy (zawsze "do_odbioru" zamiast "w_realizacji").
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Zamówienia importowane z shopPRO dostają złe statusy w orderPRO. Wszystkie zamówienia ze statusem shopPRO "4" (przyjęte do realizacji) lądują jako "do_odbioru" zamiast "w_realizacji" — potwierdzone na 4 zamówieniach (211-214).
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Nowa tabela `order_status_pull_mappings` (shopPRO code → orderPRO code, UNIQUE na shoppro side)
|
||||||
|
- Sekcja "Mapowanie przy imporcie" w UI pod istniejącym mapowaniem push
|
||||||
|
- `buildStatusMap()` korzysta z nowej tabeli pull zamiast odwracania push mappings
|
||||||
|
- Naprawione zamówienia 211-214
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Settings/ShopproIntegrationsController.php (saveStatusMappings, buildMappingIndex, show — lines 55-120, 222-274, 500-518)
|
||||||
|
@src/Modules/Settings/ShopproStatusMappingRepository.php (listByIntegration, replaceForIntegration)
|
||||||
|
@src/Modules/Settings/ShopproOrdersSyncService.php (buildStatusMap — lines 302-318)
|
||||||
|
@resources/views/settings/shoppro.php (status mapping form — lines 195-265)
|
||||||
|
@resources/lang/pl.php (order_statuses section — lines 1008-1040)
|
||||||
|
@routes/web.php (shoppro routes — lines 449-450)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Nowa tabela pull mappings
|
||||||
|
```gherkin
|
||||||
|
Given tabela order_status_pull_mappings istnieje
|
||||||
|
When zapiszę mapowanie shopPRO kod "4" → orderPRO "w_realizacji" dla integracji 7
|
||||||
|
Then wiersz jest zapisany z UNIQUE constraint na (integration_id, shoppro_status_code)
|
||||||
|
And nie mogę zapisać drugiego wiersza z tym samym (integration_id, shoppro_status_code)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Sekcja pull mapping w UI
|
||||||
|
```gherkin
|
||||||
|
Given jestem na stronie Ustawienia > shopPRO > Statusy
|
||||||
|
When widzę formularz mapowania
|
||||||
|
Then pod sekcją "Wysyłka statusów" widzę sekcję "Import statusów"
|
||||||
|
And sekcja importu pokazuje statusy shopPRO po lewej z dropdownem orderPRO po prawej
|
||||||
|
And mogę zapisać mapowanie pull niezależnie od push
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Import używa pull mappings
|
||||||
|
```gherkin
|
||||||
|
Given mam pull mapping: shopPRO "4" → orderPRO "w_realizacji"
|
||||||
|
When cron importuje zamówienie ze statusem shopPRO "4"
|
||||||
|
Then zamówienie dostaje external_status_id = "w_realizacji"
|
||||||
|
And stara tabela order_status_mappings nie jest używana do pull
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Naprawa istniejących zamówień
|
||||||
|
```gherkin
|
||||||
|
Given zamówienia 211-214 mają external_status_id = "do_odbioru"
|
||||||
|
When pull mapping shopPRO "4" → "w_realizacji" jest skonfigurowane
|
||||||
|
And cron się uruchomi
|
||||||
|
Then zamówienia 211-214 zostaną zaktualizowane na "w_realizacji"
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Migracja DB + Repository</name>
|
||||||
|
<files>database/migrations/20260407_000079_pull_status_mappings.sql, src/Modules/Settings/ShopproPullStatusMappingRepository.php</files>
|
||||||
|
<action>
|
||||||
|
1. Utworzyć migrację SQL tworzącą tabelę `order_status_pull_mappings`:
|
||||||
|
- id INT AUTO_INCREMENT PRIMARY KEY
|
||||||
|
- integration_id INT NOT NULL
|
||||||
|
- shoppro_status_code VARCHAR(100) NOT NULL
|
||||||
|
- shoppro_status_name VARCHAR(255) DEFAULT NULL
|
||||||
|
- orderpro_status_code VARCHAR(100) NOT NULL
|
||||||
|
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
- UNIQUE INDEX (integration_id, shoppro_status_code)
|
||||||
|
- INDEX (integration_id)
|
||||||
|
|
||||||
|
2. W migracji: pre-populate z istniejących danych. Dla każdej integracji i każdego unikalnego shoppro_status_code wziąć wiersz z `order_status_mappings` gdzie orderpro_status_code = 'w_realizacji' (jeśli istnieje) lub najnowszy wiersz (MAX(id)):
|
||||||
|
```sql
|
||||||
|
INSERT INTO order_status_pull_mappings (integration_id, shoppro_status_code, shoppro_status_name, orderpro_status_code)
|
||||||
|
SELECT osm.integration_id, osm.shoppro_status_code, osm.shoppro_status_name, osm.orderpro_status_code
|
||||||
|
FROM order_status_mappings osm
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT integration_id, shoppro_status_code, MAX(id) as max_id
|
||||||
|
FROM order_status_mappings
|
||||||
|
WHERE shoppro_status_code <> ''
|
||||||
|
GROUP BY integration_id, shoppro_status_code
|
||||||
|
) latest ON osm.id = latest.max_id
|
||||||
|
WHERE osm.shoppro_status_code <> '';
|
||||||
|
```
|
||||||
|
Uwaga: to da domyślne mapowanie bazujące na najnowszym wpisie. Użytkownik może potem skorygować w UI.
|
||||||
|
|
||||||
|
3. Utworzyć `ShopproPullStatusMappingRepository` z metodami:
|
||||||
|
- `listByIntegration(int $integrationId): array` — zwraca wiersze z pull tabeli
|
||||||
|
- `replaceForIntegration(int $integrationId, array $mappings): void` — DELETE + INSERT (analogicznie do ShopproStatusMappingRepository)
|
||||||
|
- Wzorować na istniejącym ShopproStatusMappingRepository ale z odwróconymi kolumnami logicznymi (klucz = shoppro_status_code)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Migracja SQL parsuje się bez błędów: `php -r "echo 'OK';"` (manualna weryfikacja składni)
|
||||||
|
- Repository ma metody listByIntegration i replaceForIntegration
|
||||||
|
- UNIQUE na (integration_id, shoppro_status_code) zapobiega duplikatom
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 satisfied: Tabela pull mappings z UNIQUE na shoppro side istnieje i ma repository</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: UI sekcja pull mapping + controller + routing</name>
|
||||||
|
<files>resources/views/settings/shoppro.php, src/Modules/Settings/ShopproIntegrationsController.php, routes/web.php, resources/lang/pl.php</files>
|
||||||
|
<action>
|
||||||
|
1. W `ShopproIntegrationsController::show()`:
|
||||||
|
- Wstrzyknąć `ShopproPullStatusMappingRepository` przez konstruktor
|
||||||
|
- Załadować pull mappings: `$pullMappingIndex` = index po shoppro_status_code
|
||||||
|
- Przekazać do widoku: `'pullMappingIndex' => $pullMappingIndex`
|
||||||
|
|
||||||
|
2. Dodać nową metodę `savePullStatusMappings(Request $request): Response`:
|
||||||
|
- Analogicznie do `saveStatusMappings()` ale odwrócona logika
|
||||||
|
- Odczytuje `shoppro_status_code[]`, `orderpro_status_code[]`, `shoppro_status_name[]`
|
||||||
|
- Waliduje: shoppro_status_code nie może się powtarzać (UNIQUE)
|
||||||
|
- Zapisuje przez `ShopproPullStatusMappingRepository::replaceForIntegration()`
|
||||||
|
- Flash success/error, redirect do tab statuses
|
||||||
|
|
||||||
|
3. W `routes/web.php`:
|
||||||
|
- Dodać: `POST /settings/integrations/shoppro/statuses/save-pull` → `savePullStatusMappings`
|
||||||
|
|
||||||
|
4. W `resources/views/settings/shoppro.php` — pod istniejącym formularzem push (po linii ~264):
|
||||||
|
- Dodać nagłówek sekcji: "Mapowanie przy imporcie (shopPRO → orderPRO)"
|
||||||
|
- Opis: "Określ, jaki status orderPRO ma otrzymać zamówienie importowane z danym statusem shopPRO."
|
||||||
|
- Formularz z tabelą: lewa kolumna = status shopPRO (stały tekst + hidden input), prawa = dropdown z orderPRO statusami
|
||||||
|
- Action: `/settings/integrations/shoppro/statuses/save-pull`
|
||||||
|
- Iterować po `$shopproStatuses` (te same co w dropdownach push)
|
||||||
|
- Dla każdego shopPRO statusu: dropdown z orderPRO statusami, pre-selected z `$pullMappingIndex`
|
||||||
|
- Przycisk "Zapisz mapowanie importu"
|
||||||
|
|
||||||
|
5. W `resources/lang/pl.php` dodać klucze tłumaczeń:
|
||||||
|
- `settings.order_statuses.pull.title` → 'Mapowanie przy imporcie (shopPRO → orderPRO)'
|
||||||
|
- `settings.order_statuses.pull.description` → 'Określ, jaki status orderPRO ma otrzymać zamówienie importowane z danym statusem shopPRO.'
|
||||||
|
- `settings.order_statuses.pull.save` → 'Zapisz mapowanie importu'
|
||||||
|
- `settings.order_statuses.pull.saved` → 'Mapowanie importu zapisane.'
|
||||||
|
- `settings.order_statuses.pull.save_failed` → 'Błąd zapisu mapowania importu.'
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- Strona /settings/integrations/shoppro?tab=statuses wyświetla dwie sekcje mapowania
|
||||||
|
- Formularz pull ma shopPRO statusy po lewej, dropdown orderPRO po prawej
|
||||||
|
- Zapis działa i dane trafiają do tabeli order_status_pull_mappings
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 satisfied: Sekcja pull mapping w UI z niezależnym zapisem</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: buildStatusMap() z pull tabeli + naprawienie etykiet sekcji push</name>
|
||||||
|
<files>src/Modules/Settings/ShopproOrdersSyncService.php, src/Modules/Settings/ShopproIntegrationsController.php</files>
|
||||||
|
<action>
|
||||||
|
1. W `ShopproOrdersSyncService`:
|
||||||
|
- Wstrzyknąć `ShopproPullStatusMappingRepository` przez konstruktor
|
||||||
|
- Zmienić `buildStatusMap()` aby korzystała z pull tabeli:
|
||||||
|
```php
|
||||||
|
private function buildStatusMap(int $integrationId): array
|
||||||
|
{
|
||||||
|
$rows = $this->pullStatusMappings->listByIntegration($integrationId);
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$shopCode = strtolower(trim((string) ($row['shoppro_status_code'] ?? '')));
|
||||||
|
$orderCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? '')));
|
||||||
|
if ($shopCode === '' || $orderCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$map[$shopCode] = $orderCode; // No "first wins" — UNIQUE constraint guarantees one row per shopCode
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Usunąć `if (!isset($map[$shopCode]))` guard — UNIQUE constraint na tabeli pull gwarantuje brak duplikatów
|
||||||
|
|
||||||
|
2. Zaktualizować `CronHandlerFactory` jeśli potrzebne — sprawdzić czy `ShopproOrdersSyncService` jest tam konstruowany i dodać nową zależność.
|
||||||
|
|
||||||
|
3. W istniejącej sekcji push w widoku: zmienić nagłówek sekcji z ogólnego "Mapowanie statusów" na "Wysyłka statusów (orderPRO → shopPRO)" żeby było jasne że to push direction.
|
||||||
|
Zaktualizować opis na: "Określ, jaki status shopPRO ma otrzymać zamówienie po zmianie statusu w orderPRO."
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- buildStatusMap() odczytuje z order_status_pull_mappings
|
||||||
|
- Przy pull mapping shopPRO "4" → "w_realizacji", import zamówienia ze statusem "4" daje external_status_id = "w_realizacji"
|
||||||
|
- Nie ma już "first wins" — każdy shopPRO code ma dokładnie jedno mapowanie
|
||||||
|
</verify>
|
||||||
|
<done>AC-3 satisfied: Import używa pull mappings z nowej tabeli. AC-4 będzie spełnione po następnym uruchomieniu crona.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- `order_status_mappings` — istniejąca tabela push mappings, nie modyfikować struktury
|
||||||
|
- `ShopproStatusMappingRepository` — istniejący repo push, nie modyfikować
|
||||||
|
- `ShopproStatusSyncService` (push direction) — nie zmieniać, push nadal korzysta ze starej tabeli
|
||||||
|
- `ShopproPaymentStatusSyncService` — nie zmieniać
|
||||||
|
- Allegro status mappings — nie dotyczy
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Nie naprawiamy ręcznie zamówień 211-214 — po deploy cron je zaktualizuje automatycznie
|
||||||
|
- Nie dodajemy pull mapping dla Allegro (osobna faza jeśli potrzebne)
|
||||||
|
- Nie zmieniamy logiki push mappings
|
||||||
|
- Nie dodajemy walidacji UI (JS) — duplikaty są blokowane przez UNIQUE constraint w DB
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Migracja SQL tworzy tabelę i pre-populuje dane
|
||||||
|
- [ ] Repository CRUD działa (list, replace)
|
||||||
|
- [ ] UI wyświetla dwie sekcje: push i pull
|
||||||
|
- [ ] Zapis pull mappings trafia do nowej tabeli
|
||||||
|
- [ ] buildStatusMap() czyta z pull tabeli
|
||||||
|
- [ ] Brak regresji w push direction (ShopproStatusSyncService niezmodyfikowany)
|
||||||
|
- [ ] Tłumaczenia kompletne
|
||||||
|
- [ ] DOCS zaktualizowane
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Nowa tabela order_status_pull_mappings istnieje i ma UNIQUE na (integration_id, shoppro_status_code)
|
||||||
|
- UI ma dwie odrębne sekcje mapowania (push i pull) z jasnymi etykietami kierunku
|
||||||
|
- Import zamówień z shopPRO korzysta wyłącznie z pull tabeli
|
||||||
|
- Zamówienia ze statusem shopPRO "4" trafiają jako "w_realizacji" (nie "do_odbioru")
|
||||||
|
- Push sync nie jest naruszony
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/75-pull-status-mapping/75-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
142
.paul/phases/75-pull-status-mapping/75-01-SUMMARY.md
Normal file
142
.paul/phases/75-pull-status-mapping/75-01-SUMMARY.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
phase: 75-pull-status-mapping
|
||||||
|
plan: 01
|
||||||
|
subsystem: settings, orders
|
||||||
|
tags: [status-mapping, shoppro, pull, import, payment-transition]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 74-reverse-status-mapping
|
||||||
|
provides: push mapping with UNIQUE on orderpro_status_code
|
||||||
|
provides:
|
||||||
|
- dedicated pull status mapping table (order_status_pull_mappings)
|
||||||
|
- pull mapping UI section in shopPRO integration settings
|
||||||
|
- status protection on re-import (only nieoplacone→w_realizacji transition allowed)
|
||||||
|
affects: [shoppro-import, status-sync, automation]
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [separate push/pull mapping tables, payment-transition guard on re-import]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- database/migrations/20260407_000079_pull_status_mappings.sql
|
||||||
|
- src/Modules/Settings/ShopproPullStatusMappingRepository.php
|
||||||
|
modified:
|
||||||
|
- src/Modules/Settings/ShopproIntegrationsController.php
|
||||||
|
- src/Modules/Settings/ShopproOrdersSyncService.php
|
||||||
|
- src/Modules/Orders/OrderImportRepository.php
|
||||||
|
- src/Modules/Cron/CronHandlerFactory.php
|
||||||
|
- resources/views/settings/shoppro.php
|
||||||
|
- resources/lang/pl.php
|
||||||
|
- routes/web.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Separate pull table instead of is_pull_target flag — cleaner separation of concerns"
|
||||||
|
- "Status protection on re-import: only nieoplacone+paid=2 triggers status change"
|
||||||
|
- "Fallback to push table if pull repo not injected — backward compatibility"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Pull mapping: UNIQUE on (integration_id, shoppro_status_code) — one orderPRO status per shopPRO code"
|
||||||
|
- "Re-import status guard: getCurrentStatus() check before updateOrder()"
|
||||||
|
|
||||||
|
duration: ~45min
|
||||||
|
started: 2026-04-07T12:00:00Z
|
||||||
|
completed: 2026-04-07T12:45:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 75 Plan 01: Pull Status Mapping — Summary
|
||||||
|
|
||||||
|
**Rozdzielenie mapowania statusow shopPRO na push/pull + ochrona statusu przy re-imporcie (tylko nieoplacone→w_realizacji przy potwierdzeniu platnosci)**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~45min |
|
||||||
|
| Tasks | 3 planned + 1 extension (status protection) |
|
||||||
|
| Files modified | 12 |
|
||||||
|
| DB migration | Executed on remote |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Nowa tabela pull mappings | Pass | Tabela utworzona z UNIQUE, 18 rows pre-populated |
|
||||||
|
| AC-2: Sekcja pull mapping w UI | Pass | Dwie sekcje: push ("Wysylka statusow") + pull ("Mapowanie przy imporcie") |
|
||||||
|
| AC-3: Import uzywa pull mappings | Pass | buildStatusMap() czyta z pull tabeli, fallback na push |
|
||||||
|
| AC-4: Naprawa zamowien 211-214 | Pass | Statusy juz poprawione (w_realizacji) |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Nowa tabela `order_status_pull_mappings` z UNIQUE na `(integration_id, shoppro_status_code)` — eliminuje bug "first wins" z Phase 74
|
||||||
|
- UI w Ustawienia > shopPRO > Statusy ma dwie sekcje: push (orderPRO→shopPRO) i pull (shopPRO→orderPRO) z jasnymi etykietami kierunku
|
||||||
|
- Ochrona statusu przy re-imporcie: `updateOrder()` nie nadpisuje `external_status_id` CHYBA ZE obecny status = `nieoplacone` i `payment_status = 2` (platnosc potwierdzona)
|
||||||
|
- Przy payment transition: importowane sa rowniez dane platnosci (`replacePayments`)
|
||||||
|
- Activity log rozroznia payment transition: "Platnosc potwierdzona z shopPRO — zmiana statusu na w realizacji"
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `database/migrations/20260407_000079_pull_status_mappings.sql` | Created | Nowa tabela + pre-populate z push mappings |
|
||||||
|
| `src/Modules/Settings/ShopproPullStatusMappingRepository.php` | Created | Repository CRUD dla pull mappings |
|
||||||
|
| `src/Modules/Settings/ShopproIntegrationsController.php` | Modified | Nowa zaleznosc, savePullStatusMappings(), buildPullMappingIndex() |
|
||||||
|
| `src/Modules/Settings/ShopproOrdersSyncService.php` | Modified | buildStatusMap() z pull tabeli, payment transition log |
|
||||||
|
| `src/Modules/Orders/OrderImportRepository.php` | Modified | Status protection + payment transition logic + getCurrentStatus() |
|
||||||
|
| `src/Modules/Cron/CronHandlerFactory.php` | Modified | Wstrzykniecie ShopproPullStatusMappingRepository |
|
||||||
|
| `resources/views/settings/shoppro.php` | Modified | Sekcja pull mapping, zmieniony tytul push |
|
||||||
|
| `resources/lang/pl.php` | Modified | Klucze pull.*, zmieniony tytul/opis push |
|
||||||
|
| `routes/web.php` | Modified | Nowa route POST .../statuses/save-pull |
|
||||||
|
| `DOCS/DB_SCHEMA.md` | Modified | Dokumentacja nowej tabeli |
|
||||||
|
| `DOCS/ARCHITECTURE.md` | Modified | Nowy endpoint save-pull |
|
||||||
|
| `DOCS/TECH_CHANGELOG.md` | Modified | Wpis Phase 75 |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Osobna tabela pull zamiast flagi is_pull_target | Czysta separacja push/pull, brak ryzyka kolizji | Dodatkowa tabela ale prosta logika |
|
||||||
|
| Status protection: tylko nieoplacone+paid=2 | User chce push-only z jednym wyjatkiem: platnosc | Re-import nie nadpisuje recznych zmian statusu |
|
||||||
|
| Fallback na push table jesli pull repo null | Backward compatibility dla kodu ktory nie wstrzykuje pull repo | Bezpieczna migracja |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Scope additions | 1 | Ochrona statusu przy re-imporcie (na zadanie usera) |
|
||||||
|
| Auto-fixed | 1 | Naprawa order 180 ze statusem "7" |
|
||||||
|
|
||||||
|
**Total impact:** Rozszerzenie scope o kluczowa logike biznesowa — status protection
|
||||||
|
|
||||||
|
### Scope Addition: Status Protection on Re-import
|
||||||
|
|
||||||
|
- **Requested by:** User during APPLY
|
||||||
|
- **Issue:** Re-import z shopPRO nadpisywal recznie zmienione statusy (np. order 180: "wyslane" → "7")
|
||||||
|
- **Fix:** `upsertOrderAggregate()` zachowuje current status chyba ze nieoplacone + payment confirmed
|
||||||
|
- **Files:** `src/Modules/Orders/OrderImportRepository.php`
|
||||||
|
|
||||||
|
### Auto-fixed: Order 180 status "7"
|
||||||
|
|
||||||
|
- **Found during:** Investigation
|
||||||
|
- **Issue:** Surowy kod shopPRO "7" (nieznany status) nadpisal "wyslane"
|
||||||
|
- **Fix:** Status juz poprawiony (wyslane). Nowa logika zapobiega powtorzeniu.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Pull mapping w pelni funkcjonalne i konfigurowalne
|
||||||
|
- Status protection chroni reczne zmiany w orderPRO
|
||||||
|
- Push sync (ShopproStatusSyncService) niezmodyfikowany — dziala jak dotychczas
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- shopPRO status "7" nie ma mapowania — user powinien zsynchronizowac statusy (przycisk w UI) lub dodac recznie
|
||||||
|
- Allegro mappings nie maja analogicznego pull — jesli potrzebne, osobna faza
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None — kod wymaga deploy na serwer (FTP)
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 75-pull-status-mapping, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
155
.paul/phases/76-shipment-receiver-fallback/76-01-PLAN.md
Normal file
155
.paul/phases/76-shipment-receiver-fallback/76-01-PLAN.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
phase: 76-shipment-receiver-fallback
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Shipments/ShipmentController.php
|
||||||
|
- src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Naprawic puste dane odbiorcy na stronie przygotowania przesylki (`/orders/{id}/shipment/prepare`) dla zamowien shopPRO, gdzie adres dostawy zawiera tylko nazwe metody dostawy bez danych adresowych.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Sprzedawca nie musi reczne przepisywac danych klienta do formularza przesylki — formularz automatycznie wypelnia sie danymi z adresu klienta gdy adres dostawy nie ma pelnych danych.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Poprawiona metoda `buildReceiverAddress` w ShipmentController z fallbackami na dane klienta
|
||||||
|
- Poprawiona metoda `buildDeliveryAddress` w ShopproOrderMapper — rozroznienie label metody dostawy od nazwy odbiorcy
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Shipments/ShipmentController.php
|
||||||
|
@src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
|
||||||
|
## Diagnostyka
|
||||||
|
Zamowienie 183 (shopPRO) — dane w `order_addresses`:
|
||||||
|
- customer: name="Paulina Smolinska", phone="505799865", email="paulinasmolinska2@wp.pl", street="Zamojskiego 80/46", city="Zuromin", zip="09-300"
|
||||||
|
- delivery: name="Kurier - przedplata: 0 zl", phone="505799865", email="...", street=NULL, city=NULL, zip=NULL
|
||||||
|
|
||||||
|
Problem: `buildReceiverAddress` uzywa delivery jako bazy, ale fallbacki dzialaja TYLKO dla name/phone/email — brak fallbacku na street/city/zip/country z customer address.
|
||||||
|
Dodatkowy problem: mapper zapisuje label metody dostawy jako `name` w delivery address.
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
No specialized flows configured
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Fallback danych adresowych na klienta
|
||||||
|
```gherkin
|
||||||
|
Given zamowienie shopPRO z adresem delivery bez street/city/zip
|
||||||
|
And adres customer ma pelne dane (name, street, city, zip)
|
||||||
|
When uzytkownik otwiera /orders/{id}/shipment/prepare
|
||||||
|
Then formularz "Adres odbiorcy" jest wypelniony danymi z adresu customer (imie, ulica, miasto, kod)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Delivery z pelnymi danymi — bez zmian
|
||||||
|
```gherkin
|
||||||
|
Given zamowienie z adresem delivery zawierajacym pelne dane (street, city, zip)
|
||||||
|
When uzytkownik otwiera /orders/{id}/shipment/prepare
|
||||||
|
Then formularz uzywa danych z adresu delivery (bez zmian wzgledem obecnego zachowania)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Nazwa odbiorcy — fallback na klienta gdy delivery name to label metody
|
||||||
|
```gherkin
|
||||||
|
Given zamowienie shopPRO z delivery name = "Kurier - przedplata: 0 zl"
|
||||||
|
And customer name = "Paulina Smolinska"
|
||||||
|
When uzytkownik otwiera /orders/{id}/shipment/prepare
|
||||||
|
Then pole "Imie i nazwisko" zawiera "Paulina Smolinska"
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Rozszerzyc fallbacki w buildReceiverAddress</name>
|
||||||
|
<files>src/Modules/Shipments/ShipmentController.php</files>
|
||||||
|
<action>
|
||||||
|
W metodzie `buildReceiverAddress` (linia 423) dodac fallbacki z customer na delivery dla pol adresowych:
|
||||||
|
- `street_name`: jezeli w delivery jest puste/null, uzyj z customer
|
||||||
|
- `street_number`: jezeli w delivery jest puste/null, uzyj z customer
|
||||||
|
- `city`: jezeli w delivery jest puste/null, uzyj z customer
|
||||||
|
- `zip_code`: jezeli w delivery jest puste/null, uzyj z customer
|
||||||
|
- `country`: jezeli w delivery jest puste/null, uzyj z customer
|
||||||
|
|
||||||
|
Dodatkowo zmienic warunek fallbacku `name`:
|
||||||
|
Obecny: fallback na customer name tylko gdy pickup point LUB delivery name jest puste.
|
||||||
|
Nowy: fallback na customer name rowniez gdy delivery name wyglada jak label metody dostawy
|
||||||
|
(nie ma adresu ulicy — tzn. jezeli delivery nie ma street_name, to name tez powinno byc z customer).
|
||||||
|
|
||||||
|
Logika: jezeli delivery nie ma street_name (jest null/pusty) i customer ma name, uzyj customer name.
|
||||||
|
To pokrywa zarowno przypadek pickup point, jak i "Kurier - przedplata" bez danych adresowych.
|
||||||
|
|
||||||
|
Wzorzec fallbacku (analogicznie do istniejacych phone/email):
|
||||||
|
```php
|
||||||
|
if (trim((string) ($result['street_name'] ?? '')) === '' && trim((string) ($customer['street_name'] ?? '')) !== '') {
|
||||||
|
$result['street_name'] = $customer['street_name'];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
I tak samo dla street_number, city, zip_code, country.
|
||||||
|
|
||||||
|
Dla name — zmienic warunek z:
|
||||||
|
```php
|
||||||
|
if (($this->isPickupPointDelivery($delivery) || $deliveryName === '') && $customerName !== '') {
|
||||||
|
```
|
||||||
|
na:
|
||||||
|
```php
|
||||||
|
$deliveryHasAddress = trim((string) ($delivery['street_name'] ?? '')) !== '';
|
||||||
|
if ((!$deliveryHasAddress || $this->isPickupPointDelivery($delivery) || $deliveryName === '') && $customerName !== '') {
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Sprawdzic ze zamowienie 183 na /orders/183/shipment/prepare wyswietla dane klienta w formularzu odbiorcy.
|
||||||
|
Sprawdzic ze zamowienia z pelnym adresem delivery nadal dzialaja poprawnie.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-2, AC-3 satisfied: formularz wypelnia sie danymi klienta gdy delivery nie ma danych adresowych</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- src/Modules/Orders/OrderImportRepository.php (logika importu)
|
||||||
|
- src/Modules/Orders/OrdersRepository.php (logika zapytan)
|
||||||
|
- resources/views/shipments/prepare.php (widok — formularz juz prawidlowo czyta z $receiver)
|
||||||
|
- database/migrations/* (schemat bazy)
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Nie zmieniamy sposobu importu adresow z shopPRO (ShopproOrderMapper::buildDeliveryAddress) — problem jest w prezentacji, nie w imporcie
|
||||||
|
- Nie zmieniamy struktury tabeli order_addresses
|
||||||
|
- Fix dotyczy tylko buildReceiverAddress w ShipmentController
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Zamowienie 183: formularz na /orders/183/shipment/prepare wypelnia sie danymi klienta
|
||||||
|
- [ ] Zamowienie z pelnym adresem delivery: formularz uzywa danych delivery
|
||||||
|
- [ ] Zamowienie z pickup point (paczkomat): formularz uzywa name klienta i adres punktu
|
||||||
|
- [ ] Brak bledow PHP
|
||||||
|
- [ ] Docs zaktualizowane (ARCHITECTURE.md, TECH_CHANGELOG.md)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Formularz odbiorcy na stronie przygotowania przesylki jest wypelniony danymi klienta gdy delivery nie ma adresu
|
||||||
|
- Istniejace zamowienia z pelnym adresem delivery dzialaja bez zmian
|
||||||
|
- Brak regresji w tworzeniu przesylek
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/76-shipment-receiver-fallback/76-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
112
.paul/phases/76-shipment-receiver-fallback/76-01-SUMMARY.md
Normal file
112
.paul/phases/76-shipment-receiver-fallback/76-01-SUMMARY.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
phase: 76-shipment-receiver-fallback
|
||||||
|
plan: 01
|
||||||
|
subsystem: shipments
|
||||||
|
tags: [address-fallback, shoppro, receiver-data]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: none
|
||||||
|
provides: none
|
||||||
|
provides:
|
||||||
|
- Fallback danych odbiorcy z customer na delivery w formularzu przesylki
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [address-field-fallback-loop]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- src/Modules/Shipments/ShipmentController.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Fix w buildReceiverAddress zamiast w mapperze — problem prezentacyjny, nie importowy"
|
||||||
|
- "Foreach loop zamiast osobnych if-ow — czystszy kod, latwiejsze rozszerzenie"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "buildReceiverAddress fallback: delivery -> customer dla wszystkich pol adresowych"
|
||||||
|
|
||||||
|
duration: 10min
|
||||||
|
started: 2026-04-07T00:00:00Z
|
||||||
|
completed: 2026-04-07T00:10:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 76 Plan 01: Shipment Receiver Fallback Summary
|
||||||
|
|
||||||
|
**Fallback danych odbiorcy z customer address gdy delivery address nie ma danych adresowych (ulica/miasto/kod)**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~10min |
|
||||||
|
| Tasks | 1 completed |
|
||||||
|
| Files modified | 3 (code + docs) |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Fallback danych adresowych na klienta | Pass | Loop fallback na 7 pol (phone, email, street_name, street_number, city, zip_code, country) |
|
||||||
|
| AC-2: Delivery z pelnymi danymi — bez zmian | Pass | Fallback uruchamia sie tylko gdy pole jest puste |
|
||||||
|
| AC-3: Nazwa odbiorcy — fallback gdy delivery nie ma ulicy | Pass | Warunek `!$deliveryHasAddress` pokrywa przypadek label metody dostawy |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Formularz odbiorcy na `/orders/{id}/shipment/prepare` automatycznie wypelnia sie danymi klienta gdy delivery nie ma danych adresowych
|
||||||
|
- Uproszczono kod — foreach loop zamiast powtarzajacych sie if-ow dla phone/email, rozszerzony o street/city/zip/country
|
||||||
|
- Dodano warunek name fallback: gdy delivery nie ma ulicy, name tez jest pobierane z customer (pokrywa "Kurier - przedplata" jako delivery name)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Shipments/ShipmentController.php` | Modified | Rozszerzono `buildReceiverAddress` o fallbacki pol adresowych z customer |
|
||||||
|
| `DOCS/ARCHITECTURE.md` | Modified | Opis nowej logiki fallbacku w sekcji przeplywu tworzenia przesylki |
|
||||||
|
| `DOCS/TECH_CHANGELOG.md` | Modified | Wpis Phase 76 z opisem problemu i rozwiazania |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Fix w buildReceiverAddress, nie w mapperze | Problem jest prezentacyjny — dane w DB sa poprawne (customer ma pelne dane), mapper robi co moze z danymi API shopPRO | Brak zmian w logice importu |
|
||||||
|
| Foreach loop zamiast osobnych if-ow | Uproszczenie kodu, latwiejsze dodanie nowych pol w przyszlosci | Zamieniono 2 osobne if-y na 1 loop pokrywajacy 7 pol |
|
||||||
|
| ShopproOrderMapper.php bez zmian | Plan przewidywal potencjalna modyfikacje, ale fix w kontrolerze wystarczyl | Mniej zmian, mniejsze ryzyko regresji importu |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Scope reduction | 1 | Pozytywny — mniej zmian |
|
||||||
|
|
||||||
|
**Total impact:** Mniejszy zakres niz planowany — ShopproOrderMapper nie wymaga zmian
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
**1. ShopproOrderMapper.php bez zmian**
|
||||||
|
- **Plan:** files_modified zawieral ShopproOrderMapper.php
|
||||||
|
- **Rzeczywistosc:** Fix w buildReceiverAddress wystarczyl, mapper nie wymaga modyfikacji
|
||||||
|
- **Impact:** Pozytywny — mniej kodu do zmiany, zero ryzyka regresji importu
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Formularz przesylki dziala poprawnie dla zamowien shopPRO z niekompletnymi adresami delivery
|
||||||
|
- Logika jest generyczna — dziala dla kazdego zrodla zamowien
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 76-shipment-receiver-fallback, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
157
.paul/phases/77-cod-amount-fix/77-01-PLAN.md
Normal file
157
.paul/phases/77-cod-amount-fix/77-01-PLAN.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
phase: 77-cod-amount-fix
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/Core/Support/StringHelper.php
|
||||||
|
- src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- src/Modules/Orders/OrdersController.php
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Naprawić automatyczne uzupełnianie kwoty pobrania (COD) przy generowaniu przesyłki dla zamówień spoza Allegro (shopPRO).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Zamówienia shopPRO z płatnością za pobraniem nie mają auto-wypełnionej kwoty COD w formularzu przesyłki, ponieważ pole `external_payment_type_id` zawiera wartość z shopPRO API (np. `"cod"`, `"pobranie"`), a nie Allegro-specyficzne `"CASH_ON_DELIVERY"`.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Centralna metoda `StringHelper::isCodPayment()` zastępuje wszystkie hardcoded sprawdzenia
|
||||||
|
- ShopproOrderMapper normalizuje typ płatności do `CASH_ON_DELIVERY` przy imporcie
|
||||||
|
- Kwota pobrania auto-wypełnia się poprawnie dla zamówień z każdego źródła
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Core/Support/StringHelper.php
|
||||||
|
@src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
@resources/views/shipments/prepare.php
|
||||||
|
@resources/views/orders/show.php
|
||||||
|
@src/Modules/Orders/OrdersController.php
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
No specialized flows configured — optional skills only.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: COD auto-fill dla zamówień shopPRO
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie shopPRO z payment_method "cod" lub "pobranie" lub "za pobraniem"
|
||||||
|
When użytkownik wchodzi na /orders/{id}/shipment/prepare
|
||||||
|
Then pole "Pobranie" zawiera kwotę total_with_tax zamówienia
|
||||||
|
And widoczny jest badge "ZA POBRANIEM"
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: COD auto-fill dla zamówień Allegro (regresja)
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie Allegro z external_payment_type_id "CASH_ON_DELIVERY"
|
||||||
|
When użytkownik wchodzi na /orders/{id}/shipment/prepare
|
||||||
|
Then pole "Pobranie" zawiera kwotę total_with_tax zamówienia (bez zmian w zachowaniu)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Detekcja COD w widoku szczegółów zamówienia
|
||||||
|
```gherkin
|
||||||
|
Given zamówienie z dowolnym wariantem nazwy COD
|
||||||
|
When użytkownik przegląda szczegóły zamówienia
|
||||||
|
Then badge "Za pobraniem" wyświetla się poprawnie
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Centralna metoda isCodPayment w StringHelper + normalizacja w ShopproOrderMapper</name>
|
||||||
|
<files>src/Core/Support/StringHelper.php, src/Modules/Settings/ShopproOrderMapper.php</files>
|
||||||
|
<action>
|
||||||
|
1. W `StringHelper` dodaj statyczną metodę `isCodPayment(string $value): bool`:
|
||||||
|
- Normalizuj wartość: `strtoupper(trim($value))`
|
||||||
|
- Sprawdź czy pasuje do znanego zbioru: `CASH_ON_DELIVERY`, `COD`, `POBRANIE`, `ZA POBRANIEM`
|
||||||
|
- Zwróć true/false
|
||||||
|
|
||||||
|
2. W `ShopproOrderMapper::mapOrderAggregate()` (linia 139):
|
||||||
|
- Po odczytaniu `payment_method` ze ścieżek `['payment_method', 'payment.method', 'payments.method']`
|
||||||
|
- Jeśli odczytana wartość jest rozpoznawana jako COD (przez `StringHelper::isCodPayment()`), znormalizuj na `'CASH_ON_DELIVERY'`
|
||||||
|
- Dzięki temu nowe importy będą miały ujednolicony format
|
||||||
|
|
||||||
|
Nie zmieniaj kontraktu ShopproOrderMapper dla pozostałych pól.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Przegląd kodu: metoda isCodPayment obsługuje min. 4 warianty; ShopproOrderMapper normalizuje COD.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 i AC-2 spełnione od strony danych — nowe importy mają ujednolicony format</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Zamiana hardcoded sprawdzeń na StringHelper::isCodPayment</name>
|
||||||
|
<files>resources/views/shipments/prepare.php, resources/views/orders/show.php, src/Modules/Orders/OrdersController.php</files>
|
||||||
|
<action>
|
||||||
|
1. `resources/views/shipments/prepare.php` linia 41:
|
||||||
|
- Zamień: `strtoupper(trim(...)) === 'CASH_ON_DELIVERY'`
|
||||||
|
- Na: `\App\Core\Support\StringHelper::isCodPayment((string) ($orderRow['external_payment_type_id'] ?? ''))`
|
||||||
|
|
||||||
|
2. `src/Modules/Orders/OrdersController.php` linia 343:
|
||||||
|
- Zamień: `$isCod = $paymentType === 'CASH_ON_DELIVERY'`
|
||||||
|
- Na: `$isCod = StringHelper::isCodPayment($paymentType)`
|
||||||
|
- Upewnij się że `use App\Core\Support\StringHelper;` jest w importach
|
||||||
|
|
||||||
|
3. `resources/views/orders/show.php`:
|
||||||
|
- Znajdź wszystkie porównania z `'CASH_ON_DELIVERY'` (linie ~222, 228, 578, 678)
|
||||||
|
- Zamień na `\App\Core\Support\StringHelper::isCodPayment(...)`
|
||||||
|
- Zachowaj mapę wyświetlania nazw (array z 'CASH_ON_DELIVERY' => 'Za pobraniem' itp.) — dodaj warianty COD do mapy
|
||||||
|
|
||||||
|
Nie zmieniaj logiki wyświetlania ani formatowania — tylko warunek detekcji COD.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Grep po codebase: brak hardcoded `=== 'CASH_ON_DELIVERY'` w plikach PHP poza testami i DOCS.
|
||||||
|
Widok shipment prepare: zamówienie shopPRO z pobraniem ma auto-wypełnioną kwotę.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-2, AC-3 spełnione — detekcja COD działa dla wszystkich źródeł zamówień</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- src/Modules/Settings/AllegroOrderImportService.php (Allegro import działa poprawnie)
|
||||||
|
- database/migrations/* (brak zmian schematu)
|
||||||
|
- src/Modules/Shipments/ShipmentController.php (logika tworzenia przesyłki nie wymaga zmian)
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Nie aktualizujemy istniejących danych w bazie (istniejące zamówienia shopPRO z "cod" zachowają starą wartość — helper obsłuży je w runtime)
|
||||||
|
- Nie dodajemy migracji
|
||||||
|
- Nie zmieniamy API ani endpointów
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `StringHelper::isCodPayment()` rozpoznaje: CASH_ON_DELIVERY, COD, POBRANIE, ZA POBRANIEM
|
||||||
|
- [ ] ShopproOrderMapper normalizuje COD na 'CASH_ON_DELIVERY' przy nowych importach
|
||||||
|
- [ ] Brak hardcoded `=== 'CASH_ON_DELIVERY'` w prepare.php, show.php, OrdersController.php
|
||||||
|
- [ ] Formularz przesyłki auto-wypełnia kwotę COD dla zamówień shopPRO
|
||||||
|
- [ ] Brak regresji: zamówienia Allegro nadal działają poprawnie
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Wszystkie 2 taski ukończone
|
||||||
|
- Weryfikacja przeszła pomyślnie
|
||||||
|
- Brak błędów PHP na stronach przygotowania przesyłki i szczegółów zamówienia
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/77-cod-amount-fix/77-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
127
.paul/phases/77-cod-amount-fix/77-01-SUMMARY.md
Normal file
127
.paul/phases/77-cod-amount-fix/77-01-SUMMARY.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
phase: 77-cod-amount-fix
|
||||||
|
plan: 01
|
||||||
|
subsystem: shipments
|
||||||
|
tags: [cod, payment, stringhelper, shoppro]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: none
|
||||||
|
provides: n/a
|
||||||
|
provides:
|
||||||
|
- StringHelper::isCodPayment() — centralna detekcja platnosci COD
|
||||||
|
- Normalizacja COD w ShopproOrderMapper przy imporcie
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [keyword-based COD detection via StringHelper]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- src/Core/Support/StringHelper.php
|
||||||
|
- src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- src/Modules/Orders/OrdersController.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Keyword matching zamiast exact match — shopPRO wysyla pelne polskie nazwy metod platnosci"
|
||||||
|
- "Dwupoziomowa detekcja: exact match (COD_PAYMENT_TYPES) + keyword search (COD_PAYMENT_KEYWORDS)"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "StringHelper::isCodPayment() jako jedyne miejsce detekcji COD w codebase"
|
||||||
|
|
||||||
|
duration: 15min
|
||||||
|
started: 2026-04-07T00:00:00Z
|
||||||
|
completed: 2026-04-07T00:15:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 77 Plan 01: COD Amount Fix Summary
|
||||||
|
|
||||||
|
**Centralna detekcja platnosci COD (StringHelper::isCodPayment) z keyword matching dla shopPRO wartosci typu "Platnosc przy odbiorze"**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~15min |
|
||||||
|
| Started | 2026-04-07 |
|
||||||
|
| Completed | 2026-04-07 |
|
||||||
|
| Tasks | 2 completed |
|
||||||
|
| Files modified | 5 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: COD auto-fill dla zamowien shopPRO | Pass | Testowane na zamowieniach 188/207 z wartoscia "Platnosc przy odbiorze" |
|
||||||
|
| AC-2: COD auto-fill dla zamowien Allegro (regresja) | Pass | CASH_ON_DELIVERY nadal rozpoznawane przez exact match |
|
||||||
|
| AC-3: Detekcja COD w widoku szczegulow zamowienia | Pass | Badge "Za pobraniem" wyswietla sie dla wszystkich wariantow COD |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Centralna metoda `StringHelper::isCodPayment()` z dwupoziomowa detekcja: exact match na 4 warianty + keyword match na 3 frazy (PRZY ODBIORZE, POBRANIEM, POBRANIE)
|
||||||
|
- Normalizacja COD na `CASH_ON_DELIVERY` w `ShopproOrderMapper` przy nowych importach
|
||||||
|
- Eliminacja wszystkich hardcoded `=== 'CASH_ON_DELIVERY'` z kodu produkcyjnego (3 pliki)
|
||||||
|
- Rozszerzenie map etykiet platnosci w `show.php` o warianty COD
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Core/Support/StringHelper.php` | Modified | Dodano COD_PAYMENT_TYPES, COD_PAYMENT_KEYWORDS, isCodPayment() |
|
||||||
|
| `src/Modules/Settings/ShopproOrderMapper.php` | Modified | normalizeCodPaymentType() + uzycie w mapOrderAggregate |
|
||||||
|
| `resources/views/shipments/prepare.php` | Modified | Zamiana hardcoded na StringHelper::isCodPayment() |
|
||||||
|
| `resources/views/orders/show.php` | Modified | Zamiana 2 sprawdzen + rozszerzenie map etykiet o warianty COD |
|
||||||
|
| `src/Modules/Orders/OrdersController.php` | Modified | Zamiana hardcoded na StringHelper::isCodPayment() |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Keyword matching zamiast samego exact match | shopPRO wysyla pelne polskie nazwy np. "Platnosc przy odbiorze" — nie da sie przewidziec wszystkich wariantow | Odpornosc na nowe warianty nazw COD |
|
||||||
|
| Dwupoziomowa detekcja (exact + keyword) | Exact match jest szybszy i pewniejszy dla znanych wartosci, keyword jako fallback | Brak false positives dla ONLINE/TRANSFER, elastycznosc dla COD |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | Kluczowe — bez tego fix nie dzialal |
|
||||||
|
| Scope additions | 0 | - |
|
||||||
|
| Deferred | 0 | - |
|
||||||
|
|
||||||
|
**Total impact:** Konieczne rozszerzenie — plan zakladal 4 warianty exact match, rzeczywistosc wymagala keyword matching.
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. Wartosc shopPRO to "Platnosc przy odbiorze" — nie ma w liscie exact match**
|
||||||
|
- **Found during:** Weryfikacja po APPLY (user report)
|
||||||
|
- **Issue:** Plan zakladal warianty "cod", "pobranie", "za pobraniem". Rzeczywista wartosc w DB to "Platnosc przy odbiorze"
|
||||||
|
- **Fix:** Dodano COD_PAYMENT_KEYWORDS z keyword matching (str_contains) jako drugi poziom detekcji
|
||||||
|
- **Files:** src/Core/Support/StringHelper.php, resources/views/orders/show.php
|
||||||
|
- **Verification:** 9/9 unit testow przeszlo, w tym "Platnosc przy odbiorze" => true
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
| Issue | Resolution |
|
||||||
|
|-------|------------|
|
||||||
|
| Poczatkowy fix nie dzialal — shopPRO wysyla "Platnosc przy odbiorze" nie "cod" | Sprawdzenie DB (orders WHERE id IN (188,207)), dodanie keyword matching |
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- StringHelper::isCodPayment() gotowy do uzycia w kolejnych miejscach
|
||||||
|
- Nowe importy shopPRO beda mialy znormalizowane CASH_ON_DELIVERY
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Istniejace zamowienia w DB zachowuja oryginalna wartosc "Platnosc przy odbiorze" — helper obsluguje je w runtime, ale dane nie sa znormalizowane
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 77-cod-amount-fix, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
109
.paul/phases/78-preset-auto-submit/78-01-PLAN.md
Normal file
109
.paul/phases/78-preset-auto-submit/78-01-PLAN.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
phase: 78-preset-auto-submit
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Presety przesyłek po autofill automatycznie submitują formularz (kliknięcie "Utwórz przesyłkę").
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Aktualnie użytkownik musi kliknąć preset, a potem ręcznie kliknąć "Utwórz przesyłkę". Skoro preset wypełnia wszystkie dane — submit powinien nastąpić automatycznie, oszczędzając jedno kliknięcie.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Funkcja `applyPreset()` po wypełnieniu pól automatycznie submituje formularz
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@resources/views/shipments/prepare.php (linie 957-993: applyPreset + setTimeout 200ms)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
No specialized flows configured — optional skills only.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Auto-submit po kliknięciu presetu
|
||||||
|
```gherkin
|
||||||
|
Given formularz przygotowania przesyłki z co najmniej jednym presetem
|
||||||
|
When użytkownik klika przycisk presetu
|
||||||
|
Then formularz wypełnia się danymi presetu
|
||||||
|
And formularz automatycznie się submituje (tworzenie przesyłki)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Brak regresji autofill
|
||||||
|
```gherkin
|
||||||
|
Given preset z zapisanymi danymi (carrier, wymiary, metoda dostawy)
|
||||||
|
When preset jest aplikowany
|
||||||
|
Then wszystkie pola formularza są poprawnie wypełnione przed submitem
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Dodanie auto-submit po applyPreset</name>
|
||||||
|
<files>resources/views/shipments/prepare.php</files>
|
||||||
|
<action>
|
||||||
|
W funkcji `applyPreset()` (linia ~957), wewnątrz istniejącego `setTimeout` (200ms) na końcu callbacku (po `selectDeliveryService(preset)`):
|
||||||
|
|
||||||
|
1. Dodaj drugi `setTimeout` (krótki, np. 100ms) po zakończeniu autofill, który:
|
||||||
|
- Znajduje formularz: `document.getElementById('shipment-form')` lub `document.querySelector('form[action*="shipment/create"]')`
|
||||||
|
- Wywołuje `form.submit()` lub klika przycisk submit
|
||||||
|
|
||||||
|
2. Formularz może nie mieć id — sprawdź czy `<form>` ma id. Jeśli nie, dodaj `id="shipment-form"` do tagu `<form>`.
|
||||||
|
|
||||||
|
Uwaga: `selectDeliveryService()` może mieć swój setTimeout — sprawdź czy submit nie nastąpi przed zakończeniem selekcji. Użyj wystarczającego opóźnienia (np. łącznie 400-500ms od kliknięcia presetu).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Przegląd kodu: po applyPreset formularz submituje się automatycznie.
|
||||||
|
PHP lint: brak błędów składni.
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 i AC-2 spełnione — preset autofill + auto-submit</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- Logika autofill presetu (kolejność wypełniania pól, setTimeout 200ms)
|
||||||
|
- Backend ShipmentController (logika tworzenia przesyłki)
|
||||||
|
- API presetów (/api/shipment-presets/*)
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Tylko auto-submit po kliknięciu presetu
|
||||||
|
- Nie zmieniamy zachowania przycisku "Utwórz przesyłkę" ani ręcznego formularza
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Kliknięcie presetu wypełnia formularz i submituje go
|
||||||
|
- [ ] Formularz ma id umożliwiające łatwe znalezienie w JS
|
||||||
|
- [ ] Brak błędów PHP syntax
|
||||||
|
- [ ] Ręczne wypełnienie formularza i kliknięcie "Utwórz przesyłkę" nadal działa
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Task 1 ukończony
|
||||||
|
- Weryfikacja przeszła
|
||||||
|
- Brak błędów PHP
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/78-preset-auto-submit/78-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
91
.paul/phases/78-preset-auto-submit/78-01-SUMMARY.md
Normal file
91
.paul/phases/78-preset-auto-submit/78-01-SUMMARY.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
phase: 78-preset-auto-submit
|
||||||
|
plan: 01
|
||||||
|
subsystem: shipments
|
||||||
|
tags: [presets, autofill, auto-submit, ux]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 23-shipment-presets-backend
|
||||||
|
provides: preset API i autofill
|
||||||
|
provides:
|
||||||
|
- Auto-submit formularza po kliknieciu presetu przesylki
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: []
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified:
|
||||||
|
- resources/views/shipments/prepare.php
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "500ms laczny delay (200ms autofill + 300ms submit) — wystarczajacy na selectDeliveryService"
|
||||||
|
|
||||||
|
patterns-established: []
|
||||||
|
|
||||||
|
duration: 5min
|
||||||
|
started: 2026-04-07T00:00:00Z
|
||||||
|
completed: 2026-04-07T00:05:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 78 Plan 01: Preset Auto Submit Summary
|
||||||
|
|
||||||
|
**Presety przesylek automatycznie submituja formularz po autofill — jedno klikniecie zamiast dwoch**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~5min |
|
||||||
|
| Started | 2026-04-07 |
|
||||||
|
| Completed | 2026-04-07 |
|
||||||
|
| Tasks | 1 completed |
|
||||||
|
| Files modified | 1 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Auto-submit po kliknieciu presetu | Pass | form.submit() po 500ms od klikniecia |
|
||||||
|
| AC-2: Brak regresji autofill | Pass | Autofill bez zmian, submit nastepuje po nim |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Dodano `id="shipment-form"` na formularz tworzenia przesylki
|
||||||
|
- `applyPreset()` po autofill (200ms) czeka 300ms i wywoluje `form.submit()`
|
||||||
|
- Jedno klikniecie presetu = wypelnienie + utworzenie przesylki
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `resources/views/shipments/prepare.php` | Modified | id na formularzu + auto-submit w applyPreset() |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
None — plan wykonany zgodnie ze specyfikacja.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Preset flow kompletny: klik → autofill → submit
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 78-preset-auto-submit, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
167
.paul/phases/79-personalization-message-field/79-01-PLAN.md
Normal file
167
.paul/phases/79-personalization-message-field/79-01-PLAN.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
phase: 79-personalization-message-field
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Dodanie pola `message` z API shopPRO do personalizacji produktow w zamowieniach. Aktualnie `extractPersonalization()` sprawdza tylko `attributes` i `custom_fields`, a shopPRO zwraca rowniez pole `message` z wiadomoscia personalizacji klienta (np. "Milenie na pamiatke I Komunii Swietej").
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Klienci wpisuja wiadomosci personalizacji przy zamowieniach w shopPRO. Te dane sa kluczowe dla realizacji zamowien (np. grawerunki, dedykacje). Bez ich importu pracownik musi reczne sprawdzac dane w shopPRO.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Zaktualizowany `extractPersonalization()` w ShopproOrderMapper — obsluguje pole `message`
|
||||||
|
- Istniejace zamowienia z `message` w payload_json — backfill personalizacji
|
||||||
|
- Zaktualizowana dokumentacja
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Settings/ShopproOrderMapper.php (metoda extractPersonalization, linia ~590)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
No specialized flows required for this plan.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Pole message importowane do personalizacji
|
||||||
|
```gherkin
|
||||||
|
Given zamowienie shopPRO z pozycja majaca pole "message" w odpowiedzi API
|
||||||
|
When pozycja jest importowana/aktualizowana przez ShopproOrderMapper
|
||||||
|
Then wartosc pola "message" jest zapisana w kolumnie personalization tabeli order_items
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Laczenie message z attributes i custom_fields
|
||||||
|
```gherkin
|
||||||
|
Given pozycja shopPRO majaca zarowno "attributes" jak i "message"
|
||||||
|
When extractPersonalization przetwarza dane
|
||||||
|
Then oba pola sa polaczone w personalization oddzielone nowa linia
|
||||||
|
And pole "message" jest poprzedzone etykieta "Wiadomosc:"
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Backfill istniejacych zamowien
|
||||||
|
```gherkin
|
||||||
|
Given istniejace pozycje zamowien z polem "message" w payload_json ale pustym personalization
|
||||||
|
When uruchomiona jest migracja/skrypt backfill
|
||||||
|
Then kolumna personalization zostaje wypelniona danymi z payload_json.message
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Dodanie pola message do extractPersonalization</name>
|
||||||
|
<files>src/Modules/Settings/ShopproOrderMapper.php</files>
|
||||||
|
<action>
|
||||||
|
W metodzie `extractPersonalization()` (linia ~590):
|
||||||
|
1. Dodac pole `message` do listy sprawdzanych pol OSOBNO po petli attributes/custom_fields
|
||||||
|
2. Wartosc message powinna byc poprzedzona etykieta "Wiadomosc: " (z dwukropkiem i spacja)
|
||||||
|
3. Zachowac istniejaca logike czyszczenia HTML (strip_tags, html_entity_decode, trim)
|
||||||
|
4. Jesli message jest jedynym polem — zwrocic "Wiadomosc: {tresc}"
|
||||||
|
5. Jesli sa tez attributes/custom_fields — dodac message na koncu po nowej linii
|
||||||
|
|
||||||
|
Logika:
|
||||||
|
```
|
||||||
|
// Po istniejącej pętli attributes/custom_fields:
|
||||||
|
$message = $this->readPath($row, ['message']);
|
||||||
|
if ($message !== null && $message !== '' && $message !== false) {
|
||||||
|
$text = str_replace(['<br>', '<br/>', '<br />'], "\n", (string) $message);
|
||||||
|
$text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
$text = trim($text);
|
||||||
|
if ($text !== '') {
|
||||||
|
$parts[] = 'Wiadomość: ' . $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Sprawdzic w kodzie ze extractPersonalization obsluguje 3 pola: attributes, custom_fields, message.
|
||||||
|
Zweryfikowac ze message jest poprzedzony etykieta "Wiadomosc:".
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 i AC-2 satisfied: pole message jest importowane do personalizacji z etykieta</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Migracja backfill personalizacji z payload_json</name>
|
||||||
|
<files>database/migrations/20260407_000080_backfill_personalization_message.sql</files>
|
||||||
|
<action>
|
||||||
|
Utworzyc migracje SQL ktora:
|
||||||
|
1. Aktualizuje kolumne personalization dla pozycji majacych message w payload_json
|
||||||
|
2. Warunek: personalization IS NULL AND payload_json zawiera niepuste pole message
|
||||||
|
3. Uzyc JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.message')) do wyciagniecia wartosci
|
||||||
|
4. Ustawic personalization = CONCAT('Wiadomość: ', extracted_message)
|
||||||
|
5. Jesli personalization juz istnieje (nie NULL) — nie nadpisywac (dodac do WHERE)
|
||||||
|
|
||||||
|
Uwaga: Jezeli pozycja ma tez attributes/custom_fields w payload_json, sam SQL nie zbuduje pelnej personalizacji.
|
||||||
|
Dla prostoty: backfill dotyczy TYLKO pozycji z pustym personalization.
|
||||||
|
Pozycje z istniejacym personalization (z attributes/custom_fields) i brakujacym message — pomijamy
|
||||||
|
(przyszly re-import uzupelni je poprawnie dzieki Task 1).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Uruchomic migracje na bazie i sprawdzic ze pozycje zamowienia #217 maja wypelniona personalizacje z polem Wiadomosc.
|
||||||
|
</verify>
|
||||||
|
<done>AC-3 satisfied: istniejace zamowienia maja uzupelniona personalizacje</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Aktualizacja dokumentacji</name>
|
||||||
|
<files>DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md</files>
|
||||||
|
<action>
|
||||||
|
1. W ARCHITECTURE.md — zaktualizowac opis ShopproOrderMapper::extractPersonalization o pole message
|
||||||
|
2. W TECH_CHANGELOG.md — dodac wpis o rozszerzeniu importu personalizacji o pole message
|
||||||
|
</action>
|
||||||
|
<verify>Sprawdzic ze dokumenty sa aktualne</verify>
|
||||||
|
<done>Dokumentacja zaktualizowana</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- Logika importu zamowien (OrderImportRepository) — zmiana tylko w mapperze
|
||||||
|
- Widok show.php — juz obsluguje personalizacje (nl2br), nie wymaga zmian
|
||||||
|
- Struktura tabeli order_items — kolumna personalization juz istnieje
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Nie zmieniamy sposobu wyswietlania personalizacji w widoku (juz dziala)
|
||||||
|
- Nie dodajemy nowych kolumn do bazy
|
||||||
|
- Backfill tylko dla pozycji z pustym personalization (nie nadpisujemy istniejacych)
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] extractPersonalization obsluguje pola: attributes, custom_fields, message
|
||||||
|
- [ ] Pole message jest poprzedzone etykieta "Wiadomosc:"
|
||||||
|
- [ ] Migracja backfill wykonana pomyslnie
|
||||||
|
- [ ] Zamowienie #217 wyswietla personalizacje z wiadomosciami
|
||||||
|
- [ ] Dokumentacja zaktualizowana
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Nowe zamowienia shopPRO z polem message importuja personalizacje
|
||||||
|
- Istniejace zamowienia z message w payload_json maja uzupelniona personalizacje
|
||||||
|
- Brak regresji w imporcie zamowien
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/79-personalization-message-field/79-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
117
.paul/phases/79-personalization-message-field/79-01-SUMMARY.md
Normal file
117
.paul/phases/79-personalization-message-field/79-01-SUMMARY.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
phase: 79-personalization-message-field
|
||||||
|
plan: 01
|
||||||
|
subsystem: api
|
||||||
|
tags: [shoppro, import, personalization, order-notes]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 63-order-item-personalization
|
||||||
|
provides: extractPersonalization z attributes/custom_fields
|
||||||
|
provides:
|
||||||
|
- import pola message z API shopPRO do personalizacji pozycji zamowien
|
||||||
|
- import pola message z API shopPRO do notatek zamowienia (order_notes)
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: []
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- database/migrations/20260407_000080_backfill_personalization_message.sql
|
||||||
|
modified:
|
||||||
|
- src/Modules/Settings/ShopproOrderMapper.php
|
||||||
|
- resources/views/orders/show.php
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Pole message na pozycji poprzedzone etykieta 'Wiadomosc:' dla odroznienia od attributes/custom_fields"
|
||||||
|
- "Pole message na poziomie zamowienia importowane do order_notes jako note_type=message"
|
||||||
|
- "Usunieto etykiete 'Personalizacja:' z widoku — kolor tla wystarczajacy"
|
||||||
|
|
||||||
|
patterns-established: []
|
||||||
|
|
||||||
|
duration: ~15min
|
||||||
|
started: 2026-04-07T00:00:00Z
|
||||||
|
completed: 2026-04-07T00:15:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 79 Plan 01: Personalization Message Field Summary
|
||||||
|
|
||||||
|
**Import pola `message` z API shopPRO do personalizacji pozycji i notatek zamowienia + backfill istniejacych danych**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~15min |
|
||||||
|
| Tasks | 3 completed (plan) + 2 deviations |
|
||||||
|
| Files modified | 5 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Pole message importowane do personalizacji | Pass | extractPersonalization sprawdza attributes, custom_fields, message |
|
||||||
|
| AC-2: Laczenie message z attributes i custom_fields | Pass | message poprzedzony etykieta "Wiadomosc:" |
|
||||||
|
| AC-3: Backfill istniejacych zamowien | Pass | 21 pozycji + 70 notatek zamowien |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- `extractPersonalization()` rozszerzony o pole `message` z etykieta "Wiadomosc:"
|
||||||
|
- `mapNotes()` rozszerzony o pole `message` na poziomie zamowienia (wiadomosc klienta do sprzedawcy)
|
||||||
|
- Backfill: 21 pozycji zamowien uzupelnionych o personalizacje, 70 zamowien uzupelnionych o notatki
|
||||||
|
- Usunieto zbedna etykiete "Personalizacja:" z widoku zamowienia
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Settings/ShopproOrderMapper.php` | Modified | Dodano pole message do extractPersonalization + mapNotes |
|
||||||
|
| `database/migrations/20260407_000080_backfill_personalization_message.sql` | Created | Backfill personalizacji i notatek z payload_json |
|
||||||
|
| `resources/views/orders/show.php` | Modified | Usunieto etykiete "Personalizacja:" |
|
||||||
|
| `DOCS/ARCHITECTURE.md` | Modified | Opis extractPersonalization z 3 polami |
|
||||||
|
| `DOCS/TECH_CHANGELOG.md` | Modified | Wpis Phase 79 |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Scope additions | 2 | Niezbedne uzupelnienia wykryte podczas UAT |
|
||||||
|
|
||||||
|
**Total impact:** Wiadomosc klienta importowana zarowno na poziomie pozycji jak i zamowienia.
|
||||||
|
|
||||||
|
### Scope Additions
|
||||||
|
|
||||||
|
**1. Import wiadomosci klienta do order_notes**
|
||||||
|
- **Found during:** UAT zamowienia #218
|
||||||
|
- **Issue:** Pole `message` na poziomie zamowienia (wiadomosc klienta do sprzedawcy) nie bylo importowane do sekcji "Wiadomosci i zalaczniki"
|
||||||
|
- **Fix:** Dodano `'message'` do listy kluczy w `mapNotes()` + backfill 70 zamowien
|
||||||
|
- **Files:** `src/Modules/Settings/ShopproOrderMapper.php`, migracja SQL
|
||||||
|
|
||||||
|
**2. Usuniecie etykiety "Personalizacja:"**
|
||||||
|
- **Found during:** UAT
|
||||||
|
- **Issue:** Etykieta zbedna — kolor tla wystarczajaco oznacza sekcje personalizacji
|
||||||
|
- **Fix:** Usunieto `<span class="item-personalization__label">` z widoku
|
||||||
|
- **Files:** `resources/views/orders/show.php`
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Import shopPRO kompletny — wszystkie pola personalizacji i wiadomosci klienta sa importowane
|
||||||
|
- Backfill wykonany na produkcji
|
||||||
|
|
||||||
|
**Concerns:** None
|
||||||
|
|
||||||
|
**Blockers:** None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 79-personalization-message-field, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
106
.paul/phases/80-status-change-reload/80-01-PLAN.md
Normal file
106
.paul/phases/80-status-change-reload/80-01-PLAN.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
phase: 80-status-change-reload
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified: [public/assets/js/modules/inline-status-change.js]
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Po zmianie statusu zamowienia inline na liscie zamowien (/orders/list), strona przeladowuje sie automatycznie, dzieki czemu zamowienie znika z aktualnego widoku filtrowanego po statusie.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Gdy uzytkownik filtruje zamowienia po statusie i zmienia status jednego z nich, zamowienie powinno zniknac z listy (bo juz nie pasuje do filtra). Obecnie badge aktualizuje sie w miejscu, ale zamowienie pozostaje na liscie co jest mylace.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
Zmodyfikowany `public/assets/js/modules/inline-status-change.js` z przeladowaniem strony po udanej zmianie statusu.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@public/assets/js/modules/inline-status-change.js
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Przeladowanie listy po zmianie statusu
|
||||||
|
```gherkin
|
||||||
|
Given uzytkownik jest na liscie zamowien z aktywnym filtrem statusu
|
||||||
|
When zmienia status zamowienia przez inline dropdown
|
||||||
|
Then strona przeladowuje sie po udanej odpowiedzi serwera
|
||||||
|
And zamowienie z nowym statusem nie pojawia sie na liscie (bo filtr go wyklucza)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Brak przeladowania przy bledzie
|
||||||
|
```gherkin
|
||||||
|
Given uzytkownik zmienia status zamowienia inline
|
||||||
|
When serwer zwraca blad (np. 500 lub validation error)
|
||||||
|
Then strona NIE przeladowuje sie
|
||||||
|
And badge wraca do poprzedniego statusu
|
||||||
|
And wyswietla sie komunikat bledu
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Dodanie location.reload() po udanej zmianie statusu</name>
|
||||||
|
<files>public/assets/js/modules/inline-status-change.js</files>
|
||||||
|
<action>
|
||||||
|
W funkcji `changeStatus()`, w bloku `.then(function (result) {...})`:
|
||||||
|
- Po linii 153-154 (po udanej aktualizacji badge'a), dodac `location.reload()`.
|
||||||
|
- Reload powinien nastapic TYLKO gdy `result.ok && result.data.success`.
|
||||||
|
- Blok error (linie 142-149) i `.catch()` (linie 156-162) pozostaja bez zmian — brak reloadu przy bledzie.
|
||||||
|
- Mozna usunac aktualizacje badge'a (linie 152-154) bo reload i tak odswieza strone, ale lepiej zostawic dla plynnosci UX — uzytkownik widzi natychmiastowa zmiane badge'a, potem reload.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. Otworz /orders/list z filtrem statusu (np. "Nowe")
|
||||||
|
2. Zmien status zamowienia na inny (np. "W realizacji")
|
||||||
|
3. Strona przeladowuje sie i zamowienie znika z listy
|
||||||
|
4. Zmien status bez filtra — strona tez sie przeladowuje (zamowienie pojawia sie z nowym statusem)
|
||||||
|
</verify>
|
||||||
|
<done>AC-1 i AC-2 spelnione: reload po sukcesie, brak reloadu przy bledzie</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- src/Modules/Orders/OrdersController.php (backend endpoint dziala poprawnie)
|
||||||
|
- resources/views/orders/list.php (konfiguracja JS jest OK)
|
||||||
|
- routes/web.php
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Nie dodawac AJAX-owego odswiezania listy (pelny reload jest prostszy i wystarczajacy)
|
||||||
|
- Nie zmieniac logiki dropdowna ani budowania badge'y
|
||||||
|
- Nie zmieniac obslugi bledow
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Po zmianie statusu z filtrem — zamowienie znika z listy
|
||||||
|
- [ ] Po zmianie statusu bez filtra — zamowienie ma nowy status po reload
|
||||||
|
- [ ] Przy bledzie serwera — brak reloadu, badge wraca, komunikat bledu
|
||||||
|
- [ ] Dropdown dziala jak wczesniej (otwieranie, zamykanie, Escape)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Task 1 ukonczony
|
||||||
|
- Wszystkie verification checks przechodzą
|
||||||
|
- Brak regresji w inline status change
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/80-status-change-reload/80-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
81
.paul/phases/80-status-change-reload/80-01-SUMMARY.md
Normal file
81
.paul/phases/80-status-change-reload/80-01-SUMMARY.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
phase: 80-status-change-reload
|
||||||
|
plan: 01
|
||||||
|
subsystem: ui
|
||||||
|
tags: [javascript, ajax, orders-list, inline-status]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 44-inline-status-change
|
||||||
|
provides: inline status change dropdown on orders list
|
||||||
|
provides:
|
||||||
|
- page reload after successful inline status change
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: []
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified: [public/assets/js/modules/inline-status-change.js]
|
||||||
|
|
||||||
|
key-decisions: []
|
||||||
|
|
||||||
|
patterns-established: []
|
||||||
|
|
||||||
|
duration: 2min
|
||||||
|
started: 2026-04-07T00:00:00Z
|
||||||
|
completed: 2026-04-07T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 80 Plan 01: Status Change Reload Summary
|
||||||
|
|
||||||
|
**Dodanie `location.reload()` po udanej zmianie statusu inline na liscie zamowien — zamowienie znika z filtrowanego widoku.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | 2min |
|
||||||
|
| Tasks | 1 completed |
|
||||||
|
| Files modified | 1 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Przeladowanie listy po zmianie statusu | Pass | `location.reload()` po sukcesie AJAX |
|
||||||
|
| AC-2: Brak przeladowania przy bledzie | Pass | Bloki error/catch bez zmian — revert badge + alert |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Dodano `location.reload()` w `changeStatus()` po udanej odpowiedzi serwera (1 linia)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `public/assets/js/modules/inline-status-change.js` | Modified | Dodano reload po udanej zmianie statusu |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Inline status change z reloadem dziala poprawnie
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 80-status-change-reload, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
207
.paul/phases/81-global-search-bar/81-01-PLAN.md
Normal file
207
.paul/phases/81-global-search-bar/81-01-PLAN.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
---
|
||||||
|
phase: 81-global-search-bar
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified: [src/Modules/Orders/OrdersRepository.php, src/Modules/Orders/OrdersController.php, routes/web.php, resources/views/layouts/app.php, public/assets/js/modules/global-search.js, resources/scss/components/_global-search.scss, resources/scss/app.scss]
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Globalna wyszukiwarka zamowien widoczna w topbarze na kazdej stronie orderPRO. Wyszukuje po: numerze zamowienia, nazwisku klienta, e-mailu, telefonie, nazwie produktu. Wyniki pojawiaja sie jako dropdown pod polem wyszukiwania. Klikniecie wyniku przenosi do /orders/{id}.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Szybki dostep do zamowienia z dowolnego miejsca w aplikacji — bez koniecznosci przechodzenia na liste zamowien i ustawiania filtrow. Analogicznie do wyszukiwarki w shopPRO.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Nowy endpoint GET /api/orders/search?q=...&limit=10
|
||||||
|
- Pole wyszukiwania w topbarze (layout app.php)
|
||||||
|
- Modul JS global-search.js (debounced AJAX, dropdown wynikow, nawigacja klawiaturowa)
|
||||||
|
- Style SCSS dla komponentu wyszukiwarki
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Orders/OrdersRepository.php (linie 91-106 — istniejacy search SQL)
|
||||||
|
@src/Modules/Orders/OrdersController.php (index() linie 44-71)
|
||||||
|
@routes/web.php (linie 408-413 — order routes)
|
||||||
|
@resources/views/layouts/app.php (linie 121-136 — topbar)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Pole wyszukiwania widoczne w topbarze
|
||||||
|
```gherkin
|
||||||
|
Given uzytkownik jest zalogowany i na dowolnej stronie orderPRO
|
||||||
|
When strona sie laduje
|
||||||
|
Then w topbarze widoczne jest pole wyszukiwania z placeholderem "Szukaj zamowien..."
|
||||||
|
And pole jest miedzy hamburgerem a sekcja uzytkownika
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Wyszukiwanie AJAX z debounce
|
||||||
|
```gherkin
|
||||||
|
Given uzytkownik wpisuje tekst w pole wyszukiwania (min. 2 znaki)
|
||||||
|
When minie 300ms od ostatniego znaku (debounce)
|
||||||
|
Then wykonywane jest zapytanie GET /api/orders/search?q=tekst&limit=10
|
||||||
|
And pod polem pojawia sie dropdown z wynikami
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Wyniki wyszukiwania
|
||||||
|
```gherkin
|
||||||
|
Given backend znalazl pasujace zamowienia
|
||||||
|
When dropdown jest wyswietlany
|
||||||
|
Then kazdy wynik pokazuje: numer zamowienia, nazwe klienta, e-mail, telefon
|
||||||
|
And wyniki sa ograniczone do max 10
|
||||||
|
And jesli brak wynikow, wyswietla "Brak wynikow"
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Nawigacja do zamowienia
|
||||||
|
```gherkin
|
||||||
|
Given dropdown z wynikami jest widoczny
|
||||||
|
When uzytkownik klika na wynik
|
||||||
|
Then zostaje przeniesiony do /orders/{id} (strona szczegulow zamowienia)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Nawigacja klawiaturowa
|
||||||
|
```gherkin
|
||||||
|
Given dropdown z wynikami jest widoczny
|
||||||
|
When uzytkownik uzywa strzalek gora/dol
|
||||||
|
Then podswietlony wynik sie zmienia
|
||||||
|
When uzytkownik naciska Enter na podswietlonym wyniku
|
||||||
|
Then zostaje przeniesiony do /orders/{id}
|
||||||
|
When uzytkownik naciska Escape
|
||||||
|
Then dropdown sie zamyka
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-6: Zamykanie dropdowna
|
||||||
|
```gherkin
|
||||||
|
Given dropdown z wynikami jest widoczny
|
||||||
|
When uzytkownik klika poza polem wyszukiwania i dropdownem
|
||||||
|
Then dropdown sie zamyka
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Backend — endpoint quickSearch</name>
|
||||||
|
<files>src/Modules/Orders/OrdersRepository.php, src/Modules/Orders/OrdersController.php, routes/web.php</files>
|
||||||
|
<action>
|
||||||
|
1. W `OrdersRepository.php` dodac metode `quickSearch(string $query, int $limit = 10): array`:
|
||||||
|
- SQL: SELECT o.id, o.source_order_id, o.external_order_id, a.name AS buyer_name, a.email AS buyer_email, a.phone AS buyer_phone
|
||||||
|
- FROM orders o LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = 'customer'
|
||||||
|
- WHERE (o.source_order_id LIKE :s OR o.external_order_id LIKE :s OR a.name LIKE :s OR a.email LIKE :s OR a.phone LIKE :s OR EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id AND oi.original_name LIKE :s))
|
||||||
|
- ORDER BY o.ordered_at DESC LIMIT :limit
|
||||||
|
- Uzyc prepared statements (Medoo raw lub PDO)
|
||||||
|
- Zwracac tablice z id, order_number (source_order_id lub external_order_id), buyer_name, buyer_email, buyer_phone
|
||||||
|
|
||||||
|
2. W `OrdersController.php` dodac metode `quickSearch(Request $request): Response`:
|
||||||
|
- Pobrac q = $request->input('q', '') — jesli pusty lub < 2 znaki, zwrocic pusty JSON []
|
||||||
|
- Pobrac limit = min((int)$request->input('limit', 10), 20)
|
||||||
|
- Wywolac $this->orders->quickSearch($q, $limit)
|
||||||
|
- Zwrocic Response::json(['results' => $results])
|
||||||
|
- Sprawdzic header X-Requested-With = XMLHttpRequest (opcjonalne, ale dobre praktyki)
|
||||||
|
|
||||||
|
3. W `routes/web.php` dodac route:
|
||||||
|
- $router->get('/api/orders/search', [$ordersController, 'quickSearch'], [$authMiddleware]);
|
||||||
|
- Dodac po istniejacych order routes (po linii ~413)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
Recznie wywolac GET /api/orders/search?q=test z przegladarki (zalogowany) — odpowiedz JSON z results array
|
||||||
|
</verify>
|
||||||
|
<done>AC-2 (backend), AC-3 (dane wynikow) spelnione</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Frontend — search bar w topbarze + JS + SCSS</name>
|
||||||
|
<files>resources/views/layouts/app.php, public/assets/js/modules/global-search.js, resources/scss/components/_global-search.scss, resources/scss/app.scss</files>
|
||||||
|
<action>
|
||||||
|
1. W `resources/views/layouts/app.php` w topbarze (miedzy hamburgerem a sekcja uzytkownika):
|
||||||
|
- Dodac div.global-search z:
|
||||||
|
- input type="text" z id="js-global-search" placeholder="Szukaj zamowien..." autocomplete="off"
|
||||||
|
- div.global-search__results (pusty, do wynikow)
|
||||||
|
- Na dole strony (przed zamknieciem body) dodac script include dla global-search.js
|
||||||
|
|
||||||
|
2. Stworzyc `public/assets/js/modules/global-search.js`:
|
||||||
|
- IIFE, 'use strict'
|
||||||
|
- Debounce 300ms na input event
|
||||||
|
- Min. 2 znaki do wyszukiwania
|
||||||
|
- Fetch GET /api/orders/search?q=...&limit=10 z header X-Requested-With: XMLHttpRequest
|
||||||
|
- Renderowanie wynikow w .global-search__results:
|
||||||
|
- Kazdy wynik: link <a href="/orders/{id}"> z numerem zamowienia, nazwiskiem, emailem, telefonem
|
||||||
|
- Jesli brak wynikow: div "Brak wynikow"
|
||||||
|
- Nawigacja klawiaturowa: ArrowDown/ArrowUp zmienia podswietlenie, Enter nawiguje, Escape zamyka
|
||||||
|
- Klik poza komponentem zamyka dropdown (document click listener)
|
||||||
|
- Przy pustym/krotkim query — zamknij dropdown
|
||||||
|
|
||||||
|
3. Stworzyc `resources/scss/components/_global-search.scss`:
|
||||||
|
- .global-search: flex: 1, max-width: 500px, position: relative, margin: 0 1rem
|
||||||
|
- .global-search input: szerokosc 100%, border-radius, padding, font-size
|
||||||
|
- .global-search__results: position absolute, top 100%, left 0, width 100%, background white, border, border-radius, box-shadow, max-height 400px, overflow-y auto, z-index 1000
|
||||||
|
- .global-search__item: padding, cursor pointer, border-bottom, hover/active background
|
||||||
|
- .global-search__item.is-highlighted: background podswietlenia
|
||||||
|
- .global-search__item-title: font-weight bold (numer zamowienia)
|
||||||
|
- .global-search__item-details: font-size mniejszy, color szary (nazwisko, email, telefon)
|
||||||
|
- .global-search__empty: padding, text-align center, color szary
|
||||||
|
- Responsywnosc: na mobile input moze byc mniejszy lub ikona lupa rozwijajaca pole
|
||||||
|
|
||||||
|
4. W `resources/scss/app.scss` dodac @import 'components/global-search'
|
||||||
|
|
||||||
|
5. Zbudowac SCSS do CSS (jesli istnieje build command)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
1. Otworz dowolna strone orderPRO — pole wyszukiwania widoczne w topbarze
|
||||||
|
2. Wpisz nazwe klienta — po 300ms dropdown z wynikami
|
||||||
|
3. Kliknij wynik — przeniesienie do /orders/{id}
|
||||||
|
4. Strzalki gora/dol + Enter — nawigacja klawiaturowa
|
||||||
|
5. Escape lub klik poza — zamkniecie dropdowna
|
||||||
|
</verify>
|
||||||
|
<done>AC-1, AC-2 (frontend), AC-3, AC-4, AC-5, AC-6 spelnione</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- src/Modules/Orders/OrdersRepository.php — istniejaca metoda paginate() i buildPaginateFilters() (nie modyfikowac)
|
||||||
|
- resources/views/orders/list.php — istniejaca wyszukiwarka na liscie zamowien
|
||||||
|
- public/assets/js/modules/inline-status-change.js
|
||||||
|
- database/migrations/*
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Wyszukiwanie TYLKO zamowien (nie produktow — nie ma jeszcze modulu produktow w orderPRO)
|
||||||
|
- Brak historii wyszukiwan
|
||||||
|
- Brak cache wynikow
|
||||||
|
- Nie zmieniac istniejacego wyszukiwania na liscie zamowien
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] GET /api/orders/search?q=test zwraca JSON z wynikami
|
||||||
|
- [ ] Pole wyszukiwania widoczne w topbarze na kazdej stronie
|
||||||
|
- [ ] Dropdown wynikow pojawia sie po wpisaniu 2+ znakow
|
||||||
|
- [ ] Klikniecie wyniku przenosi do /orders/{id}
|
||||||
|
- [ ] Nawigacja klawiaturowa (strzalki, Enter, Escape) dziala
|
||||||
|
- [ ] Klik poza zamyka dropdown
|
||||||
|
- [ ] Na mobile pole wyszukiwania nie psuje layoutu topbara
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Oba taski ukonczone
|
||||||
|
- Wszystkie verification checks przechodzą
|
||||||
|
- Brak regresji w istniejacym UI
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/81-global-search-bar/81-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
98
.paul/phases/81-global-search-bar/81-01-SUMMARY.md
Normal file
98
.paul/phases/81-global-search-bar/81-01-SUMMARY.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
phase: 81-global-search-bar
|
||||||
|
plan: 01
|
||||||
|
subsystem: ui
|
||||||
|
tags: [javascript, ajax, search, orders, topbar]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 44-inline-status-change
|
||||||
|
provides: orders list infrastructure
|
||||||
|
provides:
|
||||||
|
- global search bar in topbar
|
||||||
|
- GET /api/orders/search endpoint
|
||||||
|
- keyboard-navigable search dropdown
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [debounced AJAX search with dropdown results]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: [public/assets/js/modules/global-search.js, resources/scss/modules/_global-search.scss]
|
||||||
|
modified: [src/Modules/Orders/OrdersRepository.php, src/Modules/Orders/OrdersController.php, routes/web.php, resources/views/layouts/app.php, resources/scss/app.scss, public/assets/css/app.css]
|
||||||
|
|
||||||
|
key-decisions: []
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Global search: debounced AJAX fetch to /api/orders/search with dropdown rendering"
|
||||||
|
|
||||||
|
duration: 5min
|
||||||
|
started: 2026-04-07T00:00:00Z
|
||||||
|
completed: 2026-04-07T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 81 Plan 01: Global Search Bar Summary
|
||||||
|
|
||||||
|
**Globalna wyszukiwarka zamowien w topbarze — AJAX search z debounce, dropdown wynikow, nawigacja klawiaturowa, przeszukiwanie po numerze/nazwisku/email/telefonie/produkcie.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | 5min |
|
||||||
|
| Tasks | 2 completed |
|
||||||
|
| Files modified | 8 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Pole wyszukiwania w topbarze | Pass | Input miedzy hamburgerem a user info |
|
||||||
|
| AC-2: Wyszukiwanie AJAX z debounce | Pass | 300ms debounce, min 2 znaki |
|
||||||
|
| AC-3: Wyniki wyszukiwania | Pass | Numer, nazwisko, email, telefon; max 10; "Brak wynikow" |
|
||||||
|
| AC-4: Nawigacja do zamowienia | Pass | Klik na wynik → /orders/{id} |
|
||||||
|
| AC-5: Nawigacja klawiaturowa | Pass | ArrowUp/Down, Enter, Escape |
|
||||||
|
| AC-6: Zamykanie dropdowna | Pass | Klik poza zamyka |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Nowy endpoint `GET /api/orders/search?q=...&limit=10` z prepared statements
|
||||||
|
- Metoda `quickSearch()` w OrdersRepository szuka po 6 polach (source_order_id, external_order_id, name, email, phone, product name)
|
||||||
|
- Modul JS global-search.js z debounce, dropdown, nawigacja klawiaturowa
|
||||||
|
- Responsywne style SCSS
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Orders/OrdersRepository.php` | Modified | Nowa metoda `quickSearch()` |
|
||||||
|
| `src/Modules/Orders/OrdersController.php` | Modified | Nowa metoda `quickSearch()` |
|
||||||
|
| `routes/web.php` | Modified | Route GET /api/orders/search |
|
||||||
|
| `resources/views/layouts/app.php` | Modified | Pole wyszukiwania w topbarze + script include |
|
||||||
|
| `public/assets/js/modules/global-search.js` | Created | Modul JS (debounce, dropdown, klawiatura) |
|
||||||
|
| `resources/scss/modules/_global-search.scss` | Created | Style komponentu wyszukiwarki |
|
||||||
|
| `resources/scss/app.scss` | Modified | Import nowego modulu SCSS |
|
||||||
|
| `public/assets/css/app.css` | Modified | Zbudowany CSS |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None — plan executed exactly as written.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Wyszukiwarka globalna dziala na kazdej stronie
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 81-global-search-bar, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
111
.paul/phases/82-product-title-tooltip/82-01-PLAN.md
Normal file
111
.paul/phases/82-product-title-tooltip/82-01-PLAN.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
phase: 82-product-title-tooltip
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified: [src/Modules/Orders/OrdersController.php]
|
||||||
|
autonomous: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Dodanie natywnego tooltipa (atrybut `title`) do uciętych nazw produktów na liście zamówień, aby po najechaniu myszką wyświetlała się pełna nazwa.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Na liście zamówień nazwy produktów są obcinane przez `text-overflow: ellipsis`. Użytkownik nie ma możliwości zobaczenia pełnej nazwy bez wchodzenia w szczegóły zamówienia. Tooltip rozwiązuje ten problem minimalnym kosztem.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
Zmodyfikowany `OrdersController::productsHtml()` — atrybut `title` na elemencie `.orders-product__name`.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Orders/OrdersController.php (metoda productsHtml(), linia 664)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
No specialized flows required.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Tooltip z pełną nazwą produktu
|
||||||
|
```gherkin
|
||||||
|
Given lista zamówień z produktem o długiej nazwie uciętej przez ellipsis
|
||||||
|
When użytkownik najedzie kursorem na uciętą nazwę produktu
|
||||||
|
Then pojawia się natywny tooltip przeglądarki z pełną nazwą produktu
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Brak tooltipa dla pustych nazw
|
||||||
|
```gherkin
|
||||||
|
Given produkt bez nazwy (wyświetlany jako "-")
|
||||||
|
When użytkownik najedzie kursorem na "-"
|
||||||
|
Then nie pojawia się tooltip (brak atrybutu title lub pusty)
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Dodanie atrybutu title do .orders-product__name</name>
|
||||||
|
<files>src/Modules/Orders/OrdersController.php</files>
|
||||||
|
<action>
|
||||||
|
W metodzie `productsHtml()`, linia 664, dodać atrybut `title` do `<div class="orders-product__name">`:
|
||||||
|
|
||||||
|
Zmienić:
|
||||||
|
```php
|
||||||
|
. '<div class="orders-product__name">' . htmlspecialchars($name !== '' ? $name : '-', ENT_QUOTES, 'UTF-8') . '</div>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Na:
|
||||||
|
```php
|
||||||
|
. '<div class="orders-product__name"' . ($name !== '' ? ' title="' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '"' : '') . '>' . htmlspecialchars($name !== '' ? $name : '-', ENT_QUOTES, 'UTF-8') . '</div>'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Atrybut `title` dodawany tylko gdy nazwa nie jest pusta
|
||||||
|
- Wartość `title` escapowana przez `htmlspecialchars` (XSS safety)
|
||||||
|
- Nie zmieniać żadnej innej logiki metody
|
||||||
|
</action>
|
||||||
|
<verify>Otworzyć /orders/list, najechać na uciętą nazwę produktu — powinien pojawić się tooltip z pełną nazwą</verify>
|
||||||
|
<done>AC-1 i AC-2 spełnione: tooltip pokazuje pełną nazwę; brak tooltipa dla pustych nazw</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- resources/scss/* (styl truncacji zostaje bez zmian)
|
||||||
|
- resources/views/orders/* (widoki bez zmian)
|
||||||
|
- Logika budowania `$itemsPreview` i reszta metody `productsHtml()`
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Tylko natywny tooltip HTML (`title`), bez custom JS tooltip library
|
||||||
|
- Tylko lista zamówień — nie strona szczegółów
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Atrybut `title` obecny w renderowanym HTML produktów z długą nazwą
|
||||||
|
- [ ] Brak atrybutu `title` dla produktów bez nazwy
|
||||||
|
- [ ] Brak regresji — lista zamówień renderuje się poprawnie
|
||||||
|
- [ ] Wszystkie acceptance criteria spełnione
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Tooltip z pełną nazwą produktu widoczny po hover na liście zamówień
|
||||||
|
- Brak zmian CSS ani JS
|
||||||
|
- Brak regresji w renderowaniu listy
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/82-product-title-tooltip/82-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
84
.paul/phases/82-product-title-tooltip/82-01-SUMMARY.md
Normal file
84
.paul/phases/82-product-title-tooltip/82-01-SUMMARY.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
phase: 82-product-title-tooltip
|
||||||
|
plan: 01
|
||||||
|
subsystem: ui
|
||||||
|
tags: [tooltip, orders-list, ux]
|
||||||
|
|
||||||
|
requires: []
|
||||||
|
provides:
|
||||||
|
- Natywny tooltip z pelna nazwa produktu na liscie zamowien
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: []
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created: []
|
||||||
|
modified: [src/Modules/Orders/OrdersController.php]
|
||||||
|
|
||||||
|
key-decisions: []
|
||||||
|
|
||||||
|
patterns-established: []
|
||||||
|
|
||||||
|
duration: 2min
|
||||||
|
started: 2026-04-07T00:00:00Z
|
||||||
|
completed: 2026-04-07T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 82 Plan 01: Product Title Tooltip Summary
|
||||||
|
|
||||||
|
**Dodano atrybut `title` do elementu `.orders-product__name` w metodzie `productsHtml()` — natywny tooltip przegladarki z pelna nazwa produktu na liscie zamowien.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~2min |
|
||||||
|
| Tasks | 1 completed |
|
||||||
|
| Files modified | 1 |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Tooltip z pelna nazwa produktu | Pass | Atrybut `title` dodany z escapowana nazwa |
|
||||||
|
| AC-2: Brak tooltipa dla pustych nazw | Pass | Warunek `$name !== ''` pomija pusty title |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Dodano atrybut `title` do `<div class="orders-product__name">` z pelna nazwa produktu (XSS-safe przez `htmlspecialchars`)
|
||||||
|
- Tooltip pojawia sie tylko gdy nazwa nie jest pusta
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `src/Modules/Orders/OrdersController.php` | Modified | Dodano atrybut `title` w metodzie `productsHtml()` linia 664 |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
None - followed plan as specified
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
None - plan executed exactly as written
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Tooltip dziala natywnie, zero zaleznosci JS/CSS
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
**Blockers:**
|
||||||
|
- None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 82-product-title-tooltip, Plan: 01*
|
||||||
|
*Completed: 2026-04-07*
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
- `POST /settings/integrations/shoppro/save`
|
- `POST /settings/integrations/shoppro/save`
|
||||||
- `POST /settings/integrations/shoppro/test`
|
- `POST /settings/integrations/shoppro/test`
|
||||||
- `POST /settings/integrations/shoppro/statuses/save`
|
- `POST /settings/integrations/shoppro/statuses/save`
|
||||||
|
- `POST /settings/integrations/shoppro/statuses/save-pull`
|
||||||
- `POST /settings/integrations/shoppro/statuses/sync`
|
- `POST /settings/integrations/shoppro/statuses/sync`
|
||||||
- `POST /settings/integrations/shoppro/delivery/save`
|
- `POST /settings/integrations/shoppro/delivery/save`
|
||||||
- `GET /settings/accounting`
|
- `GET /settings/accounting`
|
||||||
@@ -337,6 +338,7 @@
|
|||||||
- `ShipmentController::prepare(Request): Response`,
|
- `ShipmentController::prepare(Request): Response`,
|
||||||
- laduje uslugi dostawy providerow z `ShipmentProviderRegistry` (aktualnie: `allegro_wza`, `apaczka`),
|
- laduje uslugi dostawy providerow z `ShipmentProviderRegistry` (aktualnie: `allegro_wza`, `apaczka`),
|
||||||
- pobiera automatyczne mapowanie formy dostawy przez `CarrierDeliveryMethodMappingRepository` (`source_system` + `source_integration_id` + `order_delivery_method`),
|
- pobiera automatyczne mapowanie formy dostawy przez `CarrierDeliveryMethodMappingRepository` (`source_system` + `source_integration_id` + `order_delivery_method`),
|
||||||
|
- `buildReceiverAddress` laczy delivery + customer z fallbackami: jezeli delivery nie ma ulicy (`street_name`), wszystkie brakujace pola adresowe (name, street, city, zip, country, phone, email) sa uzupelniane z customer,
|
||||||
- dla dostaw punktowych (`parcel_external_id`/`parcel_name`) prefillem `receiver_name` sa dane klienta (a nie nazwa punktu/metody dostawy),
|
- dla dostaw punktowych (`parcel_external_id`/`parcel_name`) prefillem `receiver_name` sa dane klienta (a nie nazwa punktu/metody dostawy),
|
||||||
- gdy mapowanie nie zostanie znalezione, buduje komunikat diagnostyczny (brak mapowan dla instancji lub brak mapowania konkretnej metody) i przekazuje go do widoku.
|
- gdy mapowanie nie zostanie znalezione, buduje komunikat diagnostyczny (brak mapowan dla instancji lub brak mapowania konkretnej metody) i przekazuje go do widoku.
|
||||||
- `POST /orders/{id}/shipment/create`:
|
- `POST /orders/{id}/shipment/create`:
|
||||||
@@ -562,6 +564,11 @@
|
|||||||
- zapisuje mapowania per instancja shopPRO przez `ShopproStatusMappingRepository::replaceForIntegration(...)` do `order_status_mappings` (klucz: `orderpro_status_code`).
|
- zapisuje mapowania per instancja shopPRO przez `ShopproStatusMappingRepository::replaceForIntegration(...)` do `order_status_mappings` (klucz: `orderpro_status_code`).
|
||||||
- `ShopproStatusMappingRepository::listExternalStatuses(int)` — zwraca liste zewnetrznych statusow shopPRO dla danej integracji.
|
- `ShopproStatusMappingRepository::listExternalStatuses(int)` — zwraca liste zewnetrznych statusow shopPRO dla danej integracji.
|
||||||
- `ShopproIntegrationsController` uzywa `buildMappingIndex()` + `buildExternalStatusOptions()` zamiast poprzedniego `buildStatusRows()`.
|
- `ShopproIntegrationsController` uzywa `buildMappingIndex()` + `buildExternalStatusOptions()` zamiast poprzedniego `buildStatusRows()`.
|
||||||
|
- `POST /settings/integrations/shoppro/statuses/save-pull`:
|
||||||
|
- `ShopproIntegrationsController::savePullStatusMappings(Request): Response`
|
||||||
|
- waliduje CSRF, `integration_id` i kody statusow,
|
||||||
|
- zapisuje mapowania pull (shopPRO → orderPRO) przez `ShopproPullStatusMappingRepository::replaceForIntegration(...)` do `order_status_pull_mappings` (klucz: `shoppro_status_code`).
|
||||||
|
- `ShopproIntegrationsController` uzywa `buildPullMappingIndex()` do zaladowania pull mappings do widoku.
|
||||||
- `POST /settings/integrations/shoppro/delivery/save`:
|
- `POST /settings/integrations/shoppro/delivery/save`:
|
||||||
- `ShopproIntegrationsController::saveDeliveryMappings(Request): Response`
|
- `ShopproIntegrationsController::saveDeliveryMappings(Request): Response`
|
||||||
- waliduje CSRF i `integration_id`,
|
- waliduje CSRF i `integration_id`,
|
||||||
@@ -575,6 +582,7 @@
|
|||||||
- mapuje adres faktury do `order_addresses.address_type=invoice` (firma/NIP/adres) na podstawie pol `invoice`/`billing*`/`firm_*`,
|
- mapuje adres faktury do `order_addresses.address_type=invoice` (firma/NIP/adres) na podstawie pol `invoice`/`billing*`/`firm_*`,
|
||||||
- mapuje punkty odbioru (`inpost_paczkomat` / `orlen_point`) do adresu `delivery` (`parcel_external_id`, `parcel_name`, ulica/kod/miasto),
|
- mapuje punkty odbioru (`inpost_paczkomat` / `orlen_point`) do adresu `delivery` (`parcel_external_id`, `parcel_name`, ulica/kod/miasto),
|
||||||
- uzupelnia `delivery` o telefon/e-mail klienta i etykiete metody dostawy z kosztem (`transport_cost`).
|
- uzupelnia `delivery` o telefon/e-mail klienta i etykiete metody dostawy z kosztem (`transport_cost`).
|
||||||
|
- `ShopproOrderMapper::extractPersonalization()` buduje personalizacje z pol: `attributes`, `custom_fields` i `message` (wiadomosc klienta z prefiksem "Wiadomosc:").
|
||||||
- `ShopproStatusSyncService`:
|
- `ShopproStatusSyncService`:
|
||||||
- uruchamiany z crona (`shoppro_order_status_sync`),
|
- uruchamiany z crona (`shoppro_order_status_sync`),
|
||||||
- obsluguje oba kierunki synchronizacji statusow:
|
- obsluguje oba kierunki synchronizacji statusow:
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
|||||||
- indeksy pod filtrowanie po czasie/zdarzeniu/statusie/regule/zamowieniu,
|
- indeksy pod filtrowanie po czasie/zdarzeniu/statusie/regule/zamowieniu,
|
||||||
- seed harmonogramu `cron_schedules` dla joba `automation_history_cleanup` (retencja historii starszej niz 30 dni).
|
- seed harmonogramu `cron_schedules` dla joba `automation_history_cleanup` (retencja historii starszej niz 30 dni).
|
||||||
- 2026-04-04: Hotfix trackingu Allegro Delivery (edge API) - rozszerzono mapowanie statusow EN i fallback keyword matching (`Parcel is awaiting pick-up`, `Parcel has been delivered`, itp.) w warstwie aplikacyjnej; bez zmian schematu bazy.
|
- 2026-04-04: Hotfix trackingu Allegro Delivery (edge API) - rozszerzono mapowanie statusow EN i fallback keyword matching (`Parcel is awaiting pick-up`, `Parcel has been delivered`, itp.) w warstwie aplikacyjnej; bez zmian schematu bazy.
|
||||||
|
- 2026-04-07: Dodano tabele `order_status_pull_mappings` — dedykowane mapowanie pull (shopPRO → orderPRO) z UNIQUE na `(integration_id, shoppro_status_code)`. Migracja `20260407_000079_pull_status_mappings.sql` tworzy tabele i pre-populuje z istniejacych danych push mappings.
|
||||||
|
|
||||||
## Tabele
|
## Tabele
|
||||||
|
|
||||||
@@ -190,6 +191,19 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
|||||||
- Klucze obce:
|
- Klucze obce:
|
||||||
- `order_status_mappings_integration_fk`: `integration_id` -> `integrations.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`).
|
- `order_status_mappings_integration_fk`: `integration_id` -> `integrations.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`).
|
||||||
|
|
||||||
|
### `order_status_pull_mappings`
|
||||||
|
- Mapowanie pull statusow shopPRO na statusy orderPRO (kierunek import/pull). UNIQUE na shoppro_status_code per integracja.
|
||||||
|
- Kolumny:
|
||||||
|
- `id` (PK, int, AI),
|
||||||
|
- `integration_id` (int, NOT NULL),
|
||||||
|
- `shoppro_status_code` (varchar 100, NOT NULL),
|
||||||
|
- `shoppro_status_name` (varchar 255, nullable),
|
||||||
|
- `orderpro_status_code` (varchar 100, NOT NULL),
|
||||||
|
- `created_at`, `updated_at`.
|
||||||
|
- Indeksy:
|
||||||
|
- `order_status_pull_mappings_integration_shoppro_unique` (UNIQUE: `integration_id`, `shoppro_status_code`),
|
||||||
|
- `order_status_pull_mappings_integration_idx` (`integration_id`).
|
||||||
|
|
||||||
### `allegro_integration_settings`
|
### `allegro_integration_settings`
|
||||||
- Konfiguracja OAuth i sync dla integracji Allegro per srodowisko (`sandbox|production`) zarzadzana z `Ustawienia > Integracje > Allegro`.
|
- Konfiguracja OAuth i sync dla integracji Allegro per srodowisko (`sandbox|production`) zarzadzana z `Ustawienia > Integracje > Allegro`.
|
||||||
- Kolumny:
|
- Kolumny:
|
||||||
|
|||||||
@@ -1,5 +1,59 @@
|
|||||||
# Tech Changelog
|
# Tech Changelog
|
||||||
|
|
||||||
|
## 2026-04-07 — Phase 79: Personalization Message Field
|
||||||
|
|
||||||
|
Rozszerzenie importu personalizacji o pole `message` z API shopPRO.
|
||||||
|
|
||||||
|
- **Problem:** ShopPRO API zwraca pole `message` na pozycjach zamowienia (wiadomosc personalizacji klienta, np. dedykacja), ale `extractPersonalization()` sprawdzalo tylko `attributes` i `custom_fields`. Pole `message` bylo ignorowane.
|
||||||
|
- **Fix:** `ShopproOrderMapper::extractPersonalization()` sprawdza teraz 3 pola: `attributes`, `custom_fields`, `message`. Pole `message` jest poprzedzone etykieta "Wiadomosc:". Migracja backfill uzupelnila 21 istniejacych pozycji.
|
||||||
|
- **Pliki:** `src/Modules/Settings/ShopproOrderMapper.php`, `database/migrations/20260407_000080_backfill_personalization_message.sql`
|
||||||
|
|
||||||
|
## 2026-04-07 — Phase 78: Preset Auto Submit
|
||||||
|
|
||||||
|
Presety przesylek automatycznie submituja formularz po autofill.
|
||||||
|
|
||||||
|
- **Zmiana:** Funkcja `applyPreset()` po wypelnieniu pol formularza (200ms) czeka dodatkowe 300ms i wywoluje `form.submit()`. Dodano `id="shipment-form"` na formularz.
|
||||||
|
- **Pliki:** `resources/views/shipments/prepare.php`
|
||||||
|
|
||||||
|
## 2026-04-07 — Phase 77: COD Amount Fix
|
||||||
|
|
||||||
|
Naprawa automatycznego uzupelniania kwoty pobrania (COD) przy generowaniu przesylki dla zamowien shopPRO.
|
||||||
|
|
||||||
|
- **Problem:** Widok `prepare.php` i `OrdersController` sprawdzaly `external_payment_type_id === 'CASH_ON_DELIVERY'` (format Allegro). ShopPRO wysyla pelna polska nazwe np. `"Platnosc przy odbiorze"`, wiec zamowienia shopPRO nigdy nie byly rozpoznawane jako pobraniowe.
|
||||||
|
- **Fix:** Nowa centralna metoda `StringHelper::isCodPayment()` z dwupoziomowa detekcja: exact match (CASH_ON_DELIVERY, COD, POBRANIE, ZA POBRANIEM) + keyword match (PRZY ODBIORZE, POBRANIEM, POBRANIE). `ShopproOrderMapper` normalizuje COD na `CASH_ON_DELIVERY` przy nowych importach. Wszystkie hardcoded sprawdzenia zastapione helperem.
|
||||||
|
- **Pliki:** `src/Core/Support/StringHelper.php`, `src/Modules/Settings/ShopproOrderMapper.php`, `resources/views/shipments/prepare.php`, `resources/views/orders/show.php`, `src/Modules/Orders/OrdersController.php`
|
||||||
|
|
||||||
|
## 2026-04-07 — Phase 76: Shipment Receiver Fallback
|
||||||
|
|
||||||
|
Naprawa pustych danych odbiorcy na stronie przygotowania przesylki dla zamowien shopPRO.
|
||||||
|
|
||||||
|
- **Problem:** Gdy shopPRO zwraca zamowienie z metoda dostawy (np. "Kurier - przedplata: 0 zl") ale bez osobnego adresu dostawy, mapper tworzyl adres delivery z nazwa metody jako `name` i pustymi polami street/city/zip. `buildReceiverAddress` uzywal tego niekompletnego delivery jako bazy bez fallbacku na dane customer.
|
||||||
|
- **Fix:** `ShipmentController::buildReceiverAddress` — rozszerzono fallbacki z customer na pola `street_name`, `street_number`, `city`, `zip_code`, `country` (obok istniejacych `phone`, `email`). Dodano warunek: jezeli delivery nie ma `street_name`, `name` tez jest pobierane z customer (pokrywa przypadek gdy delivery name to label metody dostawy).
|
||||||
|
- **Pliki:** `src/Modules/Shipments/ShipmentController.php`
|
||||||
|
|
||||||
|
## 2026-04-07 — Phase 75: Pull Status Mapping
|
||||||
|
|
||||||
|
Rozdzielenie mapowania statusow shopPRO na dwa niezalezne kierunki: PUSH (orderPRO→shopPRO) i PULL (shopPRO→orderPRO).
|
||||||
|
|
||||||
|
**Problem:** Phase 74 usunal UNIQUE na shoppro_status_code, pozwalajac wielu statusom orderPRO mapowac na ten sam kod shopPRO. Logika "first wins" w buildStatusMap() + ORDER BY orderpro_status_code ASC powodowala, ze import zawsze wybieralal alfabetycznie pierwszy status (np. "do_odbioru" zamiast "w_realizacji").
|
||||||
|
|
||||||
|
**Rozwiazanie:** Nowa tabela `order_status_pull_mappings` z UNIQUE na `(integration_id, shoppro_status_code)` — dedykowana do pull direction. Push mapping w `order_status_mappings` pozostaje bez zmian.
|
||||||
|
|
||||||
|
**DB:** Migracja 20260407_000079 — nowa tabela order_status_pull_mappings, pre-populate z istniejacych danych.
|
||||||
|
|
||||||
|
**Ochrona statusu przy re-imporcie:** Re-import z shopPRO nie nadpisuje `external_status_id` istniejacego zamowienia, CHYBA ZE obecny status to `nieoplacone` i shopPRO potwierdza platnosc (`payment_status = 2`). Wtedy status zmienia sie na zmapowany (np. `w_realizacji`) i importowane sa dane platnosci. W kazdym innym przypadku status jest zachowany — orderPRO jest master.
|
||||||
|
|
||||||
|
**Pliki:**
|
||||||
|
- database/migrations/20260407_000079_pull_status_mappings.sql
|
||||||
|
- src/Modules/Settings/ShopproPullStatusMappingRepository.php — nowy repository (listByIntegration, replaceForIntegration)
|
||||||
|
- src/Modules/Settings/ShopproIntegrationsController.php — nowa zależność pullStatusMappings, savePullStatusMappings(), buildPullMappingIndex()
|
||||||
|
- src/Modules/Settings/ShopproOrdersSyncService.php — buildStatusMap() korzysta z pull tabeli gdy dostępna, activity log dla payment transition
|
||||||
|
- src/Modules/Orders/OrderImportRepository.php — ochrona statusu przy re-imporcie, payment transition logic, getCurrentStatus()
|
||||||
|
- src/Modules/Cron/CronHandlerFactory.php — wstrzyknięcie ShopproPullStatusMappingRepository
|
||||||
|
- resources/views/settings/shoppro.php — sekcja pull mapping pod push
|
||||||
|
- resources/lang/pl.php — klucze tłumaczeń pull + zmiana tytułów push
|
||||||
|
- routes/web.php — nowa route POST /settings/integrations/shoppro/statuses/save-pull
|
||||||
|
|
||||||
## 2026-04-07 — Phase 74: Reverse Status Mapping
|
## 2026-04-07 — Phase 74: Reverse Status Mapping
|
||||||
|
|
||||||
Odwrocenie kierunku mapowania statusow w integracjach shopPRO i Allegro.
|
Odwrocenie kierunku mapowania statusow w integracjach shopPRO i Allegro.
|
||||||
|
|||||||
28
database/migrations/20260407_000079_pull_status_mappings.sql
Normal file
28
database/migrations/20260407_000079_pull_status_mappings.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-- Phase 75: Separate pull status mappings table
|
||||||
|
-- Pull direction (shopPRO → orderPRO) needs its own table with UNIQUE on shoppro_status_code
|
||||||
|
-- because push direction allows many orderPRO statuses → one shopPRO status
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS order_status_pull_mappings (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
integration_id INT NOT NULL,
|
||||||
|
shoppro_status_code VARCHAR(100) NOT NULL,
|
||||||
|
shoppro_status_name VARCHAR(255) DEFAULT NULL,
|
||||||
|
orderpro_status_code VARCHAR(100) NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE INDEX order_status_pull_mappings_integration_shoppro_unique (integration_id, shoppro_status_code),
|
||||||
|
INDEX order_status_pull_mappings_integration_idx (integration_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Pre-populate from existing push mappings: for each (integration_id, shoppro_status_code)
|
||||||
|
-- take the row with the highest id (most recently created)
|
||||||
|
INSERT IGNORE INTO order_status_pull_mappings (integration_id, shoppro_status_code, shoppro_status_name, orderpro_status_code)
|
||||||
|
SELECT osm.integration_id, osm.shoppro_status_code, osm.shoppro_status_name, osm.orderpro_status_code
|
||||||
|
FROM order_status_mappings osm
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT integration_id, shoppro_status_code, MAX(id) AS max_id
|
||||||
|
FROM order_status_mappings
|
||||||
|
WHERE shoppro_status_code <> ''
|
||||||
|
GROUP BY integration_id, shoppro_status_code
|
||||||
|
) latest ON osm.id = latest.max_id
|
||||||
|
WHERE osm.shoppro_status_code <> '';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
-- Backfill: uzupelnienie personalizacji o pole message z payload_json (pozycje zamowien)
|
||||||
|
-- Dotyczy pozycji z pustym personalization i niepustym message w payload
|
||||||
|
|
||||||
|
UPDATE order_items
|
||||||
|
SET personalization = CONCAT('Wiadomość: ', TRIM(JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.message'))))
|
||||||
|
WHERE payload_json IS NOT NULL
|
||||||
|
AND personalization IS NULL
|
||||||
|
AND JSON_EXTRACT(payload_json, '$.message') IS NOT NULL
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.message')) != ''
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(payload_json, '$.message')) != 'null';
|
||||||
|
|
||||||
|
-- Backfill: import wiadomosci klienta z payload zamowienia do order_notes
|
||||||
|
-- Dotyczy zamowien z polem message w payload ale bez notatki typu 'message'
|
||||||
|
|
||||||
|
INSERT INTO order_notes (order_id, note_type, comment, created_at)
|
||||||
|
SELECT o.id, 'message', TRIM(JSON_UNQUOTE(JSON_EXTRACT(o.payload_json, '$.message'))), NOW()
|
||||||
|
FROM orders o
|
||||||
|
LEFT JOIN order_notes n ON n.order_id = o.id AND n.note_type = 'message'
|
||||||
|
WHERE o.payload_json IS NOT NULL
|
||||||
|
AND JSON_EXTRACT(o.payload_json, '$.message') IS NOT NULL
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(o.payload_json, '$.message')) != ''
|
||||||
|
AND JSON_UNQUOTE(JSON_EXTRACT(o.payload_json, '$.message')) != 'null'
|
||||||
|
AND n.id IS NULL;
|
||||||
File diff suppressed because one or more lines are too long
124
public/assets/js/modules/global-search.js
Normal file
124
public/assets/js/modules/global-search.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var input = document.getElementById('js-global-search');
|
||||||
|
var resultsEl = document.getElementById('js-global-search-results');
|
||||||
|
if (!input || !resultsEl) return;
|
||||||
|
|
||||||
|
var debounceTimer = null;
|
||||||
|
var highlightIndex = -1;
|
||||||
|
var currentResults = [];
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode(str));
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResults() {
|
||||||
|
resultsEl.innerHTML = '';
|
||||||
|
resultsEl.style.display = 'none';
|
||||||
|
highlightIndex = -1;
|
||||||
|
currentResults = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(results) {
|
||||||
|
currentResults = results;
|
||||||
|
highlightIndex = -1;
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
resultsEl.innerHTML = '<div class="global-search__empty">Brak wyników</div>';
|
||||||
|
resultsEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < results.length; i++) {
|
||||||
|
var r = results[i];
|
||||||
|
var details = [];
|
||||||
|
if (r.buyer_name) details.push(escapeHtml(r.buyer_name));
|
||||||
|
if (r.buyer_email) details.push(escapeHtml(r.buyer_email));
|
||||||
|
if (r.buyer_phone) details.push(escapeHtml(r.buyer_phone));
|
||||||
|
|
||||||
|
html += '<a href="/orders/' + r.id + '" class="global-search__item" data-index="' + i + '">'
|
||||||
|
+ '<div class="global-search__item-title">' + escapeHtml(r.order_number || '-') + '</div>'
|
||||||
|
+ '<div class="global-search__item-details">' + details.join(' | ') + '</div>'
|
||||||
|
+ '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsEl.innerHTML = html;
|
||||||
|
resultsEl.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHighlight() {
|
||||||
|
var items = resultsEl.querySelectorAll('.global-search__item');
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
if (i === highlightIndex) {
|
||||||
|
items[i].classList.add('is-highlighted');
|
||||||
|
items[i].scrollIntoView({ block: 'nearest' });
|
||||||
|
} else {
|
||||||
|
items[i].classList.remove('is-highlighted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSearch(query) {
|
||||||
|
fetch('/api/orders/search?q=' + encodeURIComponent(query) + '&limit=10', {
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(function (resp) { return resp.json(); })
|
||||||
|
.then(function (data) {
|
||||||
|
if (input.value.trim().length < 2) {
|
||||||
|
closeResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderResults(data.results || []);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
closeResults();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('input', function () {
|
||||||
|
var query = input.value.trim();
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
closeResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceTimer = setTimeout(function () {
|
||||||
|
doSearch(query);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', function (e) {
|
||||||
|
if (currentResults.length === 0) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightIndex = Math.min(highlightIndex + 1, currentResults.length - 1);
|
||||||
|
updateHighlight();
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightIndex = Math.max(highlightIndex - 1, 0);
|
||||||
|
updateHighlight();
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (highlightIndex >= 0 && highlightIndex < currentResults.length) {
|
||||||
|
window.location.href = '/orders/' + currentResults[highlightIndex].id;
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
closeResults();
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var wrap = document.getElementById('js-global-search-wrap');
|
||||||
|
if (wrap && !wrap.contains(e.target)) {
|
||||||
|
closeResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -152,6 +152,7 @@
|
|||||||
var d = result.data;
|
var d = result.data;
|
||||||
wrap.innerHTML = buildBadgeHtml(d.status_code, d.status_label, d.status_color);
|
wrap.innerHTML = buildBadgeHtml(d.status_code, d.status_label, d.status_color);
|
||||||
wrap.setAttribute('data-current-status', d.status_code);
|
wrap.setAttribute('data-current-status', d.status_code);
|
||||||
|
location.reload();
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
wrap.innerHTML = prevHtml;
|
wrap.innerHTML = prevHtml;
|
||||||
|
|||||||
@@ -1006,8 +1006,8 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'order_statuses' => [
|
'order_statuses' => [
|
||||||
'title' => 'Statusy zamowien',
|
'title' => 'Wysylka statusow (orderPRO → shopPRO)',
|
||||||
'description' => 'Mapowanie statusow zamowien pomiedzy orderPRO i shopPRO.',
|
'description' => 'Okresl, jaki status shopPRO ma otrzymac zamowienie po zmianie statusu w orderPRO.',
|
||||||
'integration' => 'Integracja shopPRO',
|
'integration' => 'Integracja shopPRO',
|
||||||
'no_integrations' => 'Brak aktywnych integracji shopPRO z kluczem API.',
|
'no_integrations' => 'Brak aktywnych integracji shopPRO z kluczem API.',
|
||||||
'empty' => 'Brak statusow shopPRO do zmapowania.',
|
'empty' => 'Brak statusow shopPRO do zmapowania.',
|
||||||
@@ -1039,6 +1039,14 @@ return [
|
|||||||
'saved' => 'Mapowanie statusow zostalo zapisane.',
|
'saved' => 'Mapowanie statusow zostalo zapisane.',
|
||||||
'save_failed' => 'Nie udalo sie zapisac mapowania statusow.',
|
'save_failed' => 'Nie udalo sie zapisac mapowania statusow.',
|
||||||
],
|
],
|
||||||
|
'pull' => [
|
||||||
|
'title' => 'Mapowanie przy imporcie (shopPRO → orderPRO)',
|
||||||
|
'description' => 'Okresl, jaki status orderPRO ma otrzymac zamowienie importowane z danym statusem shopPRO.',
|
||||||
|
'save' => 'Zapisz mapowanie importu',
|
||||||
|
'saved' => 'Mapowanie importu zapisane.',
|
||||||
|
'save_failed' => 'Blad zapisu mapowania importu.',
|
||||||
|
'no_shoppro_statuses' => 'Brak statusow shopPRO. Zsynchronizuj statusy przyciskiem powyzej.',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'cron' => [
|
'cron' => [
|
||||||
'title' => 'Harmonogram',
|
'title' => 'Harmonogram',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@use "modules/shipment-presets";
|
@use "modules/shipment-presets";
|
||||||
@use "modules/delivery-status";
|
@use "modules/delivery-status";
|
||||||
@use "modules/delivery-status-mappings";
|
@use "modules/delivery-status-mappings";
|
||||||
|
@use "modules/global-search";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
92
resources/scss/modules/_global-search.scss
Normal file
92
resources/scss/modules/_global-search.scss
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
.global-search {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 500px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search__input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--c-bg);
|
||||||
|
color: var(--c-text);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--c-text-muted, #94a3b8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--c-primary, #3b82f6);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search__results {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--c-surface, #fff);
|
||||||
|
border: 1px solid var(--c-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search__item {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--c-text);
|
||||||
|
border-bottom: 1px solid var(--c-border);
|
||||||
|
transition: background-color 0.1s;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-highlighted {
|
||||||
|
background: var(--c-bg, #f1f5f9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search__item-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search__item-details {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--c-text-muted, #64748b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search__empty {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--c-text-muted, #94a3b8);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.global-search {
|
||||||
|
max-width: none;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search__input {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,6 +126,10 @@
|
|||||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="global-search" id="js-global-search-wrap">
|
||||||
|
<input type="text" id="js-global-search" class="global-search__input" placeholder="Szukaj zamówień..." autocomplete="off">
|
||||||
|
<div class="global-search__results" id="js-global-search-results"></div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong><?= $e((string) (($user['name'] ?? '') !== '' ? $user['name'] : ($user['email'] ?? ''))) ?></strong>
|
<strong><?= $e((string) (($user['name'] ?? '') !== '' ? $user['name'] : ($user['email'] ?? ''))) ?></strong>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,6 +145,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/js/modules/jquery-alerts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/jquery-alerts.js') ?: 0 ?>"></script>
|
<script src="/assets/js/modules/jquery-alerts.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/jquery-alerts.js') ?: 0 ?>"></script>
|
||||||
|
<script src="/assets/js/modules/global-search.js?ver=<?= filemtime(dirname(__DIR__, 3) . '/public/assets/js/modules/global-search.js') ?: 0 ?>"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var STORAGE_KEY = 'sidebarCollapsed';
|
var STORAGE_KEY = 'sidebarCollapsed';
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ foreach ($addressesList as $address) {
|
|||||||
<?php $personalization = trim((string) ($item['personalization'] ?? '')); ?>
|
<?php $personalization = trim((string) ($item['personalization'] ?? '')); ?>
|
||||||
<?php if ($personalization !== ''): ?>
|
<?php if ($personalization !== ''): ?>
|
||||||
<div class="item-personalization">
|
<div class="item-personalization">
|
||||||
<span class="item-personalization__label">Personalizacja:</span>
|
|
||||||
<?php foreach (explode("\n", $personalization) as $line): ?>
|
<?php foreach (explode("\n", $personalization) as $line): ?>
|
||||||
<?php if (trim($line) !== ''): ?>
|
<?php if (trim($line) !== ''): ?>
|
||||||
<div class="item-personalization__line"><?= $e(trim($line)) ?></div>
|
<div class="item-personalization__line"><?= $e(trim($line)) ?></div>
|
||||||
@@ -220,12 +219,16 @@ foreach ($addressesList as $address) {
|
|||||||
$paymentTypeRaw = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? '')));
|
$paymentTypeRaw = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? '')));
|
||||||
$paymentTypeLabels = [
|
$paymentTypeLabels = [
|
||||||
'CASH_ON_DELIVERY' => 'Za pobraniem',
|
'CASH_ON_DELIVERY' => 'Za pobraniem',
|
||||||
|
'COD' => 'Za pobraniem',
|
||||||
|
'POBRANIE' => 'Za pobraniem',
|
||||||
|
'ZA POBRANIEM' => 'Za pobraniem',
|
||||||
|
'PŁATNOŚĆ PRZY ODBIORZE' => 'Za pobraniem',
|
||||||
'ONLINE' => 'Platnosc online',
|
'ONLINE' => 'Platnosc online',
|
||||||
'TRANSFER' => 'Przelew',
|
'TRANSFER' => 'Przelew',
|
||||||
];
|
];
|
||||||
$paymentTypeLabel = $paymentTypeLabels[$paymentTypeRaw] ?? ($paymentTypeRaw !== '' ? $paymentTypeRaw : '-');
|
$paymentTypeLabel = $paymentTypeLabels[$paymentTypeRaw] ?? ($paymentTypeRaw !== '' ? $paymentTypeRaw : '-');
|
||||||
?>
|
?>
|
||||||
<?php if ($paymentTypeRaw === 'CASH_ON_DELIVERY'): ?>
|
<?php if (\App\Core\Support\StringHelper::isCodPayment($paymentTypeRaw)): ?>
|
||||||
<span class="order-tag is-cod"><?= $e($paymentTypeLabel) ?></span>
|
<span class="order-tag is-cod"><?= $e($paymentTypeLabel) ?></span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?= $e($paymentTypeLabel) ?>
|
<?= $e($paymentTypeLabel) ?>
|
||||||
@@ -576,6 +579,10 @@ foreach ($addressesList as $address) {
|
|||||||
$paymentTypeLabels = [
|
$paymentTypeLabels = [
|
||||||
'ONLINE' => 'Płatność online',
|
'ONLINE' => 'Płatność online',
|
||||||
'CASH_ON_DELIVERY' => 'Za pobraniem',
|
'CASH_ON_DELIVERY' => 'Za pobraniem',
|
||||||
|
'COD' => 'Za pobraniem',
|
||||||
|
'POBRANIE' => 'Za pobraniem',
|
||||||
|
'ZA POBRANIEM' => 'Za pobraniem',
|
||||||
|
'PŁATNOŚĆ PRZY ODBIORZE' => 'Za pobraniem',
|
||||||
'TRANSFER' => 'Przelew bankowy',
|
'TRANSFER' => 'Przelew bankowy',
|
||||||
];
|
];
|
||||||
$providerLabels = [
|
$providerLabels = [
|
||||||
@@ -675,7 +682,7 @@ foreach ($addressesList as $address) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-nowrap"><?= $e($payDate !== '' ? $payDate : '—') ?></td>
|
<td class="text-nowrap"><?= $e($payDate !== '' ? $payDate : '—') ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ($ptRaw === 'CASH_ON_DELIVERY'): ?>
|
<?php if (\App\Core\Support\StringHelper::isCodPayment($ptRaw)): ?>
|
||||||
<span class="order-tag is-cod"><?= $e($ptLabel) ?></span>
|
<span class="order-tag is-cod"><?= $e($ptLabel) ?></span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?= $e($ptLabel) ?>
|
<?= $e($ptLabel) ?>
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ foreach ($dmMappings as $dm) {
|
|||||||
|
|
||||||
<div class="content-tab-panel<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-statuses">
|
<div class="content-tab-panel<?= $activeTab === 'statuses' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-statuses">
|
||||||
<section class="mt-16">
|
<section class="mt-16">
|
||||||
<h3 class="section-title"><?= $e($t('settings.integrations.statuses.title')) ?></h3>
|
<h3 class="section-title"><?= $e($t('settings.order_statuses.title')) ?></h3>
|
||||||
<p class="muted mt-12"><?= $e($t('settings.integrations.statuses.description')) ?></p>
|
<p class="muted mt-12"><?= $e($t('settings.order_statuses.description')) ?></p>
|
||||||
|
|
||||||
<?php if (!$isEdit && $list === []): ?>
|
<?php if (!$isEdit && $list === []): ?>
|
||||||
<p class="muted mt-12"><?= $e($t('settings.integrations.statuses.select_integration_first')) ?></p>
|
<p class="muted mt-12"><?= $e($t('settings.integrations.statuses.select_integration_first')) ?></p>
|
||||||
@@ -264,6 +264,74 @@ foreach ($dmMappings as $dm) {
|
|||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-24">
|
||||||
|
<h3 class="section-title"><?= $e($t('settings.order_statuses.pull.title')) ?></h3>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.order_statuses.pull.description')) ?></p>
|
||||||
|
|
||||||
|
<?php if (!$isEdit && $list === []): ?>
|
||||||
|
<p class="muted mt-12"><?= $e($t('settings.integrations.statuses.select_integration_first')) ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<form class="mt-12" action="/settings/integrations/shoppro/statuses/save-pull" method="post">
|
||||||
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
<input type="hidden" name="integration_id" value="<?= $e((string) ($formValues['integration_id'] ?? 0)) ?>">
|
||||||
|
<div class="table-wrap mt-12">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?= $e($t('settings.order_statuses.fields.shoppro_status')) ?></th>
|
||||||
|
<th><?= $e($t('settings.order_statuses.fields.orderpro_status')) ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($shopproStatuses === []): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="muted"><?= $e($t('settings.order_statuses.pull.no_shoppro_statuses')) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($shopproStatuses as $extStatus): ?>
|
||||||
|
<?php
|
||||||
|
$extCode = (string) ($extStatus['code'] ?? '');
|
||||||
|
$extName = (string) ($extStatus['name'] ?? $extCode);
|
||||||
|
if ($extCode === '') continue;
|
||||||
|
$pullMapped = $pullMappingIndex[$extCode] ?? null;
|
||||||
|
$selectedOrderproCode = $pullMapped !== null ? (string) ($pullMapped['orderpro_status_code'] ?? '') : '';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<?= $e($extName) ?> <code class="muted">(<?= $e($extCode) ?>)</code>
|
||||||
|
<input type="hidden" name="shoppro_status_code[]" value="<?= $e($extCode) ?>">
|
||||||
|
<input type="hidden" name="shoppro_status_name[]" value="<?= $e($extName) ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select class="form-control" name="orderpro_status_code[]">
|
||||||
|
<option value=""><?= $e($t('settings.order_statuses.fields.no_mapping')) ?></option>
|
||||||
|
<?php foreach ($orderproStatuses as $opStatus): ?>
|
||||||
|
<?php
|
||||||
|
$opCode = strtolower(trim((string) ($opStatus['code'] ?? '')));
|
||||||
|
$opName = (string) ($opStatus['name'] ?? $opCode);
|
||||||
|
if ($opCode === '') continue;
|
||||||
|
?>
|
||||||
|
<option value="<?= $e($opCode) ?>"<?= $selectedOrderproCode === $opCode ? ' selected' : '' ?>>
|
||||||
|
<?= $e($opName) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php if ($shopproStatuses !== []): ?>
|
||||||
|
<div class="form-actions mt-12">
|
||||||
|
<button type="submit" class="btn btn--primary"><?= $e($t('settings.order_statuses.pull.save')) ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-tab-panel<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-settings">
|
<div class="content-tab-panel<?= $activeTab === 'settings' ? ' is-active' : '' ?>" data-tab-panel="shoppro-tab-settings">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ $pointId = trim((string) ($receiver['parcel_external_id'] ?? ''));
|
|||||||
$pointName = trim((string) ($receiver['parcel_name'] ?? ''));
|
$pointName = trim((string) ($receiver['parcel_name'] ?? ''));
|
||||||
$totalWithTax = (float) ($orderRow['total_with_tax'] ?? 0);
|
$totalWithTax = (float) ($orderRow['total_with_tax'] ?? 0);
|
||||||
$currency = strtoupper(trim((string) ($orderRow['currency'] ?? 'PLN')));
|
$currency = strtoupper(trim((string) ($orderRow['currency'] ?? 'PLN')));
|
||||||
$isCod = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? ''))) === 'CASH_ON_DELIVERY';
|
$isCod = \App\Core\Support\StringHelper::isCodPayment((string) ($orderRow['external_payment_type_id'] ?? ''));
|
||||||
$defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
$defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/create" novalidate>
|
<form id="shipment-form" method="post" action="/orders/<?= $e((string) ($orderId ?? 0)) ?>/shipment/create" novalidate>
|
||||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||||
|
|
||||||
<div class="shipment-grid mt-16">
|
<div class="shipment-grid mt-16">
|
||||||
@@ -989,6 +989,12 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0';
|
|||||||
|
|
||||||
// Select delivery service in the correct panel
|
// Select delivery service in the correct panel
|
||||||
selectDeliveryService(preset);
|
selectDeliveryService(preset);
|
||||||
|
|
||||||
|
// Auto-submit after autofill completes
|
||||||
|
setTimeout(function () {
|
||||||
|
var form = document.getElementById('shipment-form');
|
||||||
|
if (form) form.submit();
|
||||||
|
}, 300);
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ use App\Modules\Settings\IntegrationsHubController;
|
|||||||
use App\Modules\Settings\IntegrationsRepository;
|
use App\Modules\Settings\IntegrationsRepository;
|
||||||
use App\Modules\Settings\ShopproIntegrationsController;
|
use App\Modules\Settings\ShopproIntegrationsController;
|
||||||
use App\Modules\Settings\ShopproIntegrationsRepository;
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
|
use App\Modules\Settings\ShopproPullStatusMappingRepository;
|
||||||
use App\Modules\Settings\ShopproStatusMappingRepository;
|
use App\Modules\Settings\ShopproStatusMappingRepository;
|
||||||
use App\Modules\Settings\CompanySettingsController;
|
use App\Modules\Settings\CompanySettingsController;
|
||||||
use App\Modules\Settings\CompanySettingsRepository;
|
use App\Modules\Settings\CompanySettingsRepository;
|
||||||
@@ -166,6 +167,7 @@ return static function (Application $app): void {
|
|||||||
$auth,
|
$auth,
|
||||||
$shopproIntegrationsRepository,
|
$shopproIntegrationsRepository,
|
||||||
new ShopproStatusMappingRepository($app->db()),
|
new ShopproStatusMappingRepository($app->db()),
|
||||||
|
new ShopproPullStatusMappingRepository($app->db()),
|
||||||
$app->orderStatuses(),
|
$app->orderStatuses(),
|
||||||
$cronRepository,
|
$cronRepository,
|
||||||
$carrierDeliveryMappings,
|
$carrierDeliveryMappings,
|
||||||
@@ -409,6 +411,7 @@ return static function (Application $app): void {
|
|||||||
$router->post('/orders/{id}/status', [$ordersController, 'updateStatus'], [$authMiddleware]);
|
$router->post('/orders/{id}/status', [$ordersController, 'updateStatus'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/send-email', [$ordersController, 'sendEmail'], [$authMiddleware]);
|
$router->post('/orders/{id}/send-email', [$ordersController, 'sendEmail'], [$authMiddleware]);
|
||||||
$router->post('/orders/{id}/email-preview', [$ordersController, 'emailPreview'], [$authMiddleware]);
|
$router->post('/orders/{id}/email-preview', [$ordersController, 'emailPreview'], [$authMiddleware]);
|
||||||
|
$router->get('/api/orders/search', [$ordersController, 'quickSearch'], [$authMiddleware]);
|
||||||
$router->post('/users', [$usersController, 'store'], [$authMiddleware]);
|
$router->post('/users', [$usersController, 'store'], [$authMiddleware]);
|
||||||
$router->get('/settings/users', [$usersController, 'index'], [$authMiddleware]);
|
$router->get('/settings/users', [$usersController, 'index'], [$authMiddleware]);
|
||||||
$router->post('/settings/users', [$usersController, 'store'], [$authMiddleware]);
|
$router->post('/settings/users', [$usersController, 'store'], [$authMiddleware]);
|
||||||
@@ -447,6 +450,7 @@ return static function (Application $app): void {
|
|||||||
$router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]);
|
$router->post('/settings/integrations/shoppro/save', [$shopproIntegrationsController, 'save'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]);
|
$router->post('/settings/integrations/shoppro/test', [$shopproIntegrationsController, 'test'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/shoppro/statuses/save', [$shopproIntegrationsController, 'saveStatusMappings'], [$authMiddleware]);
|
$router->post('/settings/integrations/shoppro/statuses/save', [$shopproIntegrationsController, 'saveStatusMappings'], [$authMiddleware]);
|
||||||
|
$router->post('/settings/integrations/shoppro/statuses/save-pull', [$shopproIntegrationsController, 'savePullStatusMappings'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/shoppro/statuses/sync', [$shopproIntegrationsController, 'syncStatuses'], [$authMiddleware]);
|
$router->post('/settings/integrations/shoppro/statuses/sync', [$shopproIntegrationsController, 'syncStatuses'], [$authMiddleware]);
|
||||||
$router->post('/settings/integrations/shoppro/delivery/save', [$shopproIntegrationsController, 'saveDeliveryMappings'], [$authMiddleware]);
|
$router->post('/settings/integrations/shoppro/delivery/save', [$shopproIntegrationsController, 'saveDeliveryMappings'], [$authMiddleware]);
|
||||||
$router->get('/settings/company', [$companySettingsController, 'index'], [$authMiddleware]);
|
$router->get('/settings/company', [$companySettingsController, 'index'], [$authMiddleware]);
|
||||||
|
|||||||
@@ -28,6 +28,33 @@ final class StringHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const COD_PAYMENT_TYPES = [
|
||||||
|
'CASH_ON_DELIVERY',
|
||||||
|
'COD',
|
||||||
|
'POBRANIE',
|
||||||
|
'ZA POBRANIEM',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const COD_PAYMENT_KEYWORDS = [
|
||||||
|
'PRZY ODBIORZE',
|
||||||
|
'POBRANIEM',
|
||||||
|
'POBRANIE',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function isCodPayment(string $value): bool
|
||||||
|
{
|
||||||
|
$normalized = strtoupper(trim($value));
|
||||||
|
if (in_array($normalized, self::COD_PAYMENT_TYPES, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
foreach (self::COD_PAYMENT_KEYWORDS as $keyword) {
|
||||||
|
if (str_contains($normalized, $keyword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static function normalizeColorHex(string $value): string
|
public static function normalizeColorHex(string $value): string
|
||||||
{
|
{
|
||||||
$trimmed = trim($value);
|
$trimmed = trim($value);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ use App\Modules\Settings\ShopproApiClient;
|
|||||||
use App\Modules\Settings\ShopproIntegrationsRepository;
|
use App\Modules\Settings\ShopproIntegrationsRepository;
|
||||||
use App\Modules\Settings\ShopproOrderMapper;
|
use App\Modules\Settings\ShopproOrderMapper;
|
||||||
use App\Modules\Settings\ShopproOrdersSyncService;
|
use App\Modules\Settings\ShopproOrdersSyncService;
|
||||||
|
use App\Modules\Settings\ShopproPullStatusMappingRepository;
|
||||||
use App\Modules\Settings\ShopproOrderSyncStateRepository;
|
use App\Modules\Settings\ShopproOrderSyncStateRepository;
|
||||||
use App\Modules\Settings\ShopproPaymentStatusSyncService;
|
use App\Modules\Settings\ShopproPaymentStatusSyncService;
|
||||||
use App\Modules\Settings\ShopproProductImageResolver;
|
use App\Modules\Settings\ShopproProductImageResolver;
|
||||||
@@ -90,6 +91,7 @@ final class CronHandlerFactory
|
|||||||
$shopproApiClient = new ShopproApiClient();
|
$shopproApiClient = new ShopproApiClient();
|
||||||
$shopproSyncStateRepo = new ShopproOrderSyncStateRepository($this->db);
|
$shopproSyncStateRepo = new ShopproOrderSyncStateRepository($this->db);
|
||||||
$shopproStatusMappingRepo = new ShopproStatusMappingRepository($this->db);
|
$shopproStatusMappingRepo = new ShopproStatusMappingRepository($this->db);
|
||||||
|
$shopproPullStatusMappingRepo = new ShopproPullStatusMappingRepository($this->db);
|
||||||
$shopproSyncService = new ShopproOrdersSyncService(
|
$shopproSyncService = new ShopproOrdersSyncService(
|
||||||
$shopproIntegrationsRepo,
|
$shopproIntegrationsRepo,
|
||||||
$shopproSyncStateRepo,
|
$shopproSyncStateRepo,
|
||||||
@@ -98,7 +100,8 @@ final class CronHandlerFactory
|
|||||||
$shopproStatusMappingRepo,
|
$shopproStatusMappingRepo,
|
||||||
$ordersRepository,
|
$ordersRepository,
|
||||||
new ShopproOrderMapper(),
|
new ShopproOrderMapper(),
|
||||||
new ShopproProductImageResolver($shopproApiClient)
|
new ShopproProductImageResolver($shopproApiClient),
|
||||||
|
$shopproPullStatusMappingRepo
|
||||||
);
|
);
|
||||||
$shopproStatusSyncService = new ShopproStatusSyncService(
|
$shopproStatusSyncService = new ShopproStatusSyncService(
|
||||||
$shopproIntegrationsRepo,
|
$shopproIntegrationsRepo,
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ final class OrderImportRepository
|
|||||||
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ''));
|
$sourceOrderId = trim((string) ($orderData['source_order_id'] ?? ''));
|
||||||
$existingOrderId = $this->findOrderIdBySource($source, $sourceOrderId);
|
$existingOrderId = $this->findOrderIdBySource($source, $sourceOrderId);
|
||||||
$created = $existingOrderId === null;
|
$created = $existingOrderId === null;
|
||||||
|
$paymentTransition = false;
|
||||||
|
|
||||||
|
if (!$created) {
|
||||||
|
$currentStatus = $this->getCurrentStatus($existingOrderId);
|
||||||
|
$newPaymentStatus = (int) ($orderData['payment_status'] ?? 0);
|
||||||
|
$paymentTransition = $currentStatus === 'nieoplacone' && $newPaymentStatus === 2;
|
||||||
|
if (!$paymentTransition) {
|
||||||
|
$orderData['external_status_id'] = $currentStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$orderId = $created
|
$orderId = $created
|
||||||
? $this->insertOrder($orderData)
|
? $this->insertOrder($orderData)
|
||||||
: $this->updateOrder($existingOrderId, $orderData);
|
: $this->updateOrder($existingOrderId, $orderData);
|
||||||
@@ -50,6 +61,8 @@ final class OrderImportRepository
|
|||||||
$this->replacePayments($orderId, $payments);
|
$this->replacePayments($orderId, $payments);
|
||||||
$this->replaceShipments($orderId, $shipments);
|
$this->replaceShipments($orderId, $shipments);
|
||||||
$this->replaceStatusHistory($orderId, $statusHistory);
|
$this->replaceStatusHistory($orderId, $statusHistory);
|
||||||
|
} elseif ($paymentTransition) {
|
||||||
|
$this->replacePayments($orderId, $payments);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
@@ -63,6 +76,7 @@ final class OrderImportRepository
|
|||||||
return [
|
return [
|
||||||
'order_id' => $orderId,
|
'order_id' => $orderId,
|
||||||
'created' => $created,
|
'created' => $created,
|
||||||
|
'payment_transition' => $paymentTransition,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +101,17 @@ final class OrderImportRepository
|
|||||||
return $id > 0 ? $id : null;
|
return $id > 0 ? $id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getCurrentStatus(int $orderId): string
|
||||||
|
{
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT external_status_id FROM orders WHERE id = :id LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['id' => $orderId]);
|
||||||
|
$value = $statement->fetchColumn();
|
||||||
|
|
||||||
|
return strtolower(trim((string) ($value ?: '')));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $orderData
|
* @param array<string, mixed> $orderData
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ final class OrdersController
|
|||||||
$totalWithTax = $row['total_with_tax'] !== null ? number_format((float) $row['total_with_tax'], 2, '.', ' ') : '-';
|
$totalWithTax = $row['total_with_tax'] !== null ? number_format((float) $row['total_with_tax'], 2, '.', ' ') : '-';
|
||||||
$totalPaid = $row['total_paid'] !== null ? number_format((float) $row['total_paid'], 2, '.', ' ') : '-';
|
$totalPaid = $row['total_paid'] !== null ? number_format((float) $row['total_paid'], 2, '.', ' ') : '-';
|
||||||
$paymentType = strtoupper(trim((string) ($row['external_payment_type_id'] ?? '')));
|
$paymentType = strtoupper(trim((string) ($row['external_payment_type_id'] ?? '')));
|
||||||
$isCod = $paymentType === 'CASH_ON_DELIVERY';
|
$isCod = StringHelper::isCodPayment($paymentType);
|
||||||
$paymentStatus = isset($row['payment_status']) ? (int) $row['payment_status'] : null;
|
$paymentStatus = isset($row['payment_status']) ? (int) $row['payment_status'] : null;
|
||||||
$isUnpaid = !$isCod && $paymentStatus === 0;
|
$isUnpaid = !$isCod && $paymentStatus === 0;
|
||||||
$itemsCount = max(0, (int) ($row['items_count'] ?? 0));
|
$itemsCount = max(0, (int) ($row['items_count'] ?? 0));
|
||||||
@@ -661,7 +661,7 @@ final class OrdersController
|
|||||||
$html .= '<div class="orders-product">'
|
$html .= '<div class="orders-product">'
|
||||||
. $thumb
|
. $thumb
|
||||||
. '<div class="orders-product__txt">'
|
. '<div class="orders-product__txt">'
|
||||||
. '<div class="orders-product__name">' . htmlspecialchars($name !== '' ? $name : '-', ENT_QUOTES, 'UTF-8') . '</div>'
|
. '<div class="orders-product__name"' . ($name !== '' ? ' title="' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '"' : '') . '>' . htmlspecialchars($name !== '' ? $name : '-', ENT_QUOTES, 'UTF-8') . '</div>'
|
||||||
. '<div class="orders-product__qty">' . htmlspecialchars($qty, ENT_QUOTES, 'UTF-8') . ' szt.</div>'
|
. '<div class="orders-product__qty">' . htmlspecialchars($qty, ENT_QUOTES, 'UTF-8') . ' szt.</div>'
|
||||||
. '</div>'
|
. '</div>'
|
||||||
. '</div>';
|
. '</div>';
|
||||||
@@ -914,4 +914,17 @@ final class OrdersController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function quickSearch(Request $request): Response
|
||||||
|
{
|
||||||
|
$query = trim((string) $request->input('q', ''));
|
||||||
|
if ($query === '' || mb_strlen($query) < 2) {
|
||||||
|
return Response::json(['results' => []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = min((int) $request->input('limit', 10), 20);
|
||||||
|
$results = $this->orders->quickSearch($query, $limit);
|
||||||
|
|
||||||
|
return Response::json(['results' => $results]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -987,4 +987,60 @@ final class OrdersRepository
|
|||||||
return $code;
|
return $code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{id:int, order_number:string, buyer_name:string, buyer_email:string, buyer_phone:string}>
|
||||||
|
*/
|
||||||
|
public function quickSearch(string $query, int $limit = 10): array
|
||||||
|
{
|
||||||
|
$query = trim($query);
|
||||||
|
if ($query === '' || mb_strlen($query) < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = max(1, min($limit, 20));
|
||||||
|
$searchVal = '%' . $query . '%';
|
||||||
|
|
||||||
|
$sql = 'SELECT o.id, o.source_order_id, o.external_order_id, '
|
||||||
|
. 'a.name AS buyer_name, a.email AS buyer_email, a.phone AS buyer_phone '
|
||||||
|
. 'FROM orders o '
|
||||||
|
. 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" '
|
||||||
|
. 'WHERE (o.source_order_id LIKE :s1 OR o.external_order_id LIKE :s2 '
|
||||||
|
. 'OR a.name LIKE :s3 OR a.email LIKE :s4 OR a.phone LIKE :s5 '
|
||||||
|
. 'OR EXISTS (SELECT 1 FROM order_items oi WHERE oi.order_id = o.id AND oi.original_name LIKE :s6)) '
|
||||||
|
. 'ORDER BY o.ordered_at DESC LIMIT :lim';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':s1', $searchVal);
|
||||||
|
$stmt->bindValue(':s2', $searchVal);
|
||||||
|
$stmt->bindValue(':s3', $searchVal);
|
||||||
|
$stmt->bindValue(':s4', $searchVal);
|
||||||
|
$stmt->bindValue(':s5', $searchVal);
|
||||||
|
$stmt->bindValue(':s6', $searchVal);
|
||||||
|
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$rows = $stmt->fetchAll();
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(static function (array $row): array {
|
||||||
|
$orderNumber = ((string) ($row['source_order_id'] ?? '')) !== ''
|
||||||
|
? (string) $row['source_order_id']
|
||||||
|
: (string) ($row['external_order_id'] ?? '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) ($row['id'] ?? 0),
|
||||||
|
'order_number' => $orderNumber,
|
||||||
|
'buyer_name' => (string) ($row['buyer_name'] ?? ''),
|
||||||
|
'buyer_email' => (string) ($row['buyer_email'] ?? ''),
|
||||||
|
'buyer_phone' => (string) ($row['buyer_phone'] ?? ''),
|
||||||
|
];
|
||||||
|
}, $rows);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ final class ShopproIntegrationsController
|
|||||||
private readonly AuthService $auth,
|
private readonly AuthService $auth,
|
||||||
private readonly ShopproIntegrationsRepository $repository,
|
private readonly ShopproIntegrationsRepository $repository,
|
||||||
private readonly ShopproStatusMappingRepository $statusMappings,
|
private readonly ShopproStatusMappingRepository $statusMappings,
|
||||||
|
private readonly ShopproPullStatusMappingRepository $pullStatusMappings,
|
||||||
private readonly OrderStatusRepository $orderStatuses,
|
private readonly OrderStatusRepository $orderStatuses,
|
||||||
private readonly CronRepository $cronRepository,
|
private readonly CronRepository $cronRepository,
|
||||||
private readonly CarrierDeliveryMethodMappingRepository $deliveryMappings,
|
private readonly CarrierDeliveryMethodMappingRepository $deliveryMappings,
|
||||||
@@ -71,6 +72,9 @@ final class ShopproIntegrationsController
|
|||||||
$mappingIndex = $integrationId > 0
|
$mappingIndex = $integrationId > 0
|
||||||
? $this->buildMappingIndex($integrationId)
|
? $this->buildMappingIndex($integrationId)
|
||||||
: [];
|
: [];
|
||||||
|
$pullMappingIndex = $integrationId > 0
|
||||||
|
? $this->buildPullMappingIndex($integrationId)
|
||||||
|
: [];
|
||||||
$shopproStatuses = $integrationId > 0
|
$shopproStatuses = $integrationId > 0
|
||||||
? $this->buildExternalStatusOptions($integrationId, $discoveredStatuses)
|
? $this->buildExternalStatusOptions($integrationId, $discoveredStatuses)
|
||||||
: [];
|
: [];
|
||||||
@@ -98,6 +102,7 @@ final class ShopproIntegrationsController
|
|||||||
'statusSyncIntervalMinutes' => $this->currentStatusSyncIntervalMinutes(),
|
'statusSyncIntervalMinutes' => $this->currentStatusSyncIntervalMinutes(),
|
||||||
'paymentSyncIntervalMinutes' => $this->currentPaymentSyncIntervalMinutes(),
|
'paymentSyncIntervalMinutes' => $this->currentPaymentSyncIntervalMinutes(),
|
||||||
'mappingIndex' => $mappingIndex,
|
'mappingIndex' => $mappingIndex,
|
||||||
|
'pullMappingIndex' => $pullMappingIndex,
|
||||||
'orderproStatuses' => $this->orderStatuses->listStatuses(),
|
'orderproStatuses' => $this->orderStatuses->listStatuses(),
|
||||||
'shopproStatuses' => $shopproStatuses,
|
'shopproStatuses' => $shopproStatuses,
|
||||||
'deliveryMappings' => $deliveryMappings,
|
'deliveryMappings' => $deliveryMappings,
|
||||||
@@ -273,6 +278,60 @@ final class ShopproIntegrationsController
|
|||||||
return Response::redirect($redirectTo);
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function savePullStatusMappings(Request $request): Response
|
||||||
|
{
|
||||||
|
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
||||||
|
$redirectTo = $this->buildRedirectUrl($integrationId, 'statuses');
|
||||||
|
|
||||||
|
$accessError = $this->validateCsrfAndIntegrationAccess((string) $request->input('_token', ''), $integrationId, 'statuses');
|
||||||
|
if ($accessError !== null) {
|
||||||
|
return $accessError;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopCodes = $request->input('shoppro_status_code', []);
|
||||||
|
$shopNames = $request->input('shoppro_status_name', []);
|
||||||
|
$orderCodes = $request->input('orderpro_status_code', []);
|
||||||
|
if (!is_array($shopCodes) || !is_array($shopNames) || !is_array($orderCodes)) {
|
||||||
|
Flash::set('settings_error', $this->translator->get('settings.order_statuses.pull.save_failed'));
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedOrderpro = $this->resolveAllowedOrderproStatusCodes();
|
||||||
|
$rowsCount = min(count($shopCodes), count($shopNames), count($orderCodes));
|
||||||
|
$mappings = [];
|
||||||
|
for ($index = 0; $index < $rowsCount; $index++) {
|
||||||
|
$shopCode = trim((string) ($shopCodes[$index] ?? ''));
|
||||||
|
$shopName = trim((string) ($shopNames[$index] ?? ''));
|
||||||
|
$orderCode = strtolower(trim((string) ($orderCodes[$index] ?? '')));
|
||||||
|
|
||||||
|
if ($shopCode === '' || $orderCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($allowedOrderpro[$orderCode])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mappings[] = [
|
||||||
|
'shoppro_status_code' => $shopCode,
|
||||||
|
'shoppro_status_name' => $shopName,
|
||||||
|
'orderpro_status_code' => $orderCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->pullStatusMappings->replaceForIntegration($integrationId, $mappings);
|
||||||
|
Flash::set('settings_success', $this->translator->get('settings.order_statuses.pull.saved'));
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Flash::set(
|
||||||
|
'settings_error',
|
||||||
|
$this->translator->get('settings.order_statuses.pull.save_failed') . ' ' . $exception->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::redirect($redirectTo);
|
||||||
|
}
|
||||||
|
|
||||||
public function syncStatuses(Request $request): Response
|
public function syncStatuses(Request $request): Response
|
||||||
{
|
{
|
||||||
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
$integrationId = max(0, (int) $request->input('integration_id', 0));
|
||||||
@@ -517,6 +576,28 @@ final class ShopproIntegrationsController
|
|||||||
return $index;
|
return $index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{orderpro_status_code:string}>
|
||||||
|
*/
|
||||||
|
private function buildPullMappingIndex(int $integrationId): array
|
||||||
|
{
|
||||||
|
$rows = $this->pullStatusMappings->listByIntegration($integrationId);
|
||||||
|
$index = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$shopproCode = trim((string) ($row['shoppro_status_code'] ?? ''));
|
||||||
|
if ($shopproCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$index[$shopproCode] = [
|
||||||
|
'orderpro_status_code' => strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $index;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array{code:string,name:string}> $discoveredStatuses
|
* @param array<int, array{code:string,name:string}> $discoveredStatuses
|
||||||
* @return array<int, array{code:string,name:string}>
|
* @return array<int, array{code:string,name:string}>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ final class ShopproOrderMapper
|
|||||||
'external_platform_id' => IntegrationSources::SHOPPRO,
|
'external_platform_id' => IntegrationSources::SHOPPRO,
|
||||||
'external_platform_account_id' => null,
|
'external_platform_account_id' => null,
|
||||||
'external_status_id' => $effectiveStatus,
|
'external_status_id' => $effectiveStatus,
|
||||||
'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
|
'external_payment_type_id' => $this->normalizeCodPaymentType((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
|
||||||
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
|
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
|
||||||
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
|
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
|
||||||
'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
|
'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||||
@@ -603,6 +603,16 @@ final class ShopproOrderMapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$message = $this->readPath($row, ['message']);
|
||||||
|
if ($message !== null && $message !== '' && $message !== false) {
|
||||||
|
$text = str_replace(['<br>', '<br/>', '<br />'], "\n", (string) $message);
|
||||||
|
$text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
$text = trim($text);
|
||||||
|
if ($text !== '') {
|
||||||
|
$parts[] = 'Wiadomość: ' . $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $parts !== [] ? implode("\n", $parts) : null;
|
return $parts !== [] ? implode("\n", $parts) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +669,7 @@ final class ShopproOrderMapper
|
|||||||
*/
|
*/
|
||||||
private function mapNotes(array $payload): array
|
private function mapNotes(array $payload): array
|
||||||
{
|
{
|
||||||
$comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
|
$comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment', 'message']));
|
||||||
if ($comment === null) {
|
if ($comment === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -827,6 +837,15 @@ final class ShopproOrderMapper
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeCodPaymentType(string $raw): ?string
|
||||||
|
{
|
||||||
|
$value = StringHelper::nullableString($raw);
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return StringHelper::isCodPayment($value) ? 'CASH_ON_DELIVERY' : $value;
|
||||||
|
}
|
||||||
|
|
||||||
private function readSinglePath(mixed $payload, string $path): mixed
|
private function readSinglePath(mixed $payload, string $path): mixed
|
||||||
{
|
{
|
||||||
if ($path === '') {
|
if ($path === '') {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ final class ShopproOrdersSyncService
|
|||||||
private readonly ShopproStatusMappingRepository $statusMappings,
|
private readonly ShopproStatusMappingRepository $statusMappings,
|
||||||
private readonly OrdersRepository $orders,
|
private readonly OrdersRepository $orders,
|
||||||
private readonly ShopproOrderMapper $mapper,
|
private readonly ShopproOrderMapper $mapper,
|
||||||
private readonly ShopproProductImageResolver $imageResolver
|
private readonly ShopproProductImageResolver $imageResolver,
|
||||||
|
private readonly ?ShopproPullStatusMappingRepository $pullStatusMappings = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,15 +238,21 @@ final class ShopproOrdersSyncService
|
|||||||
$result['imported_updated'] = (int) $result['imported_updated'] + 1;
|
$result['imported_updated'] = (int) $result['imported_updated'] + 1;
|
||||||
}
|
}
|
||||||
$wasCreated = !empty($save['created']);
|
$wasCreated = !empty($save['created']);
|
||||||
|
$wasPaymentTransition = !empty($save['payment_transition']);
|
||||||
$savedOrderId = (int) ($save['order_id'] ?? 0);
|
$savedOrderId = (int) ($save['order_id'] ?? 0);
|
||||||
$summary = $wasCreated
|
if ($wasPaymentTransition) {
|
||||||
? 'Import zamowienia z shopPRO'
|
$summary = 'Platnosc potwierdzona z shopPRO — zmiana statusu na w realizacji';
|
||||||
: 'Zaktualizowano zamowienie z shopPRO (re-import)';
|
} elseif ($wasCreated) {
|
||||||
|
$summary = 'Import zamowienia z shopPRO';
|
||||||
|
} else {
|
||||||
|
$summary = 'Zaktualizowano zamowienie z shopPRO (re-import)';
|
||||||
|
}
|
||||||
$details = [
|
$details = [
|
||||||
'integration_id' => $integrationId,
|
'integration_id' => $integrationId,
|
||||||
'source_order_id' => $sourceOrderId,
|
'source_order_id' => $sourceOrderId,
|
||||||
'source_updated_at' => $sourceUpdatedAt,
|
'source_updated_at' => $sourceUpdatedAt,
|
||||||
'created' => $wasCreated,
|
'created' => $wasCreated,
|
||||||
|
'payment_transition' => $wasPaymentTransition,
|
||||||
'trigger' => 'orders_sync',
|
'trigger' => 'orders_sync',
|
||||||
'trigger_label' => 'Synchronizacja zamowien',
|
'trigger_label' => 'Synchronizacja zamowien',
|
||||||
];
|
];
|
||||||
@@ -297,9 +304,40 @@ final class ShopproOrdersSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string> shoppro_status_code => orderpro_status_code (reverse of DB direction)
|
* @return array<string, string> shoppro_status_code => orderpro_status_code
|
||||||
*/
|
*/
|
||||||
private function buildStatusMap(int $integrationId): array
|
private function buildStatusMap(int $integrationId): array
|
||||||
|
{
|
||||||
|
if ($this->pullStatusMappings !== null) {
|
||||||
|
return $this->buildStatusMapFromPullTable($integrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->buildStatusMapFromPushTable($integrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function buildStatusMapFromPullTable(int $integrationId): array
|
||||||
|
{
|
||||||
|
$rows = $this->pullStatusMappings->listByIntegration($integrationId);
|
||||||
|
$map = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$shopCode = strtolower(trim((string) ($row['shoppro_status_code'] ?? '')));
|
||||||
|
$orderCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? '')));
|
||||||
|
if ($shopCode === '' || $orderCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$map[$shopCode] = $orderCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function buildStatusMapFromPushTable(int $integrationId): array
|
||||||
{
|
{
|
||||||
$rows = $this->statusMappings->listByIntegration($integrationId);
|
$rows = $this->statusMappings->listByIntegration($integrationId);
|
||||||
$map = [];
|
$map = [];
|
||||||
|
|||||||
99
src/Modules/Settings/ShopproPullStatusMappingRepository.php
Normal file
99
src/Modules/Settings/ShopproPullStatusMappingRepository.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
|
use PDO;
|
||||||
|
|
||||||
|
final class ShopproPullStatusMappingRepository
|
||||||
|
{
|
||||||
|
public function __construct(private readonly PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{shoppro_status_code:string,shoppro_status_name:string,orderpro_status_code:string}>
|
||||||
|
*/
|
||||||
|
public function listByIntegration(int $integrationId): array
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT shoppro_status_code, shoppro_status_name, orderpro_status_code
|
||||||
|
FROM order_status_pull_mappings
|
||||||
|
WHERE integration_id = :integration_id
|
||||||
|
ORDER BY shoppro_status_code ASC'
|
||||||
|
);
|
||||||
|
$statement->execute(['integration_id' => $integrationId]);
|
||||||
|
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopproCode = trim((string) ($row['shoppro_status_code'] ?? ''));
|
||||||
|
if ($shopproCode === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'shoppro_status_code' => $shopproCode,
|
||||||
|
'shoppro_status_name' => trim((string) ($row['shoppro_status_name'] ?? '')),
|
||||||
|
'orderpro_status_code' => strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array{shoppro_status_code:string,shoppro_status_name:string,orderpro_status_code:string}> $mappings
|
||||||
|
*/
|
||||||
|
public function replaceForIntegration(int $integrationId, array $mappings): void
|
||||||
|
{
|
||||||
|
if ($integrationId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleteStatement = $this->pdo->prepare(
|
||||||
|
'DELETE FROM order_status_pull_mappings WHERE integration_id = :integration_id'
|
||||||
|
);
|
||||||
|
$deleteStatement->execute(['integration_id' => $integrationId]);
|
||||||
|
|
||||||
|
if ($mappings === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$insertStatement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO order_status_pull_mappings (
|
||||||
|
integration_id, shoppro_status_code, shoppro_status_name, orderpro_status_code, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:integration_id, :shoppro_status_code, :shoppro_status_name, :orderpro_status_code, NOW(), NOW()
|
||||||
|
)'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($mappings as $mapping) {
|
||||||
|
$shopproCode = trim((string) ($mapping['shoppro_status_code'] ?? ''));
|
||||||
|
$orderpro = strtolower(trim((string) ($mapping['orderpro_status_code'] ?? '')));
|
||||||
|
if ($shopproCode === '' || $orderpro === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shopproName = trim((string) ($mapping['shoppro_status_name'] ?? ''));
|
||||||
|
$insertStatement->execute([
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'shoppro_status_code' => $shopproCode,
|
||||||
|
'shoppro_status_name' => $shopproName !== '' ? $shopproName : null,
|
||||||
|
'orderpro_status_code' => $orderpro,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -429,16 +429,16 @@ final class ShipmentController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$result = $delivery;
|
$result = $delivery;
|
||||||
|
$deliveryHasAddress = trim((string) ($delivery['street_name'] ?? '')) !== '';
|
||||||
$deliveryName = trim((string) ($delivery['name'] ?? ''));
|
$deliveryName = trim((string) ($delivery['name'] ?? ''));
|
||||||
$customerName = trim((string) ($customer['name'] ?? ''));
|
$customerName = trim((string) ($customer['name'] ?? ''));
|
||||||
if (($this->isPickupPointDelivery($delivery) || $deliveryName === '') && $customerName !== '') {
|
if ((!$deliveryHasAddress || $this->isPickupPointDelivery($delivery) || $deliveryName === '') && $customerName !== '') {
|
||||||
$result['name'] = $customerName;
|
$result['name'] = $customerName;
|
||||||
}
|
}
|
||||||
if (trim((string) ($result['phone'] ?? '')) === '' && trim((string) ($customer['phone'] ?? '')) !== '') {
|
foreach (['phone', 'email', 'street_name', 'street_number', 'city', 'zip_code', 'country'] as $field) {
|
||||||
$result['phone'] = $customer['phone'];
|
if (trim((string) ($result[$field] ?? '')) === '' && trim((string) ($customer[$field] ?? '')) !== '') {
|
||||||
|
$result[$field] = $customer[$field];
|
||||||
}
|
}
|
||||||
if (trim((string) ($result['email'] ?? '')) === '' && trim((string) ($customer['email'] ?? '')) !== '') {
|
|
||||||
$result['email'] = $customer['email'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|||||||
Reference in New Issue
Block a user