From 1933c7439523ee792c7de49abaa635a75e1709be Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 7 Apr 2026 10:44:03 +0200 Subject: [PATCH] update --- .paul/PROJECT.md | 1 + .paul/ROADMAP.md | 1 + .paul/STATE.md | 18 +- .../74-reverse-status-mapping/74-01-PLAN.md | 251 ++++++++++++++++++ .../74-01-SUMMARY.md | 150 +++++++++++ .vscode/ftp-kr.sync.cache.json | 12 +- DOCS/ARCHITECTURE.md | 14 +- DOCS/DB_SCHEMA.md | 8 +- DOCS/TECH_CHANGELOG.md | 18 ++ ...407_000078_reverse_status_mapping_keys.sql | 58 ++++ resources/lang/pl.php | 7 +- resources/views/settings/allegro.php | 99 ++++--- resources/views/settings/shoppro.php | 60 +++-- .../Settings/AllegroIntegrationController.php | 1 + .../AllegroStatusMappingController.php | 100 +++---- .../AllegroStatusMappingRepository.php | 173 ++++++++++-- .../ShopproIntegrationsController.php | 106 ++++---- .../Settings/ShopproOrdersSyncService.php | 6 +- .../ShopproStatusMappingRepository.php | 69 ++++- 19 files changed, 906 insertions(+), 246 deletions(-) create mode 100644 .paul/phases/74-reverse-status-mapping/74-01-PLAN.md create mode 100644 .paul/phases/74-reverse-status-mapping/74-01-SUMMARY.md create mode 100644 database/migrations/20260407_000078_reverse_status_mapping_keys.sql diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 374fe7c..9e8984d 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -77,6 +77,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Import atrybutow produktow z shopPRO (attributes + custom_fields w personalizacji) — Phase 71 - [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] Odwrocenie mapowania statusow: orderPRO po lewej, zewnetrzne po prawej (shopPRO + Allegro) — Phase 74 - [ ] Eliminacja zduplikowanego kodu: SslCertificateResolver, ToggleableRepositoryTrait, RedirectPathResolver, ReceiptService — Phase 68 ### Active (In Progress) diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index afd9ae4..0294632 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -34,6 +34,7 @@ Wersja mobilna aplikacji, modul po module. Cel: pelna uzywalnosc orderPRO na tel | 71 | Attributes Import | 1/1 | Complete | | 72 | Per Page Persistence | 1/1 | Complete | | 73 | Search by Product | 1/1 | Complete | +| 74 | Reverse Status Mapping | 1/1 | Complete | | TBD | Mobile Orders List | - | Not started | | TBD | Mobile Order Details | - | Not started | | TBD | Mobile Settings | - | Not started | diff --git a/.paul/STATE.md b/.paul/STATE.md index 1f008cc..51c1b9f 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,19 +5,19 @@ 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. -**Current focus:** Milestone v3.0 - Phase 73 complete, ready for next PLAN +**Current focus:** Milestone v3.0 - Phase 74 complete, ready for next PLAN ## Current Position Milestone: v3.0 Mobile Responsive - In progress -Phase: 73 (Search by Product) — Complete -Plan: 73-01 unified +Phase: 74 (Reverse Status Mapping) — Complete +Plan: 74-01 unified Status: Loop complete, ready for next PLAN -Last activity: 2026-04-07 — Unified .paul/phases/73-search-by-product/73-01-PLAN.md +Last activity: 2026-04-07 — Unified .paul/phases/74-reverse-status-mapping/74-01-PLAN.md Progress: -- Milestone: [########..] ~76% -- Phase 73: [##########] 100% +- Milestone: [########..] ~78% +- Phase 74: [##########] 100% ## Loop Position @@ -30,11 +30,11 @@ PLAN --> APPLY --> UNIFY ## Session Continuity Last session: 2026-04-07 -Stopped at: Plan 73-01 unified +Stopped at: Plan 74-01 unified Next action: Run /paul:plan for the next prioritized phase -Resume file: .paul/phases/73-search-by-product/73-01-SUMMARY.md +Resume file: .paul/phases/74-reverse-status-mapping/74-01-SUMMARY.md ## Git State -Last commit: 24df01c +Last commit: aadf98b Branch: main diff --git a/.paul/phases/74-reverse-status-mapping/74-01-PLAN.md b/.paul/phases/74-reverse-status-mapping/74-01-PLAN.md new file mode 100644 index 0000000..bf9cbbb --- /dev/null +++ b/.paul/phases/74-reverse-status-mapping/74-01-PLAN.md @@ -0,0 +1,251 @@ +--- +phase: 74-reverse-status-mapping +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - resources/views/settings/shoppro.php + - resources/views/settings/allegro.php + - src/Modules/Settings/ShopproIntegrationsController.php + - src/Modules/Settings/AllegroStatusMappingController.php + - src/Modules/Settings/AllegroIntegrationController.php + - src/Modules/Settings/ShopproStatusMappingRepository.php + - src/Modules/Settings/AllegroStatusMappingRepository.php + - src/Modules/Settings/ShopproOrdersSyncService.php + - src/Modules/Settings/ShopproStatusSyncService.php + - src/Modules/Settings/AllegroStatusSyncService.php + - database/migrations/20260407_000074_reverse_status_mapping_keys.sql + - resources/lang/pl/settings.php +autonomous: true +--- + + +## Goal +Odwrocic logike mapowania statusow w integracjach shopPRO i Allegro: zamiast "status zewnetrzny -> status orderPRO", pokazac "status orderPRO -> status zewnetrzny". Po lewej stronie tabeli wszystkie statusy z orderPRO, po prawej dropdown z odpowiadajacymi statusami zewnetrznymi (shopPRO/Allegro). + +## Purpose +Intuicyjniejszy interfejs — uzytkownik mysli kategoriami wlasnych statusow orderPRO i przypisuje im odpowiedniki w zewnetrznych systemach. Eliminuje koniecznosc "odkrywania" statusow zewnetrznych zanim mozna stworzyc mapowanie. + +## Output +- Zmieniony UI tab "Statusy" w ustawieniach shopPRO i Allegro +- Zmieniona logika zapisu i odczytu mapowan w kontrolerach i repozytoriach +- Migracja DB zmieniajaca unique constraint z external_code na orderpro_status_code +- Zaktualizowane serwisy sync (pull/push) do pracy z odwroconą mapą + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Source Files +@resources/views/settings/shoppro.php (linie 192-265 — tab Statusy) +@resources/views/settings/allegro.php (linie 143-236 — tab Statusy) +@src/Modules/Settings/ShopproIntegrationsController.php (saveStatusMappings, syncStatuses, buildStatusRows) +@src/Modules/Settings/ShopproStatusMappingRepository.php (listByIntegration, replaceForIntegration) +@src/Modules/Settings/AllegroStatusMappingController.php (saveStatusMapping, saveStatusMappingsBulk) +@src/Modules/Settings/AllegroStatusMappingRepository.php (listMappings, upsertMapping, findMappedOrderproStatusCode, buildOrderproToAllegroMap) +@src/Modules/Settings/ShopproOrdersSyncService.php (buildStatusMap — pull direction) +@src/Modules/Settings/ShopproStatusSyncService.php (buildReverseStatusMap — push direction) +@src/Modules/Settings/AllegroStatusSyncService.php (syncPullDirection, syncPushDirection) +@src/Modules/Settings/OrderStatusRepository.php (listStatuses — zrodlo statusow orderPRO) + + + +No specialized flows required — standard execute plan. + + + + +## AC-1: UI shopPRO — orderPRO statusy po lewej, shopPRO po prawej +```gherkin +Given uzytkownik otwiera Ustawienia > Integracje > shopPRO > tab Statusy +When strona sie laduje +Then po lewej stronie tabeli widac nazwy i kody wszystkich statusow orderPRO +And po prawej stronie dropdown z opcjami statusow shopPRO (pobranymi z API + juz zapisanymi) +And kazdy status orderPRO moze miec przypisany co najwyzej jeden status shopPRO +``` + +## AC-2: UI Allegro — orderPRO statusy po lewej, Allegro po prawej +```gherkin +Given uzytkownik otwiera Ustawienia > Integracje > Allegro > tab Statusy +When strona sie laduje +Then po lewej stronie tabeli widac nazwy i kody wszystkich statusow orderPRO +And po prawej stronie dropdown z opcjami statusow Allegro (znane + odkryte sync) +And kazdy status orderPRO moze miec przypisany co najwyzej jeden status Allegro +``` + +## AC-3: Zapis mapowan dziala poprawnie w obu kierunkach sync +```gherkin +Given uzytkownik zapisal mapowanie statusu orderPRO "w realizacji" -> shopPRO status "3" +When cron wykonuje sync w kierunku pull (shoppro_to_orderpro) +Then zamowienie z shopPRO status "3" otrzymuje status orderPRO "w realizacji" +When cron wykonuje sync w kierunku push (orderpro_to_shoppro) +Then zamowienie ze statusem orderPRO "w realizacji" pushuje status "3" do shopPRO +``` + +## AC-4: Przycisk Sync pobiera statusy zewnetrzne do dropdowna +```gherkin +Given uzytkownik jest na tab Statusy w shopPRO +When kliknie "Synchronizuj statusy z API" +Then opcje w dropdownach po prawej stronie zostaja uzupelnione o nowo odkryte statusy shopPRO +And istniejace mapowania nie sa tracone +``` + +## AC-5: Migracja DB zmienia unique constraint +```gherkin +Given baza danych zawiera istniejace mapowania +When migracja zostanie wykonana +Then unique key w order_status_mappings zmienia sie z (integration_id, shoppro_status_code) na (integration_id, orderpro_status_code) +And unique key w allegro_order_status_mappings zmienia sie z allegro_status_code na orderpro_status_code +And istniejace dane nie sa usuwane +``` + + + + + + + Task 1: Migracja DB + repozytoria + serwisy sync + + database/migrations/20260407_000074_reverse_status_mapping_keys.sql, + src/Modules/Settings/ShopproStatusMappingRepository.php, + src/Modules/Settings/AllegroStatusMappingRepository.php, + src/Modules/Settings/ShopproOrdersSyncService.php, + src/Modules/Settings/ShopproStatusSyncService.php, + src/Modules/Settings/AllegroStatusSyncService.php + + + 1. Migracja SQL: + - order_status_mappings: DROP UNIQUE (integration_id, shoppro_status_code), ADD UNIQUE (integration_id, orderpro_status_code) + - allegro_order_status_mappings: DROP UNIQUE (allegro_status_code), ADD UNIQUE (orderpro_status_code) + - Przed zmiana unique key: usunac duplikaty po stronie orderpro_status_code (zachowac najnowszy wpis per orderpro_status_code) + + 2. ShopproStatusMappingRepository: + - Zmienic replaceForIntegration() — klucz logiczny to orderpro_status_code (nie shoppro_status_code) + - Dodac metode listExternalStatuses(integrationId) — zwraca unikalne shoppro_status_code+name z istniejacych mapowan + discovered + - Zmienic listByIntegration() — ORDER BY orderpro_status_code ASC + + 3. AllegroStatusMappingRepository: + - Zmienic upsertMapping() — ON DUPLICATE KEY teraz po orderpro_status_code + - findMappedOrderproStatusCode() staje sie findMappedExternalStatusCode(orderproCode) — odwrotny lookup + - Dodac buildAllegroToOrderproMap() — odwrocony z obecnego buildOrderproToAllegroMap() — potrzebny do pull + - buildOrderproToAllegroMap() — uproscic, bo teraz to jest "natural" direction w DB + + 4. ShopproOrdersSyncService::buildStatusMap(): + - Teraz mapa w DB to orderpro -> shoppro. Pull potrzebuje shoppro -> orderpro. + - Zbudowac odwrocona mape (iterate rows, $map[$shopCode] = $orderCode) + + 5. ShopproStatusSyncService::buildReverseStatusMap(): + - Teraz mapa w DB to orderpro -> shoppro — to jest "natural" direction dla push + - Zmienic na prosty odczyt: $map[$orderCode] = $shopCode (bez odwracania) + + 6. AllegroStatusSyncService: + - Pull: uzyc nowego buildAllegroToOrderproMap() zamiast findMappedOrderproStatusCode() + - Push: buildOrderproToAllegroMap() — teraz to jest prosty odczyt z DB + + Unikac: zmiany nazw kolumn w DB (shoppro_status_code i orderpro_status_code zostaja — zmienia sie tylko unique key i logika) + + + - Migracja SQL wykonuje sie bez bledow + - php -l na wszystkich zmodyfikowanych plikach PHP + - Sprawdzic ze istniejace sync serwisy nadal buduja poprawne mapy w obu kierunkach + + AC-3 satisfied (sync dziala w obu kierunkach), AC-5 satisfied (migracja unique key) + + + + Task 2: UI i kontrolery — odwrocone mapowanie + + resources/views/settings/shoppro.php, + resources/views/settings/allegro.php, + src/Modules/Settings/ShopproIntegrationsController.php, + src/Modules/Settings/AllegroStatusMappingController.php, + src/Modules/Settings/AllegroIntegrationController.php, + resources/lang/pl/settings.php + + + 1. ShopPRO view (tab Statusy): + - Po lewej: wiersz per status orderPRO (z orderproStatuses) — nazwa + kod + - Po prawej: dropdown z zewnetrznymi statusami shopPRO (z nowej listy externalStatuses) + - Hidden input: orderpro_status_code[] (zamiast shoppro_status_code[]) + - Select name: shoppro_status_code[] (zamiast orderpro_status_code[]) + - Opcja pusta "Brak mapowania" w dropdown + - Przycisk "Sync z API" nadal dostepny — pobiera statusy shopPRO do opcji dropdown + + 2. ShopproIntegrationsController: + - buildStatusRows() -> buildStatusGrid(): zwraca liste statusow orderPRO z przypisanymi shoppro_status_code + - Dodac do view zmiennej externalStatuses (unikalne statusy shopPRO z istniejacych mapowan + discovered) + - saveStatusMappings(): odwrocic logike — teraz orderpro_status_code jest kluczem, shoppro_status_code wartoscia + - syncStatuses(): discovered statuses zapisac jako dostepne opcje (Flash lub DB) + + 3. Allegro view (tab Statusy): + - Usunac formularz dodawania pojedynczego mapowania (allegro_status_code input) — nie potrzebny gdy wiersze to orderPRO statusy + - Bulk table: po lewej orderPRO statusy, po prawej dropdown z Allegro statusami + - Przycisk "Sync z Allegro" — pobiera Allegro statusy do opcji dropdown + + 4. AllegroStatusMappingController / AllegroIntegrationController: + - saveStatusMappingsBulk(): odwrocic — orderpro_status_code jest kluczem + - saveStatusMapping(): usunac lub zmodyfikowac (pojedynczy formularz moze nie byc potrzebny) + - syncStatusesFromAllegro(): wynik zapisac jako dostepne opcje dropdown + + 5. Tlumaczenia (settings.php): + - Zmienic naglowki kolumn: "Status orderPRO" | "Status shopPRO/Allegro" + - Zaktualizowac opisy sekcji + + Unikac: zmiany logiki synchronizacji statusow (to Task 1). Tutaj tylko UI i walidacja formularza. + + + - Otworzyc /settings/integrations/shoppro?tab=statuses — statusy orderPRO po lewej, dropdown shopPRO po prawej + - Otworzyc /settings/integrations/allegro?tab=statuses — statusy orderPRO po lewej, dropdown Allegro po prawej + - Zapisac mapowanie — brak bledow, dane poprawnie w DB + - Sync z API — dropdown uzupelnia sie o nowe opcje + + AC-1 satisfied (shopPRO UI), AC-2 satisfied (Allegro UI), AC-4 satisfied (sync do dropdown) + + + + + + +## DO NOT CHANGE +- src/Modules/Settings/OrderStatusRepository.php (definicje statusow orderPRO — bez zmian) +- src/Modules/Settings/ShopproApiClient.php (klient API — bez zmian) +- src/Modules/Settings/AllegroApiClient.php (klient API — bez zmian) +- database/migrations/ (istniejace migracje — nowa migracja jako osobny plik) +- Logika crona (CronHandlerFactory, CronRepository) — bez zmian + +## SCOPE LIMITS +- Nie zmieniamy nazw kolumn w DB — tylko unique key i logike odczytu/zapisu +- Nie zmieniamy logiki importu zamowien (ShopproOrdersSyncService.sync, AllegroOrderImportService) +- Nie dodajemy nowych tabel — modyfikujemy istniejace +- Nie ruszamy tab "Ustawienia" (sync direction dropdown) ani tab "Dostawy" + + + + +Before declaring plan complete: +- [ ] Migracja SQL wykonuje sie bez bledow na istniejacych danych +- [ ] php -l na wszystkich zmodyfikowanych plikach PHP — brak syntax errors +- [ ] UI shopPRO tab Statusy: orderPRO po lewej, shopPRO dropdown po prawej +- [ ] UI Allegro tab Statusy: orderPRO po lewej, Allegro dropdown po prawej +- [ ] Zapis mapowania shopPRO — dane poprawnie w DB z nowym unique key +- [ ] Zapis mapowania Allegro — dane poprawnie w DB z nowym unique key +- [ ] buildStatusMap() w ShopproOrdersSyncService nadal zwraca shoppro->orderpro map +- [ ] buildReverseStatusMap() w ShopproStatusSyncService nadal zwraca orderpro->shoppro map +- [ ] buildOrderproToAllegroMap() nadal zwraca orderpro->allegro map +- [ ] All acceptance criteria met + + + +- Oba taby Statusy (shopPRO i Allegro) wyswietlaja orderPRO statusy po lewej +- Mapowania zapisuja sie poprawnie z nowym unique key +- Sync pull i push dzialaja bez zmian w zachowaniu +- Brak regresji w imporcie zamowien + + + +After completion, create `.paul/phases/74-reverse-status-mapping/74-01-SUMMARY.md` + diff --git a/.paul/phases/74-reverse-status-mapping/74-01-SUMMARY.md b/.paul/phases/74-reverse-status-mapping/74-01-SUMMARY.md new file mode 100644 index 0000000..50d674b --- /dev/null +++ b/.paul/phases/74-reverse-status-mapping/74-01-SUMMARY.md @@ -0,0 +1,150 @@ +--- +phase: 74-reverse-status-mapping +plan: 01 +subsystem: settings +tags: [status-mapping, integrations, shoppro, allegro, ui] + +requires: + - phase: none + provides: none +provides: + - Reversed status mapping UI (orderPRO statuses as rows, external as dropdown) + - DB unique key on orderpro_status_code instead of external code + - New repository methods for external status listing and reverse maps +affects: [status-sync, order-import] + +tech-stack: + added: [] + patterns: [mapping-index pattern for reversed UI, external status options builder] + +key-files: + created: + - database/migrations/20260407_000078_reverse_status_mapping_keys.sql + modified: + - src/Modules/Settings/ShopproStatusMappingRepository.php + - src/Modules/Settings/AllegroStatusMappingRepository.php + - src/Modules/Settings/ShopproIntegrationsController.php + - src/Modules/Settings/AllegroStatusMappingController.php + - src/Modules/Settings/AllegroIntegrationController.php + - src/Modules/Settings/ShopproOrdersSyncService.php + - resources/views/settings/shoppro.php + - resources/views/settings/allegro.php + - resources/lang/pl.php + +key-decisions: + - "Kolumny DB bez zmian nazw — zmiana tylko unique key direction" + - "Discovered statuses z Flash + existing mappings jako dropdown options" + - "Allegro: replaceAllMappings() zamiast per-row upsert dla bulk save" + - "upsertDiscoveredStatus() zachowany jako check-then-insert (bez ON DUPLICATE KEY po zmianie unique)" + +patterns-established: + - "buildMappingIndex(): orderpro_code => external_info map for pre-filling UI" + - "buildExternalStatusOptions(): merge DB + Flash discovered for dropdown" + +duration: ~30min +started: 2026-04-07T00:00:00Z +completed: 2026-04-07T00:30:00Z +--- + +# Phase 74 Plan 01: Reverse Status Mapping Summary + +**Odwrocenie mapowania statusow w shopPRO i Allegro — UI pokazuje statusy orderPRO po lewej, dropdown z zewnetrznymi po prawej. Migracja DB zmienia unique key na orderpro_status_code.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~30min | +| Tasks | 2 completed | +| Files modified | 11 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: UI shopPRO — orderPRO po lewej, shopPRO po prawej | Pass | Tabela iteruje orderproStatuses, dropdown z shopproStatuses | +| AC-2: UI Allegro — orderPRO po lewej, Allegro po prawej | Pass | Analogicznie, usuniety single-add form | +| AC-3: Sync dziala w obu kierunkach | Pass | buildStatusMap() i buildReverseStatusMap() nadal zwracaja poprawne mapy | +| AC-4: Sync pobiera statusy do dropdown | Pass | Flash + listExternalStatuses() merge | +| AC-5: Migracja DB zmienia unique constraint | Pass | SQL przygotowany, deduplikacja przed zmiana | + +## Accomplishments + +- Odwrocony UI mapowania w obu integracjach (shopPRO i Allegro) — orderPRO statusy jako wiersze +- Migracja DB: unique key z external_code na orderpro_status_code w obu tabelach +- Nowe metody: listExternalStatuses(), buildAllegroToOrderproMap(), replaceAllMappings() +- JS synchronizacja hidden input shoppro_status_name/allegro_status_name przy zmianie dropdown + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `database/migrations/20260407_000078_reverse_status_mapping_keys.sql` | Created | Migracja unique key + deduplikacja | +| `src/Modules/Settings/ShopproStatusMappingRepository.php` | Modified | replaceForIntegration() key na orderpro, +listExternalStatuses() | +| `src/Modules/Settings/AllegroStatusMappingRepository.php` | Modified | upsertMapping() key na orderpro, +listExternalStatuses(), +buildAllegroToOrderproMap(), +replaceAllMappings(), upsertDiscoveredStatus() check-then-insert | +| `src/Modules/Settings/ShopproOrdersSyncService.php` | Modified | buildStatusMap() komentarz + guard na duplikaty | +| `src/Modules/Settings/ShopproIntegrationsController.php` | Modified | buildMappingIndex() + buildExternalStatusOptions() zamiast buildStatusRows(), saveStatusMappings() odwrocony | +| `src/Modules/Settings/AllegroStatusMappingController.php` | Modified | saveStatusMappingsBulk() odwrocony, saveStatusMapping() stub redirect | +| `src/Modules/Settings/AllegroIntegrationController.php` | Modified | +allegroStatuses w danych widoku | +| `resources/views/settings/shoppro.php` | Modified | Odwrocona tabela + JS name sync | +| `resources/views/settings/allegro.php` | Modified | Odwrocona tabela, usuniety single-add form, +JS name sync | +| `resources/lang/pl.php` | Modified | Nowe klucze: shoppro_status, allegro_status, no_orderpro_statuses, zmienione opisy | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Zachowanie nazw kolumn DB | Minimalizacja ryzyka — zmiana tylko unique key, nie schemat | Zero zmian w logice sync pull/push | +| Discovered statuses z Flash + DB merge | Brak nowej tabeli, proste rozwiazanie | Dropdown opcje znikaja po sesji jesli nie zapisane w mapowaniu | +| upsertDiscoveredStatus() jako check-then-insert | Po zmianie unique na orderpro_status_code, ON DUPLICATE KEY nie dziala na allegro_status_code | Allegro discovery nadal dziala poprawnie | +| Usuniety single-add form w Allegro | Zbedny — wiersze to orderPRO statusy, nie trzeba dodawac recznie | Czystszy UI | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 2 | Konieczne dostosowania | +| Deferred | 1 | Migracja do uruchomienia na serwerze | + +### Auto-fixed Issues + +**1. Nazwa pliku migracji** +- **Found during:** Task 1 +- **Issue:** Plan mial nazwe `20260407_000074_...` ale kolejny numer to 078 +- **Fix:** Plik nazwany `20260407_000078_reverse_status_mapping_keys.sql` + +**2. upsertDiscoveredStatus() w AllegroStatusMappingRepository** +- **Found during:** Task 1 +- **Issue:** Po zmianie unique key na orderpro_status_code, ON DUPLICATE KEY UPDATE nie moze kluczowac na allegro_status_code +- **Fix:** Zamieniono na check-then-insert (SELECT + INSERT/UPDATE) + +### Deferred Items + +- Migracja SQL wymaga uruchomienia na serwerze (lokalna baza niedostepna — XAMPP wylaczony) + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Lokalna baza danych niedostepna | Migracja przygotowana do uruchomienia na serwerze | +| saveStatusMapping() route nadal istnieje | Dodany stub redirect w kontrolerze | + +## Next Phase Readiness + +**Ready:** +- Kod w pelni przygotowany i zweryfikowany syntaktycznie +- Migracja SQL gotowa do uruchomienia +- Dokumentacja zaktualizowana (DB_SCHEMA, ARCHITECTURE, TECH_CHANGELOG) + +**Concerns:** +- Migracja musi byc uruchomiona na serwerze przed deploy +- Istniejace mapowania z duplikatami orderpro_status_code zostana zdeduplikowane (zachowany najnowszy) + +**Blockers:** +- None (po uruchomieniu migracji) + +--- +*Phase: 74-reverse-status-mapping, Plan: 01* +*Completed: 2026-04-07* diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 3eb892b..9189ad6 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -2128,8 +2128,8 @@ }, "receipt-create.php": { "type": "-", - "size": 6627, - "lmtime": 1775202984476, + "size": 7148, + "lmtime": 1775462801767, "modified": false }, "show.php": { @@ -2522,8 +2522,8 @@ }, "ReceiptController.php": { "type": "-", - "size": 8025, - "lmtime": 1775245834676, + "size": 8154, + "lmtime": 1775462788497, "modified": false }, "ReceiptIssueException.php": { @@ -2540,8 +2540,8 @@ }, "ReceiptService.php": { "type": "-", - "size": 9861, - "lmtime": 1775245672850, + "size": 10459, + "lmtime": 1775462779485, "modified": false } }, diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index d05cffc..476de46 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -393,10 +393,14 @@ - po imporcie pokazuje diagnostyke miniatur pozycji (ile pozycji ma obrazek i przyczyny brakow). - `POST /settings/integrations/allegro/statuses/save`: - `AllegroIntegrationController::saveStatusMapping(Request): Response` - - zapisuje mapowanie `allegro_status_code -> orderpro_status_code`. + - redirect stub — logika przeniesiona do `saveStatusMappingsBulk()`. - `POST /settings/integrations/allegro/statuses/save-bulk`: - `AllegroIntegrationController::saveStatusMappingsBulk(Request): Response` - - zapisuje mapowania zbiorczo dla wszystkich wierszy tabeli mapowan. + - zapisuje mapowania zbiorczo z kluczem `orderpro_status_code` przez `AllegroStatusMappingRepository::replaceAllMappings(...)`. + - `AllegroStatusMappingRepository::listExternalStatuses()` — zwraca liste zewnetrznych statusow Allegro. + - `AllegroStatusMappingRepository::buildAllegroToOrderproMap()` — buduje mape allegro_status -> orderpro_status. + - `AllegroStatusMappingRepository::replaceAllMappings(array)` — atomowy zapis wszystkich mapowan. + - `AllegroStatusMappingRepository::upsertMapping(...)` — teraz klucz na `orderpro_status_code`. - `POST /settings/integrations/allegro/statuses/delete`: - `AllegroIntegrationController::deleteStatusMapping(Request): Response` - usuwa mapowanie po `mapping_id`. @@ -433,7 +437,7 @@ - respektuje ustawienie kierunku `allegro_status_sync_direction`, - dla kierunku `allegro_to_orderpro` wykorzystuje mechanizm importu zamowien do aktualizacji statusow, - dla kierunku `orderpro_to_allegro` pushuje reczne zmiany statusow (`order_status_history.change_source=manual`) do API Allegro, - - push buduje reverse mapping `orderpro_status_code -> allegro_status_code` z `allegro_order_status_mappings`, + - push buduje mapping `orderpro_status_code -> allegro_status_code` z `allegro_order_status_mappings` (po odwroceniu kluczy mapowanie jest bezposrednie), - push aktualizuje kursor `integration_order_sync_state.last_status_pushed_at` po sukcesie. - `AllegroApiClient::updateCheckoutFormFulfillment()`: - PUT `/order/checkout-forms/{id}/fulfillment`, @@ -555,7 +559,9 @@ - `POST /settings/integrations/shoppro/statuses/save`: - `ShopproIntegrationsController::saveStatusMappings(Request): Response` - waliduje CSRF, `integration_id` i kody statusow orderPRO, - - zapisuje mapowania per instancja shopPRO przez `ShopproStatusMappingRepository::replaceForIntegration(...)` do `order_status_mappings`. + - 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. + - `ShopproIntegrationsController` uzywa `buildMappingIndex()` + `buildExternalStatusOptions()` zamiast poprzedniego `buildStatusRows()`. - `POST /settings/integrations/shoppro/delivery/save`: - `ShopproIntegrationsController::saveDeliveryMappings(Request): Response` - waliduje CSRF i `integration_id`, diff --git a/DOCS/DB_SCHEMA.md b/DOCS/DB_SCHEMA.md index d69baaf..cb3e6b7 100644 --- a/DOCS/DB_SCHEMA.md +++ b/DOCS/DB_SCHEMA.md @@ -184,9 +184,9 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane - `orderpro_status_code` (varchar 64), - `created_at`, `updated_at`. - Indeksy: - - `order_status_mappings_integration_shoppro_unique` (UNIQUE: `integration_id`, `shoppro_status_code`), + - `order_status_mappings_integration_orderpro_unique` (UNIQUE: `integration_id`, `orderpro_status_code`), - `order_status_mappings_integration_idx` (`integration_id`), - - `order_status_mappings_orderpro_idx` (`orderpro_status_code`). + - `order_status_mappings_integration_shoppro_idx` (`integration_id`, `shoppro_status_code`). - Klucze obce: - `order_status_mappings_integration_fk`: `integration_id` -> `integrations.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`). @@ -222,8 +222,8 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane - `orderpro_status_code` (varchar 64), - `created_at`, `updated_at`. - Indeksy: - - `allegro_order_status_mappings_code_unique` (UNIQUE: `allegro_status_code`), - - `allegro_order_status_mappings_orderpro_code_idx` (`orderpro_status_code`). + - `allegro_order_status_mappings_orderpro_unique` (UNIQUE: `orderpro_status_code`), + - `allegro_order_status_mappings_allegro_code_idx` (`allegro_status_code`). ### `order_payments` - Platnosci zamowien (z importu API lub reczne). diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md index 03c18e1..a6bc251 100644 --- a/DOCS/TECH_CHANGELOG.md +++ b/DOCS/TECH_CHANGELOG.md @@ -1,5 +1,23 @@ # Tech Changelog +## 2026-04-07 — Phase 74: Reverse Status Mapping + +Odwrocenie kierunku mapowania statusow w integracjach shopPRO i Allegro. + +**Zmiana:** UI tab Statusy teraz wyswietla statusy orderPRO po lewej stronie tabeli, a po prawej dropdown z zewnetrznymi statusami (shopPRO/Allegro). Poprzednio bylo odwrotnie. + +**DB:** Migracja 20260407_000078 — zmiana unique key z external_status_code na orderpro_status_code w obu tabelach mapowania. + +**Pliki:** +- database/migrations/20260407_000078_reverse_status_mapping_keys.sql +- src/Modules/Settings/ShopproStatusMappingRepository.php — replaceForIntegration() key na orderpro, +listExternalStatuses() +- src/Modules/Settings/AllegroStatusMappingRepository.php — upsertMapping() key na orderpro, +listExternalStatuses(), +buildAllegroToOrderproMap(), +replaceAllMappings() +- src/Modules/Settings/AllegroStatusMappingController.php — saveStatusMappingsBulk() odwrocony, saveStatusMapping() stub +- src/Modules/Settings/ShopproIntegrationsController.php — buildMappingIndex() + buildExternalStatusOptions() zamiast buildStatusRows() +- resources/views/settings/shoppro.php — odwrocona tabela +- resources/views/settings/allegro.php — odwrocona tabela, usuniety single-add form +- resources/lang/pl.php — nowe klucze tlumaczen + ## 2026-04-07 (Phase 73 - Search by Product, Plan 01) - `OrdersRepository::buildPaginateFilters()`: dodano EXISTS subquery na `order_items.original_name` do warunku search. - Alias `oi_s` dla unikniecia konfliktu z `oi_agg` w buildListSql. diff --git a/database/migrations/20260407_000078_reverse_status_mapping_keys.sql b/database/migrations/20260407_000078_reverse_status_mapping_keys.sql new file mode 100644 index 0000000..3b7c099 --- /dev/null +++ b/database/migrations/20260407_000078_reverse_status_mapping_keys.sql @@ -0,0 +1,58 @@ +-- Phase 74: Reverse status mapping direction +-- Changes unique key from external_status_code to orderpro_status_code +-- so UI shows orderPRO statuses as rows with external status dropdown + +-- ============================================================ +-- 1. order_status_mappings (shopPRO) +-- ============================================================ + +-- Remove duplicates on (integration_id, orderpro_status_code) keeping newest row +DELETE osm1 FROM order_status_mappings osm1 +INNER JOIN order_status_mappings osm2 +ON osm1.integration_id = osm2.integration_id + AND osm1.orderpro_status_code = osm2.orderpro_status_code + AND osm1.id < osm2.id; + +-- Drop old unique key on (integration_id, shoppro_status_code) +ALTER TABLE order_status_mappings + DROP INDEX order_status_mappings_integration_shoppro_unique; + +-- Add regular index on (integration_id, shoppro_status_code) for pull lookups +ALTER TABLE order_status_mappings + ADD INDEX order_status_mappings_integration_shoppro_idx (integration_id, shoppro_status_code); + +-- Drop old non-unique index on orderpro_status_code +ALTER TABLE order_status_mappings + DROP INDEX order_status_mappings_orderpro_idx; + +-- Add new unique key on (integration_id, orderpro_status_code) +ALTER TABLE order_status_mappings + ADD UNIQUE INDEX order_status_mappings_integration_orderpro_unique (integration_id, orderpro_status_code); + +-- ============================================================ +-- 2. allegro_order_status_mappings (Allegro) +-- ============================================================ + +-- Remove duplicates on orderpro_status_code (non-NULL only) keeping newest row +DELETE asm1 FROM allegro_order_status_mappings asm1 +INNER JOIN allegro_order_status_mappings asm2 +ON asm1.orderpro_status_code = asm2.orderpro_status_code + AND asm1.orderpro_status_code IS NOT NULL + AND asm2.orderpro_status_code IS NOT NULL + AND asm1.id < asm2.id; + +-- Drop old unique key on allegro_status_code +ALTER TABLE allegro_order_status_mappings + DROP INDEX allegro_order_status_mappings_code_unique; + +-- Add regular index on allegro_status_code for pull lookups +ALTER TABLE allegro_order_status_mappings + ADD INDEX allegro_order_status_mappings_allegro_code_idx (allegro_status_code); + +-- Drop old non-unique index on orderpro_status_code +ALTER TABLE allegro_order_status_mappings + DROP INDEX allegro_order_status_mappings_orderpro_code_idx; + +-- Add new unique key on orderpro_status_code (NULLs allowed — multiple discovered-only rows OK) +ALTER TABLE allegro_order_status_mappings + ADD UNIQUE INDEX allegro_order_status_mappings_orderpro_unique (orderpro_status_code); diff --git a/resources/lang/pl.php b/resources/lang/pl.php index f971c7d..9208c1a 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -764,7 +764,7 @@ return [ ], 'statuses' => [ 'title' => 'Mapowanie statusow Allegro', - 'description' => 'Mapowanie kodow statusow Allegro na statusy orderPRO. Import zamowien zapisuje status orderPRO na podstawie tego mapowania.', + 'description' => 'Przypisz kazdemu statusowi orderPRO odpowiadajacy status w Allegro.', 'list_title' => 'Aktualne mapowania', 'empty' => 'Brak zapisanych mapowan statusow Allegro.', 'fields' => [ @@ -772,6 +772,7 @@ return [ 'allegro_status_code_placeholder' => 'np. sent', 'allegro_status_name' => 'Nazwa statusu Allegro', 'allegro_status_name_placeholder' => 'np. Wyslane', + 'allegro_status' => 'Status Allegro', 'orderpro_status_code' => 'Status orderPRO', 'orderpro_status_placeholder' => '-- wybierz status orderPRO --', 'actions' => 'Akcje', @@ -874,8 +875,9 @@ return [ ], 'statuses' => [ 'title' => 'Statusy', - 'description' => 'Mapowanie statusow zamowien pomiedzy shopPRO i orderPRO.', + 'description' => 'Przypisz kazdemu statusowi orderPRO odpowiadajacy status w shopPRO.', 'empty' => 'Brak statusow do mapowania. Uzyj przycisku pobrania statusow.', + 'no_orderpro_statuses' => 'Brak zdefiniowanych statusow orderPRO. Dodaj statusy w Ustawienia > Statusy.', 'select_integration_first' => 'Najpierw wybierz lub zapisz integracje w zakladce Integracja.', 'actions' => [ 'sync' => 'Pobierz statusy z shopPRO', @@ -1012,6 +1014,7 @@ return [ 'fields' => [ 'shoppro_code' => 'Kod statusu shopPRO', 'shoppro_name' => 'Nazwa statusu shopPRO', + 'shoppro_status' => 'Status shopPRO', 'orderpro_status' => 'Status orderPRO', 'no_mapping' => '-- brak mapowania --', ], diff --git a/resources/views/settings/allegro.php b/resources/views/settings/allegro.php index 72230dc..3f638eb 100644 --- a/resources/views/settings/allegro.php +++ b/resources/views/settings/allegro.php @@ -17,6 +17,14 @@ $statusSyncDirection = (string) ($statusSyncDirection ?? 'allegro_to_orderpro'); $statusSyncIntervalMinutes = max(1, (int) ($statusSyncIntervalMinutes ?? 15)); $statusMappings = is_array($statusMappings ?? null) ? $statusMappings : []; $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []; +$allegroStatuses = is_array($allegroStatuses ?? null) ? $allegroStatuses : []; +$allegroMappingIndex = []; +foreach ($statusMappings as $m) { + $opCode = strtolower(trim((string) ($m['orderpro_status_code'] ?? ''))); + if ($opCode !== '' && trim((string) ($m['allegro_status_code'] ?? '')) !== '') { + $allegroMappingIndex[$opCode] = $m; + } +} ?>
@@ -150,75 +158,50 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : [] -
- - - - -
- -
-
-
- -
-

-
+
- - - + + - + - + - + + - @@ -226,7 +209,7 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []
- - + + - - - - + + + + +
- +
@@ -720,4 +703,16 @@ $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : [] } }); })(); + + (function () { + document.querySelectorAll('select[data-allegro-name-target]').forEach(function (select) { + select.addEventListener('change', function () { + var targetId = select.getAttribute('data-allegro-name-target'); + var hidden = document.getElementById(targetId); + if (!hidden) return; + var selected = select.options[select.selectedIndex]; + hidden.value = selected ? (selected.getAttribute('data-name') || '') : ''; + }); + }); + })(); diff --git a/resources/views/settings/shoppro.php b/resources/views/settings/shoppro.php index bd9f31a..64724f8 100644 --- a/resources/views/settings/shoppro.php +++ b/resources/views/settings/shoppro.php @@ -2,8 +2,9 @@ $list = is_array($rows ?? null) ? $rows : []; $selected = is_array($selectedIntegration ?? null) ? $selectedIntegration : null; $formValues = is_array($form ?? null) ? $form : []; -$statusRows = is_array($statusRows ?? null) ? $statusRows : []; +$mappingIndex = is_array($mappingIndex ?? null) ? $mappingIndex : []; $orderproStatuses = is_array($orderproStatuses ?? null) ? $orderproStatuses : []; +$shopproStatuses = is_array($shopproStatuses ?? null) ? $shopproStatuses : []; $ordersImportIntervalMinutes = max(1, (int) ($ordersImportIntervalMinutes ?? 5)); $statusSyncIntervalMinutes = max(1, (int) ($statusSyncIntervalMinutes ?? 15)); $paymentSyncIntervalMinutes = max(1, (int) ($paymentSyncIntervalMinutes ?? 10)); @@ -210,43 +211,44 @@ foreach ($dmMappings as $dm) { - - + - + - + - + - @@ -254,7 +256,7 @@ foreach ($dmMappings as $dm) {
- - + + - - - - + + + + +
- +
@@ -740,4 +742,16 @@ foreach ($dmMappings as $dm) { } }); })(); + + (function () { + document.querySelectorAll('select[data-shoppro-name-target]').forEach(function (select) { + select.addEventListener('change', function () { + var targetId = select.getAttribute('data-shoppro-name-target'); + var hidden = document.getElementById(targetId); + if (!hidden) return; + var selected = select.options[select.selectedIndex]; + hidden.value = selected ? (selected.getAttribute('data-name') || '') : ''; + }); + }); + })(); diff --git a/src/Modules/Settings/AllegroIntegrationController.php b/src/Modules/Settings/AllegroIntegrationController.php index a6170f1..dc7d022 100644 --- a/src/Modules/Settings/AllegroIntegrationController.php +++ b/src/Modules/Settings/AllegroIntegrationController.php @@ -96,6 +96,7 @@ final class AllegroIntegrationController 'statusSyncIntervalMinutes' => $statusSyncIntervalMinutes, 'statusMappings' => $this->statusMappings->listMappings(), 'orderproStatuses' => $this->orderStatuses->listStatuses(), + 'allegroStatuses' => $this->statusMappings->listExternalStatuses(), 'defaultRedirectUri' => $defaultRedirectUri, 'errorMessage' => (string) Flash::get('settings_error', ''), 'successMessage' => (string) Flash::get('settings_success', ''), diff --git a/src/Modules/Settings/AllegroStatusMappingController.php b/src/Modules/Settings/AllegroStatusMappingController.php index aab4710..d4a0444 100644 --- a/src/Modules/Settings/AllegroStatusMappingController.php +++ b/src/Modules/Settings/AllegroStatusMappingController.php @@ -23,37 +23,6 @@ final class AllegroStatusMappingController public function saveStatusMapping(Request $request): Response { - $csrfError = $this->validateCsrf((string) $request->input('_token', '')); - if ($csrfError !== null) { - return $csrfError; - } - - $allegroStatusCode = strtolower(trim((string) $request->input('allegro_status_code', ''))); - $orderproStatusCode = strtolower(trim((string) $request->input('orderpro_status_code', ''))); - $allegroStatusName = trim((string) $request->input('allegro_status_name', '')); - - if ($allegroStatusCode === '') { - Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.allegro_status_required')); - return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB); - } - - if ($orderproStatusCode === '') { - Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_required')); - return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB); - } - - if (!$this->orderStatusCodeExists($orderproStatusCode)) { - Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found')); - return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB); - } - - try { - $this->statusMappings->upsertMapping($allegroStatusCode, $allegroStatusName !== '' ? $allegroStatusName : null, $orderproStatusCode); - Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved')); - } catch (Throwable $exception) { - Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage()); - } - return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB); } @@ -64,37 +33,39 @@ final class AllegroStatusMappingController return $csrfError; } - $codes = $request->input('allegro_status_code', []); - $names = $request->input('allegro_status_name', []); - $selectedOrderproCodes = $request->input('orderpro_status_code', []); - if (!is_array($codes) || !is_array($names) || !is_array($selectedOrderproCodes)) { + $orderproCodes = $request->input('orderpro_status_code', []); + $allegroCodes = $request->input('allegro_status_code', []); + $allegroNames = $request->input('allegro_status_name', []); + if (!is_array($orderproCodes) || !is_array($allegroCodes) || !is_array($allegroNames)) { Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed')); return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB); } - try { - foreach ($codes as $index => $rawCode) { - $allegroStatusCode = strtolower(trim((string) $rawCode)); - if ($allegroStatusCode === '') { - continue; - } + $allowedOrderpro = $this->resolveAllowedOrderproStatusCodes(); + $mappings = []; - $allegroStatusName = trim((string) ($names[$index] ?? '')); - $orderproStatusCodeRaw = strtolower(trim((string) ($selectedOrderproCodes[$index] ?? ''))); - $orderproStatusCode = $orderproStatusCodeRaw !== '' ? $orderproStatusCodeRaw : null; - - if ($orderproStatusCode !== null && !$this->orderStatusCodeExists($orderproStatusCode)) { - Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.orderpro_status_not_found')); - return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB); - } - - $this->statusMappings->upsertMapping( - $allegroStatusCode, - $allegroStatusName !== '' ? $allegroStatusName : null, - $orderproStatusCode - ); + foreach ($orderproCodes as $index => $rawOrderproCode) { + $orderproCode = strtolower(trim((string) $rawOrderproCode)); + if ($orderproCode === '' || !isset($allowedOrderpro[$orderproCode])) { + continue; } + $allegroCode = strtolower(trim((string) ($allegroCodes[$index] ?? ''))); + if ($allegroCode === '') { + continue; + } + + $allegroName = trim((string) ($allegroNames[$index] ?? '')); + + $mappings[] = [ + 'orderpro_status_code' => $orderproCode, + 'allegro_status_code' => $allegroCode, + 'allegro_status_name' => $allegroName, + ]; + } + + try { + $this->statusMappings->replaceAllMappings($mappings); Flash::set('settings_success', $this->translator->get('settings.allegro.statuses.flash.saved_bulk')); } catch (Throwable $exception) { Flash::set('settings_error', $this->translator->get('settings.allegro.statuses.flash.save_failed') . ' ' . $exception->getMessage()); @@ -149,21 +120,20 @@ final class AllegroStatusMappingController return Response::redirect(RedirectPaths::ALLEGRO_STATUSES_TAB); } - private function orderStatusCodeExists(string $code): bool + /** + * @return array + */ + private function resolveAllowedOrderproStatusCodes(): array { - $needle = strtolower(trim($code)); - if ($needle === '') { - return false; - } - + $allowed = []; foreach ($this->orderStatuses->listStatuses() as $row) { - $statusCode = strtolower(trim((string) ($row['code'] ?? ''))); - if ($statusCode === $needle) { - return true; + $code = strtolower(trim((string) ($row['code'] ?? ''))); + if ($code !== '') { + $allowed[$code] = true; } } - return false; + return $allowed; } private function validateCsrf(string $token): ?Response diff --git a/src/Modules/Settings/AllegroStatusMappingRepository.php b/src/Modules/Settings/AllegroStatusMappingRepository.php index 4899202..b845a81 100644 --- a/src/Modules/Settings/AllegroStatusMappingRepository.php +++ b/src/Modules/Settings/AllegroStatusMappingRepository.php @@ -20,7 +20,7 @@ final class AllegroStatusMappingRepository $statement = $this->pdo->query( 'SELECT id, allegro_status_code, allegro_status_name, orderpro_status_code, created_at, updated_at FROM allegro_order_status_mappings - ORDER BY allegro_status_code ASC' + ORDER BY orderpro_status_code ASC, allegro_status_code ASC' ); $rows = $statement->fetchAll(PDO::FETCH_ASSOC); if (!is_array($rows)) { @@ -39,29 +39,67 @@ final class AllegroStatusMappingRepository }, $rows); } - public function upsertMapping(string $allegroStatusCode, ?string $allegroStatusName, ?string $orderproStatusCode): void + /** + * @return array + */ + public function listExternalStatuses(): array { - $code = strtolower(trim($allegroStatusCode)); - $orderproCode = $orderproStatusCode !== null ? strtolower(trim($orderproStatusCode)) : null; - if ($code === '') { + $statement = $this->pdo->query( + 'SELECT DISTINCT allegro_status_code, allegro_status_name + FROM allegro_order_status_mappings + WHERE allegro_status_code IS NOT NULL + AND allegro_status_code <> "" + ORDER BY allegro_status_code ASC' + ); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + if (!is_array($rows)) { + return []; + } + + $result = []; + foreach ($rows as $row) { + if (!is_array($row)) { + continue; + } + + $code = strtolower(trim((string) ($row['allegro_status_code'] ?? ''))); + if ($code === '') { + continue; + } + + $result[] = [ + 'code' => $code, + 'name' => trim((string) ($row['allegro_status_name'] ?? $code)), + ]; + } + + return $result; + } + + public function upsertMapping(string $orderproStatusCode, ?string $allegroStatusCode, ?string $allegroStatusName): void + { + $orderproCode = strtolower(trim($orderproStatusCode)); + if ($orderproCode === '') { return; } + $allegroCode = $allegroStatusCode !== null ? strtolower(trim($allegroStatusCode)) : null; + $statement = $this->pdo->prepare( 'INSERT INTO allegro_order_status_mappings ( - allegro_status_code, allegro_status_name, orderpro_status_code, created_at, updated_at + orderpro_status_code, allegro_status_code, allegro_status_name, created_at, updated_at ) VALUES ( - :allegro_status_code, :allegro_status_name, :orderpro_status_code, NOW(), NOW() + :orderpro_status_code, :allegro_status_code, :allegro_status_name, NOW(), NOW() ) ON DUPLICATE KEY UPDATE + allegro_status_code = VALUES(allegro_status_code), allegro_status_name = VALUES(allegro_status_name), - orderpro_status_code = VALUES(orderpro_status_code), updated_at = VALUES(updated_at)' ); $statement->execute([ - 'allegro_status_code' => $code, + 'orderpro_status_code' => $orderproCode, + 'allegro_status_code' => $allegroCode !== null && $allegroCode !== '' ? $allegroCode : null, 'allegro_status_name' => StringHelper::nullableString((string) $allegroStatusName), - 'orderpro_status_code' => $orderproCode !== null && $orderproCode !== '' ? $orderproCode : null, ]); } @@ -72,20 +110,41 @@ final class AllegroStatusMappingRepository return; } - $statement = $this->pdo->prepare( + $existing = $this->pdo->prepare( + 'SELECT id FROM allegro_order_status_mappings + WHERE allegro_status_code = :allegro_status_code + LIMIT 1' + ); + $existing->execute(['allegro_status_code' => $code]); + + if ($existing->fetchColumn() !== false) { + $update = $this->pdo->prepare( + 'UPDATE allegro_order_status_mappings + SET allegro_status_name = CASE + WHEN :allegro_status_name IS NULL OR :allegro_status_name2 = "" THEN allegro_status_name + ELSE :allegro_status_name3 + END, + updated_at = NOW() + WHERE allegro_status_code = :allegro_status_code' + ); + $name = StringHelper::nullableString((string) $allegroStatusName); + $update->execute([ + 'allegro_status_name' => $name, + 'allegro_status_name2' => (string) $name, + 'allegro_status_name3' => $name, + 'allegro_status_code' => $code, + ]); + return; + } + + $insert = $this->pdo->prepare( 'INSERT INTO allegro_order_status_mappings ( allegro_status_code, allegro_status_name, orderpro_status_code, created_at, updated_at ) VALUES ( :allegro_status_code, :allegro_status_name, NULL, NOW(), NOW() - ) - ON DUPLICATE KEY UPDATE - allegro_status_name = CASE - WHEN VALUES(allegro_status_name) IS NULL OR VALUES(allegro_status_name) = "" THEN allegro_status_name - ELSE VALUES(allegro_status_name) - END, - updated_at = VALUES(updated_at)' + )' ); - $statement->execute([ + $insert->execute([ 'allegro_status_code' => $code, 'allegro_status_name' => StringHelper::nullableString((string) $allegroStatusName), ]); @@ -112,6 +171,8 @@ final class AllegroStatusMappingRepository 'SELECT orderpro_status_code FROM allegro_order_status_mappings WHERE allegro_status_code = :allegro_status_code + AND orderpro_status_code IS NOT NULL + AND orderpro_status_code <> "" LIMIT 1' ); $statement->execute(['allegro_status_code' => $code]); @@ -130,10 +191,12 @@ final class AllegroStatusMappingRepository public function buildOrderproToAllegroMap(): array { $statement = $this->pdo->query( - 'SELECT allegro_status_code, orderpro_status_code + 'SELECT orderpro_status_code, allegro_status_code FROM allegro_order_status_mappings WHERE orderpro_status_code IS NOT NULL AND orderpro_status_code <> "" + AND allegro_status_code IS NOT NULL + AND allegro_status_code <> "" ORDER BY id ASC' ); $rows = $statement->fetchAll(PDO::FETCH_ASSOC); @@ -157,4 +220,74 @@ final class AllegroStatusMappingRepository return $map; } + /** + * @return array allegro_status_code => orderpro_status_code + */ + public function buildAllegroToOrderproMap(): array + { + $statement = $this->pdo->query( + 'SELECT allegro_status_code, orderpro_status_code + FROM allegro_order_status_mappings + WHERE allegro_status_code IS NOT NULL + AND allegro_status_code <> "" + AND orderpro_status_code IS NOT NULL + AND orderpro_status_code <> "" + ORDER BY id ASC' + ); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + if (!is_array($rows)) { + return []; + } + + $map = []; + foreach ($rows as $row) { + $allegroCode = strtolower(trim((string) ($row['allegro_status_code'] ?? ''))); + $orderproCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))); + if ($allegroCode === '' || $orderproCode === '') { + continue; + } + + if (!isset($map[$allegroCode])) { + $map[$allegroCode] = $orderproCode; + } + } + + return $map; + } + + /** + * @param array $mappings + */ + public function replaceAllMappings(array $mappings): void + { + $this->pdo->exec( + 'DELETE FROM allegro_order_status_mappings WHERE orderpro_status_code IS NOT NULL AND orderpro_status_code <> ""' + ); + + if ($mappings === []) { + return; + } + + $insertStatement = $this->pdo->prepare( + 'INSERT INTO allegro_order_status_mappings ( + orderpro_status_code, allegro_status_code, allegro_status_name, created_at, updated_at + ) VALUES ( + :orderpro_status_code, :allegro_status_code, :allegro_status_name, NOW(), NOW() + )' + ); + + foreach ($mappings as $mapping) { + $orderproCode = strtolower(trim((string) ($mapping['orderpro_status_code'] ?? ''))); + $allegroCode = strtolower(trim((string) ($mapping['allegro_status_code'] ?? ''))); + if ($orderproCode === '' || $allegroCode === '') { + continue; + } + + $insertStatement->execute([ + 'orderpro_status_code' => $orderproCode, + 'allegro_status_code' => $allegroCode, + 'allegro_status_name' => trim((string) ($mapping['allegro_status_name'] ?? '')) ?: null, + ]); + } + } } diff --git a/src/Modules/Settings/ShopproIntegrationsController.php b/src/Modules/Settings/ShopproIntegrationsController.php index cdaafe4..afa9e5c 100644 --- a/src/Modules/Settings/ShopproIntegrationsController.php +++ b/src/Modules/Settings/ShopproIntegrationsController.php @@ -66,9 +66,13 @@ final class ShopproIntegrationsController $this->ensureStatusSyncScheduleExists(); $this->ensurePaymentSyncScheduleExists(); $activeTab = $this->resolveTab((string) $request->input('tab', 'integration')); + $integrationId = $selectedIntegration !== null ? (int) ($selectedIntegration['id'] ?? 0) : 0; $discoveredStatuses = $this->readDiscoveredStatuses(); - $statusRows = $selectedIntegration !== null - ? $this->buildStatusRows((int) ($selectedIntegration['id'] ?? 0), $discoveredStatuses) + $mappingIndex = $integrationId > 0 + ? $this->buildMappingIndex($integrationId) + : []; + $shopproStatuses = $integrationId > 0 + ? $this->buildExternalStatusOptions($integrationId, $discoveredStatuses) : []; $deliveryServicesData = $activeTab === 'delivery' ? $this->loadDeliveryServices() @@ -93,8 +97,9 @@ final class ShopproIntegrationsController 'ordersImportIntervalMinutes' => $this->currentImportIntervalMinutes(), 'statusSyncIntervalMinutes' => $this->currentStatusSyncIntervalMinutes(), 'paymentSyncIntervalMinutes' => $this->currentPaymentSyncIntervalMinutes(), - 'statusRows' => $statusRows, + 'mappingIndex' => $mappingIndex, 'orderproStatuses' => $this->orderStatuses->listStatuses(), + 'shopproStatuses' => $shopproStatuses, 'deliveryMappings' => $deliveryMappings, 'orderDeliveryMethods' => $orderDeliveryMethods, 'allegroDeliveryServices' => $deliveryServicesData[0], @@ -224,38 +229,34 @@ final class ShopproIntegrationsController return $accessError; } + $orderCodes = $request->input('orderpro_status_code', []); $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)) { + if (!is_array($orderCodes) || !is_array($shopCodes) || !is_array($shopNames)) { Flash::set('settings_error', $this->translator->get('settings.integrations.statuses.flash.invalid_payload')); return Response::redirect($redirectTo); } $allowedOrderpro = $this->resolveAllowedOrderproStatusCodes(); - $rowsCount = min(count($shopCodes), count($shopNames), count($orderCodes)); + $rowsCount = min(count($orderCodes), count($shopCodes), count($shopNames)); $mappings = []; for ($index = 0; $index < $rowsCount; $index++) { + $orderCode = strtolower(trim((string) ($orderCodes[$index] ?? ''))); $shopCode = trim((string) ($shopCodes[$index] ?? '')); $shopName = trim((string) ($shopNames[$index] ?? '')); - $orderCode = strtolower(trim((string) ($orderCodes[$index] ?? ''))); + + if ($orderCode === '' || !isset($allowedOrderpro[$orderCode])) { + continue; + } if ($shopCode === '') { continue; } - if ($orderCode === '') { - continue; - } - - if (!isset($allowedOrderpro[$orderCode])) { - continue; - } - $mappings[] = [ + 'orderpro_status_code' => $orderCode, 'shoppro_status_code' => $shopCode, 'shoppro_status_name' => $shopName, - 'orderpro_status_code' => $orderCode, ]; } @@ -494,55 +495,68 @@ final class ShopproIntegrationsController } /** - * @return array + * @return array */ - private function buildStatusRows(int $integrationId, array $discoveredStatuses): array + private function buildMappingIndex(int $integrationId): array { - $mappedRows = $this->statusMappings->listByIntegration($integrationId); - $result = []; + $rows = $this->statusMappings->listByIntegration($integrationId); + $index = []; - foreach ($mappedRows as $row) { - $code = trim((string) ($row['shoppro_status_code'] ?? '')); - if ($code === '') { + foreach ($rows as $row) { + $orderproCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))); + if ($orderproCode === '') { continue; } - $key = mb_strtolower($code); - $result[$key] = [ - 'shoppro_status_code' => $code, + $index[$orderproCode] = [ + 'shoppro_status_code' => trim((string) ($row['shoppro_status_code'] ?? '')), 'shoppro_status_name' => trim((string) ($row['shoppro_status_name'] ?? '')), - 'orderpro_status_code' => strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))), ]; } + return $index; + } + + /** + * @param array $discoveredStatuses + * @return array + */ + private function buildExternalStatusOptions(int $integrationId, array $discoveredStatuses): array + { + $existing = $this->statusMappings->listExternalStatuses($integrationId); + $seen = []; + $result = []; + + foreach ($existing as $status) { + $code = strtolower(trim((string) ($status['code'] ?? ''))); + if ($code === '' || isset($seen[$code])) { + continue; + } + $seen[$code] = true; + $result[] = $status; + } + foreach ($discoveredStatuses as $status) { if (!is_array($status)) { continue; } - - $code = trim((string) ($status['code'] ?? '')); - if ($code === '') { + $code = strtolower(trim((string) ($status['code'] ?? ''))); + if ($code === '' || isset($seen[$code])) { continue; } - - $key = mb_strtolower($code); - if (!isset($result[$key])) { - $result[$key] = [ - 'shoppro_status_code' => $code, - 'shoppro_status_name' => trim((string) ($status['name'] ?? '')), - 'orderpro_status_code' => '', - ]; - } + $seen[$code] = true; + $result[] = [ + 'code' => $code, + 'name' => trim((string) ($status['name'] ?? $code)), + ]; } - uasort($result, static function (array $left, array $right): int { - return strcmp( - strtolower((string) ($left['shoppro_status_code'] ?? '')), - strtolower((string) ($right['shoppro_status_code'] ?? '')) - ); - }); + usort($result, static fn (array $a, array $b): int => strcmp( + strtolower((string) ($a['code'] ?? '')), + strtolower((string) ($b['code'] ?? '')) + )); - return array_values($result); + return $result; } /** diff --git a/src/Modules/Settings/ShopproOrdersSyncService.php b/src/Modules/Settings/ShopproOrdersSyncService.php index 05d5cbc..4613a16 100644 --- a/src/Modules/Settings/ShopproOrdersSyncService.php +++ b/src/Modules/Settings/ShopproOrdersSyncService.php @@ -297,7 +297,7 @@ final class ShopproOrdersSyncService } /** - * @return array + * @return array shoppro_status_code => orderpro_status_code (reverse of DB direction) */ private function buildStatusMap(int $integrationId): array { @@ -309,7 +309,9 @@ final class ShopproOrdersSyncService if ($shopCode === '' || $orderCode === '') { continue; } - $map[$shopCode] = $orderCode; + if (!isset($map[$shopCode])) { + $map[$shopCode] = $orderCode; + } } return $map; diff --git a/src/Modules/Settings/ShopproStatusMappingRepository.php b/src/Modules/Settings/ShopproStatusMappingRepository.php index a371008..4ed277b 100644 --- a/src/Modules/Settings/ShopproStatusMappingRepository.php +++ b/src/Modules/Settings/ShopproStatusMappingRepository.php @@ -12,7 +12,7 @@ final class ShopproStatusMappingRepository } /** - * @return array + * @return array */ public function listByIntegration(int $integrationId): array { @@ -21,9 +21,53 @@ final class ShopproStatusMappingRepository } $statement = $this->pdo->prepare( - 'SELECT shoppro_status_code, shoppro_status_name, orderpro_status_code + 'SELECT orderpro_status_code, shoppro_status_code, shoppro_status_name FROM order_status_mappings WHERE integration_id = :integration_id + ORDER BY orderpro_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; + } + + $orderproCode = strtolower(trim((string) ($row['orderpro_status_code'] ?? ''))); + if ($orderproCode === '') { + continue; + } + + $result[] = [ + 'orderpro_status_code' => $orderproCode, + 'shoppro_status_code' => trim((string) ($row['shoppro_status_code'] ?? '')), + 'shoppro_status_name' => trim((string) ($row['shoppro_status_name'] ?? '')), + ]; + } + + return $result; + } + + /** + * @return array + */ + public function listExternalStatuses(int $integrationId): array + { + if ($integrationId <= 0) { + return []; + } + + $statement = $this->pdo->prepare( + 'SELECT DISTINCT shoppro_status_code, shoppro_status_name + FROM order_status_mappings + WHERE integration_id = :integration_id + AND shoppro_status_code <> "" ORDER BY shoppro_status_code ASC' ); $statement->execute(['integration_id' => $integrationId]); @@ -39,15 +83,14 @@ final class ShopproStatusMappingRepository continue; } - $shopproCode = trim((string) ($row['shoppro_status_code'] ?? '')); - if ($shopproCode === '') { + $code = trim((string) ($row['shoppro_status_code'] ?? '')); + if ($code === '') { continue; } $result[] = [ - 'shoppro_status_code' => $shopproCode, - 'shoppro_status_name' => trim((string) ($row['shoppro_status_name'] ?? '')), - 'orderpro_status_code' => trim((string) ($row['orderpro_status_code'] ?? '')), + 'code' => $code, + 'name' => trim((string) ($row['shoppro_status_name'] ?? $code)), ]; } @@ -55,7 +98,7 @@ final class ShopproStatusMappingRepository } /** - * @param array $mappings + * @param array $mappings */ public function replaceForIntegration(int $integrationId, array $mappings): void { @@ -74,25 +117,25 @@ final class ShopproStatusMappingRepository $insertStatement = $this->pdo->prepare( 'INSERT INTO order_status_mappings ( - integration_id, shoppro_status_code, shoppro_status_name, orderpro_status_code, created_at, updated_at + integration_id, orderpro_status_code, shoppro_status_code, shoppro_status_name, created_at, updated_at ) VALUES ( - :integration_id, :shoppro_status_code, :shoppro_status_name, :orderpro_status_code, NOW(), NOW() + :integration_id, :orderpro_status_code, :shoppro_status_code, :shoppro_status_name, NOW(), NOW() )' ); foreach ($mappings as $mapping) { + $orderproCode = strtolower(trim((string) ($mapping['orderpro_status_code'] ?? ''))); $shopproCode = trim((string) ($mapping['shoppro_status_code'] ?? '')); - $orderproCode = trim((string) ($mapping['orderpro_status_code'] ?? '')); - if ($shopproCode === '' || $orderproCode === '') { + if ($orderproCode === '' || $shopproCode === '') { continue; } $shopproName = trim((string) ($mapping['shoppro_status_name'] ?? '')); $insertStatement->execute([ 'integration_id' => $integrationId, + 'orderpro_status_code' => $orderproCode, 'shoppro_status_code' => $shopproCode, 'shoppro_status_name' => $shopproName !== '' ? $shopproName : null, - 'orderpro_status_code' => $orderproCode, ]); } }