From 2ab0d0e90ea42252e2800686883662d0461228d1 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 28 Mar 2026 15:04:35 +0100 Subject: [PATCH] update --- .env | 2 + .env.example | 2 + .paul/PROJECT.md | 10 +- .paul/ROADMAP.md | 44 +- .paul/STATE.md | 47 +- .../46-allegro-status-push/46-01-PLAN.md | 199 ++++++++ .../46-allegro-status-push/46-01-SUMMARY.md | 132 ++++++ .../48-01-PLAN.md | 197 ++++++++ .../48-01-SUMMARY.md | 137 ++++++ .../49-automation-history-tab/49-01-PLAN.md | 251 ++++++++++ .../49-01-SUMMARY.md | 147 ++++++ .scannerwork/report-task.txt | 4 +- .vscode/ftp-kr.sync.cache.json | 428 ++---------------- DOCS/ARCHITECTURE.md | 35 +- DOCS/DB_SCHEMA.md | 30 ++ DOCS/TECH_CHANGELOG.md | 63 +++ DOCS/todo.md | 1 + config/app.php | 1 + ...create_automation_execution_logs_table.sql | 30 ++ public/assets/css/app.css | 2 +- public/assets/js/modules/automation-form.js | 19 + resources/lang/pl.php | 2 +- resources/scss/app.scss | 4 +- resources/scss/modules/_automation.scss | 36 ++ resources/views/automation/form.php | 23 +- resources/views/automation/index.php | 341 ++++++++++++-- resources/views/settings/allegro.php | 4 +- resources/views/settings/email-templates.php | 1 + routes/web.php | 63 ++- src/Core/Application.php | 7 +- .../Automation/AutomationController.php | 92 +++- .../AutomationExecutionLogRepository.php | 209 +++++++++ .../Automation/AutomationRepository.php | 30 +- src/Modules/Automation/AutomationService.php | 127 +++++- .../Cron/AutomationHistoryCleanupHandler.php | 29 ++ src/Modules/Cron/CronHandlerFactory.php | 8 +- src/Modules/Email/VariableResolver.php | 45 +- src/Modules/Settings/AllegroApiClient.php | 90 ++++ .../AllegroOrderSyncStateRepository.php | 47 +- .../AllegroStatusMappingRepository.php | 33 ++ .../Settings/AllegroStatusSyncService.php | 210 ++++++++- .../Settings/EmailTemplateController.php | 9 + .../Shipments/ApaczkaShipmentService.php | 65 ++- tests/Unit/AllegroStatusSyncServiceTest.php | 264 +++++++++++ 44 files changed, 3027 insertions(+), 493 deletions(-) create mode 100644 .paul/phases/46-allegro-status-push/46-01-PLAN.md create mode 100644 .paul/phases/46-allegro-status-push/46-01-SUMMARY.md create mode 100644 .paul/phases/48-email-template-shipment-variables/48-01-PLAN.md create mode 100644 .paul/phases/48-email-template-shipment-variables/48-01-SUMMARY.md create mode 100644 .paul/phases/49-automation-history-tab/49-01-PLAN.md create mode 100644 .paul/phases/49-automation-history-tab/49-01-SUMMARY.md create mode 100644 database/migrations/20260328_000072_create_automation_execution_logs_table.sql create mode 100644 src/Modules/Automation/AutomationExecutionLogRepository.php create mode 100644 src/Modules/Cron/AutomationHistoryCleanupHandler.php create mode 100644 tests/Unit/AllegroStatusSyncServiceTest.php diff --git a/.env b/.env index f0cdfb9..43823fe 100644 --- a/.env +++ b/.env @@ -15,6 +15,8 @@ DB_CHARSET=utf8mb4 INTEGRATIONS_SECRET=nB3sTkXAbBLqA2Ent74R9Mi1118bAbWa +CRON_PUBLIC_TOKEN=9b8c1e5f-9c3a-4d2b-8e7a-1f2b3c4d5e6f + pracownia.key=9554daf4bbcbbb5e72a2b48ee7d6a7f20262713d72484b781460e2c772d813fc login=jacek.pyziak@project-pro.pl diff --git a/.env.example b/.env.example index aedd4fb..f4e5490 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ SESSION_NAME=orderpro_session INTEGRATIONS_SECRET=change-me-long-random-secret CRON_RUN_ON_WEB=false CRON_WEB_LIMIT=5 +# Publiczny endpoint HTTPS do triggera crona: /cron?token=... +CRON_PUBLIC_TOKEN= DB_CONNECTION=mysql DB_HOST=127.0.0.1 diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index f4a2402..55475a7 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -13,7 +13,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| | Version | 1.0.0 | -| Status | v1.9 Complete | +| Status | v2.1 Complete | | Last Updated | 2026-03-28 | ## Requirements @@ -56,6 +56,8 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Synchronizacja statusow orderPRO -> shopPRO (cron push, reverse mapping, PUT API) — Phase 45 - [x] Synchronizacja statusow orderPRO -> Allegro (cron push, reverse mapping, fulfillment status update API) - Phase 46 - [x] Automatyzacja przesylek: natychmiastowy event `shipment.created` + akcja `update_shipment_status` - Phase 47 +- [x] Szablony e-mail: zmienne `przesylka.numer` i `przesylka.link_sledzenia` z provider-aware linkiem sledzenia - Phase 48 +- [x] Automatyzacja: tab Historia z filtrowaniem/paginacja + retencja 30 dni + akcja update_order_status - Phase 49 ### Active (In Progress) @@ -122,6 +124,9 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API | Quill.js 2.0.3 CDN dla edytora szablonĂłw | Brak build pipeline w projekcie; CDN prostszy | 2026-03-16 | Active | | Event automatyzacji `shipment.created` uruchamiany natychmiast po utworzeniu paczki | Reakcje automatyzacji nie czekaja na cron tracking; przeplyw jest natychmiastowy | 2026-03-28 | Active | | Akcja `update_shipment_status` emituje `shipment.status_changed` tylko przy realnej zmianie | Brak petli automatyzacji i brak falszywych triggerow | 2026-03-28 | Active | +| Zmienne e-mail przesylki bazuja na najnowszej paczce `shipment_packages` i `DeliveryStatus::trackingUrl` | Jeden spojny kontrakt dla numeru i linku sledzenia w szablonach | 2026-03-28 | Active | +| Historia automatyzacji zapisywana per regula (success/failed) i czyszczona cronem po 30 dniach | Audyt wykonywania regul bez recznego utrzymania danych | 2026-03-28 | Active | +| Akcja update_order_status korzysta z OrdersRepository::updateOrderStatus | Spojnosc z historia statusow i activity log bez duplikowania logiki | 2026-03-28 | Active | ## Success Metrics @@ -153,5 +158,6 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-03-28 after Phase 47 completion (Shipment Creation Automation)* +*Last updated: 2026-03-28 after Phase 49 completion (Automation History Tab)* + diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index a0ed231..87998b2 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -2,22 +2,43 @@ ## Overview -orderPRO to narzÄ™dzie do wielokanaĹ‚owego zarzÄ…dzania sprzedaĹĽÄ…. Projekt przechodzi od podstawowych integracji z marketplace'ami i generowania etykiet, przez rozbudowÄ™ o nowe ĹşrĂłdĹ‚a zamĂłwieĹ„ i przewoĹşnikĂłw, aĹĽ do peĹ‚nego zarzÄ…dzania produktami i stanami magazynowymi. +orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechodzi od podstawowych integracji z marketplace'ami i generowania etykiet, przez rozbudowe o nowe zrodla zamowien i przewoznikow, az do pelnego zarzadzania produktami i stanami magazynowymi. ## Current Milestone -No active milestone (v1.9 complete) +No active milestone - Ready to define next scope -Gotowe do zaplanowania kolejnego milestone (obszary planowane: zarzadzanie produktami i stanami magazynowymi). - -| Phase | Name | Status | Plans | -|------|------|--------|-------| -| - | - | - | - | - -Next action: utworzyc nowy milestone i roadmape kolejnego zakresu. +Next action: uruchom $paul-milestone (lub $paul-plan) dla kolejnego celu biznesowego. ## Completed Milestones +
+v2.1 Automation History & Observability - 2026-03-28 (1 phase, 1 plan) + +Rozdzielenie Ustawienia > Zadania automatyczne na taby Ustawienia i Historia, wdrozenie audytu wykonan regul (filtry + paginacja), retencja 30 dni oraz akcja update_order_status. + +| Phase | Name | Plans | Completed | +|-------|------|-------|-----------| +| 49 | Automation History Tab | 1/1 | 2026-03-28 | + +Archive: .paul/phases/49-automation-history-tab/ + +
+ + +
+v2.0 Email Template Shipment Variables - 2026-03-28 (1 phase, 1 plan) + +Rozszerzenie szablonow e-mail o zmienne przesylki (`przesylka.numer`, `przesylka.link_sledzenia`) oraz provider-aware budowanie linku sledzenia. + +| Phase | Name | Plans | Completed | +|-------|------|-------|-----------| +| 48 | Email Template Shipment Variables | 1/1 | 2026-03-28 | + +Archive: `.paul/phases/48-email-template-shipment-variables/` + +
+
v1.9 Shipment Automation Immediate Trigger - 2026-03-28 (1 phase, 1 plan) @@ -281,4 +302,7 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-03-28 - v1.8 Allegro Status Push completed* +*Last updated: 2026-03-28 - v2.1 completed (phase 49)* + + + diff --git a/.paul/STATE.md b/.paul/STATE.md index df8cdde..ff92b50 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -1,55 +1,56 @@ -# Project State +# Project State ## Project Reference See: .paul/PROJECT.md (updated 2026-03-28) **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. -**Current focus:** v1.9 complete - ready to plan next milestone +**Current focus:** Milestone v2.1 completed; ready for next milestone planning ## Current Position -Milestone: v1.9 Shipment Automation Immediate Trigger - Complete -Phase: Complete (47 - Shipment Creation Automation) -Plan: 47-01 complete -Status: Ready to plan next milestone -Last activity: 2026-03-28 14:35:00 - UNIFY completed, phase transitioned +Milestone: v2.1 Automation History & Observability - Complete +Phase: 1 of 1 (49 - Automation History Tab) - Complete +Plan: 49-01 complete +Status: Ready for next PLAN / next milestone +Last activity: 2026-03-28 14:47:06 - UNIFY closed for 49-01, SUMMARY created Progress: -- v1.9 Milestone: [##########] 100% -- Next milestone: [..........] 0% +- Milestone: [##########] 100% +- Phase 49: [##########] 100% ## Loop Position Current loop state: ``` PLAN --> APPLY --> UNIFY - done done done [Loop complete - ready for next PLAN] + done done done [Loop complete - ready for next PLAN] ``` +## Session Continuity + +Last session: 2026-03-28 14:47:06 +Stopped at: Phase 49 complete, milestone v2.1 complete +Next action: Uruchom `$paul-milestone` (lub `$paul-plan`) dla kolejnego celu +Resume file: .paul/ROADMAP.md + ## Accumulated Context ### Decisions | Date | Decision | Impact | |------|----------|--------| -| 2026-03-28 | Dodano event `shipment.created` triggerowany natychmiast po sukcesie tworzenia paczki | Reguly automatyzacji reaguja od razu, bez oczekiwania na cron | -| 2026-03-28 | Dodano akcje `update_shipment_status` z aktualizacja tylko przy realnej zmianie | Brak petli i duplikatow triggerow | -| 2026-03-28 | `AutomationService` rozszerzony o `ShipmentPackageRepository` i fallback wyboru paczki | Stabilne wykonanie akcji statusowej nawet bez `package_id` w kontekscie | +| 2026-03-28 | Rozdzielenie `/settings/automation` na taby `Ustawienia` i `Historia` | Lepsza czytelnosc i oddzielenie konfiguracji od audytu wykonania | +| 2026-03-28 | Historia automatyzacji zapisywana per regula (success/failed) + filtry/paginacja | Szybsza diagnostyka triggerow i wynikow regul | +| 2026-03-28 | Retencja historii starszej niz 30 dni przez cron `automation_history_cleanup` | Kontrola rozmiaru danych i automatyczne porzadkowanie | +| 2026-03-28 | Akcja `update_order_status` przez `OrdersRepository::updateOrderStatus` | Spojnosc z historia statusow i activity logiem | -### Skill Audit (Phase 47, Plan 01) +### Skill Audit Carry-Over | Expected | Invoked | Notes | |----------|---------|-------| -| sonar-scanner | ✓ | Scan wykonany w UNIFY; analysis successful na sonar.project-pro.pl | - -## Session Continuity - -Last session: 2026-03-28 14:35:00 -Stopped at: Phase 47 complete, loop closed -Next action: Start next milestone planning ($paul-new-milestone) -Resume file: .paul/phases/47-shipment-created-automation/47-01-SUMMARY.md +| sonar-scanner | yes | Uruchomiony po APPLY, analiza zakonczona sukcesem | ## Git State -Last commit: ad9087d +Last commit: c1d0d77 Branch: main Feature branches merged: none diff --git a/.paul/phases/46-allegro-status-push/46-01-PLAN.md b/.paul/phases/46-allegro-status-push/46-01-PLAN.md new file mode 100644 index 0000000..5efc0dd --- /dev/null +++ b/.paul/phases/46-allegro-status-push/46-01-PLAN.md @@ -0,0 +1,199 @@ +--- +phase: 46-allegro-status-push +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Settings/AllegroStatusSyncService.php + - src/Modules/Settings/AllegroApiClient.php + - src/Modules/Settings/AllegroStatusMappingRepository.php + - src/Modules/Settings/AllegroOrderSyncStateRepository.php + - src/Modules/Settings/AllegroIntegrationController.php + - resources/views/settings/allegro.php + - resources/lang/pl.php + - tests/Unit/AllegroStatusSyncServiceTest.php + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md +autonomous: true +--- + + +## Goal +Wdrozyc dzialajaca synchronizacje statusow zamowien w kierunku `orderPRO -> Allegro` i aktywowac ten kierunek w konfiguracji integracji Allegro. + +## Purpose +Opcja kierunku `orderPRO -> Allegro` jest dostepna w UI, ale obecnie jest zablokowana i backend zwraca komunikat "jeszcze nie wdrozony". Operator nie moze wypchnac zmian statusu wykonanych recznie w orderPRO do Allegro. + +## Output +- Pelna obsluga push direction w `AllegroStatusSyncService` +- Metoda API do aktualizacji statusu checkout form w Allegro +- Reverse mapping statusow `orderpro_status_code -> allegro_status_code` +- Aktywna opcja kierunku w UI ustawien Allegro +- Testy jednostkowe krytycznej logiki push +- Aktualizacja dokumentacji technicznej + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/45-shoppro-status-push/45-01-SUMMARY.md + +## Source Files +@src/Modules/Settings/AllegroStatusSyncService.php +@src/Modules/Settings/AllegroApiClient.php +@src/Modules/Settings/AllegroStatusMappingRepository.php +@src/Modules/Settings/AllegroOrderSyncStateRepository.php +@src/Modules/Settings/AllegroIntegrationController.php +@resources/views/settings/allegro.php +@resources/lang/pl.php +@src/Modules/Cron/AllegroStatusSyncHandler.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner | required | Po APPLY, przed UNIFY | o | + +**BLOCKING:** Required skills MUST be loaded before APPLY proceeds. + +## Skill Invocation Checklist +- [ ] sonar-scanner loaded (run command or confirm) + + + + +## AC-1: Cron pushuje reczne zmiany statusu orderPRO do Allegro +```gherkin +Given integracja Allegro ma ustawiony kierunek synchronizacji statusow na orderPRO -> Allegro + And zamowienie Allegro ma reczna zmiane statusu w orderPRO (order_status_history.change_source = manual) + And istnieje mapowanie statusu orderPRO na status Allegro +When uruchamia sie job cron allegro_status_sync +Then AllegroStatusSyncService wykonuje push statusu do API Allegro + And wynik synchronizacji zawiera licznik pushed i failed +``` + +## AC-2: Brak mapowania nie zatrzymuje przetwarzania +```gherkin +Given czesc zamowien nie ma reverse mapowania statusu orderPRO -> Allegro +When cron przetwarza batch zmian statusow +Then zamowienia bez mapowania sa oznaczone jako skipped + And pozostale zamowienia sa dalej przetwarzane +``` + +## AC-3: Ustawienie kierunku jest aktywne i zapisywalne w UI +```gherkin +Given operator otworzy Ustawienia > Integracje > Allegro > Ustawienia +When wybierze kierunek orderPRO -> Allegro i zapisze formularz +Then opcja nie jest zablokowana jako disabled + And wartosc jest zapisana w app_settings (allegro_status_sync_direction) +``` + + + + + + + Task 1: Implementacja push direction w AllegroStatusSyncService i reverse mapowania + src/Modules/Settings/AllegroStatusSyncService.php, src/Modules/Settings/AllegroStatusMappingRepository.php, src/Modules/Settings/AllegroOrderSyncStateRepository.php + + 1. Rozszerzyc `AllegroStatusSyncService::sync()` o realna sciezke dla `orderpro_to_allegro` zamiast zwracania "jeszcze nie wdrozony". + 2. Dodac prywatna metode push (np. `syncOrderproToAllegro()`), ktora: + - pobiera zamowienia Allegro z recznymi zmianami statusu (`order_status_history.change_source = manual`) od ostatniego kursora, + - filtruje tylko zamowienia `source = allegro`, + - buduje reverse mapowanie na podstawie `allegro_order_status_mappings` (`orderpro_status_code -> allegro_status_code`, first match wins), + - dla brakow mapowania zwieksza `skipped`, bez przerywania petli, + - aktualizuje kursor push po sukcesie batcha. + 3. W `AllegroStatusMappingRepository` dodac metode zwracajaca reverse map dla push direction. + 4. W `AllegroOrderSyncStateRepository` dodac obsluge kursora `last_status_pushed_at` (get/update), defensywnie wobec srodowisk bez kolumny. + 5. Zachowac obecna logike `allegro_to_orderpro` bez regresji. + + Avoid: + - nie pushowac zmian z `change_source = import` ani `sync` (petla synchronizacji), + - nie modyfikowac flow importu zamowien Allegro. + + php -l src/Modules/Settings/AllegroStatusSyncService.php && php -l src/Modules/Settings/AllegroStatusMappingRepository.php && php -l src/Modules/Settings/AllegroOrderSyncStateRepository.php + AC-1 i AC-2 satisfied: backend obsluguje push direction i bezpiecznie pomija brak mapowania + + + + Task 2: Dodanie metody API update statusu checkout-form w AllegroApiClient + src/Modules/Settings/AllegroApiClient.php, src/Modules/Settings/AllegroStatusSyncService.php + + 1. Dodac do `AllegroApiClient` metode aktualizacji statusu realizacji zamowienia Allegro (checkout form fulfillment) z obsluga tokena i bledow HTTP analogiczna do istniejacych metod. + 2. Uzyc tej metody w push direction serwisu sync, mapujac status orderPRO na docelowy kod Allegro. + 3. Zapewnic czytelny wynik dla kazdego rekordu (success/fail) i agregacje w odpowiedzi crona. + 4. Zachowac istniejace standardy: prepared flow, brak stringowego SQL, brak "magic values" bez stalej. + + Avoid: + - nie dodawac natywnych `alert()/confirm()` ani zmian w warstwie widoku w tym tasku, + - nie tlumic bledow API bez zapisu ich tresci do wyniku sync. + + php -l src/Modules/Settings/AllegroApiClient.php && php -l src/Modules/Settings/AllegroStatusSyncService.php + AC-1 satisfied: service moze fizycznie zaktualizowac status po stronie Allegro API + + + + Task 3: Aktywacja opcji w UI + testy jednostkowe + dokumentacja + resources/views/settings/allegro.php, src/Modules/Settings/AllegroIntegrationController.php, resources/lang/pl.php, tests/Unit/AllegroStatusSyncServiceTest.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md + + 1. Usunac blokade `disabled` z opcji `orderPRO -> Allegro` w formularzu ustawien Allegro i pozostawic poprawny zapis przez istniejacy endpoint `saveImportSettings`. + 2. Uzuplenic komunikaty i opisy w tlumaczeniach PL (jezeli wymagane), aby nie sugerowaly "wkrotce". + 3. Dodac testy jednostkowe dla krytycznych sciezek push: + - push sukces + increment `pushed`, + - brak reverse mapping + increment `skipped`, + - blad API + increment `failed` i brak przerwania batcha. + 4. Zaktualizowac `DOCS/ARCHITECTURE.md` o dzialajacy kierunek `orderpro_to_allegro`. + 5. Dopisac wpis do `DOCS/TECH_CHANGELOG.md` (co i dlaczego zostalo wdrozone). + + Avoid: + - nie zmieniac schematu DB w tym planie, + - nie rozszerzac zakresu o nowe endpointy UI poza aktywacja istniejacej opcji. + + php -l src/Modules/Settings/AllegroIntegrationController.php && php -l tests/Unit/AllegroStatusSyncServiceTest.php && C:/xampp/php/php.exe vendor/bin/phpunit tests/Unit/AllegroStatusSyncServiceTest.php + AC-3 satisfied: opcja jest aktywna, testy i dokumentacja pokrywaja nowy kierunek + + + + + + +## DO NOT CHANGE +- src/Modules/Settings/AllegroOrdersSyncService.php +- src/Modules/Settings/AllegroOrderImportService.php +- src/Modules/Orders/OrdersController.php +- database/migrations/* + +## SCOPE LIMITS +- Zakres dotyczy tylko synchronizacji statusow, bez zmian importu zamowien i bez zmian mapowania dostaw. +- Brak przebudowy UI zakladki Allegro poza aktywacja istniejacej opcji kierunku. +- Brak nowych cron job types i brak zmian interwalow harmonogramu. + + + + +Before declaring plan complete: +- [ ] `php -l` przechodzi dla wszystkich zmienionych plikow PHP +- [ ] Test jednostkowy `AllegroStatusSyncServiceTest` przechodzi lokalnie +- [ ] Kierunek `orderpro_to_allegro` nie zwraca juz komunikatu "jeszcze nie wdrozony" +- [ ] Wynik sync zawiera liczniki `pushed`, `skipped`, `failed` +- [ ] UI pozwala zapisac kierunek `orderPRO -> Allegro` +- [ ] `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md` sa zaktualizowane + + + +- Wszystkie taski zakonczone +- Wszystkie kryteria akceptacji spelnione +- Brak regresji w kierunku `allegro_to_orderpro` +- Nowy kierunek dziala z poziomu crona i jest aktywowalny w UI + + + +After completion, create `.paul/phases/46-allegro-status-push/46-01-SUMMARY.md` + diff --git a/.paul/phases/46-allegro-status-push/46-01-SUMMARY.md b/.paul/phases/46-allegro-status-push/46-01-SUMMARY.md new file mode 100644 index 0000000..03443b2 --- /dev/null +++ b/.paul/phases/46-allegro-status-push/46-01-SUMMARY.md @@ -0,0 +1,132 @@ +--- +phase: 46-allegro-status-push +plan: 01 +subsystem: api +tags: [allegro, status-sync, cron, mappings] +requires: + - phase: 45-shoppro-status-push + provides: orderPRO to external marketplace status push pattern +provides: + - Allegro status push from orderPRO manual status changes + - Active orderPRO_to_allegro direction in integration settings + - Unit coverage for core push scenarios +affects: [settings, cron, integrations] +tech-stack: + added: [] + patterns: [reverse status mapping, push cursor tracking, api retry on 401] +key-files: + created: + - .paul/phases/46-allegro-status-push/46-01-SUMMARY.md + - tests/Unit/AllegroStatusSyncServiceTest.php + modified: + - src/Modules/Settings/AllegroStatusSyncService.php + - src/Modules/Settings/AllegroApiClient.php + - src/Modules/Settings/AllegroStatusMappingRepository.php + - src/Modules/Settings/AllegroOrderSyncStateRepository.php + - src/Modules/Cron/CronHandlerFactory.php + - resources/views/settings/allegro.php + - resources/lang/pl.php + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md +key-decisions: + - "Push only manual status changes (change_source=manual) to avoid sync loops." + - "Reuse integration_order_sync_state with last_status_pushed_at cursor for incremental push." + - "Retry once after token refresh on ALLEGRO_HTTP_401." +patterns-established: + - "First-match-wins reverse mapping: orderpro_status_code -> allegro_status_code." + - "Per-order push result aggregation: pushed, skipped, failed." +duration: ~4h +started: 2026-03-28T12:20:50+01:00 +completed: 2026-03-28T13:20:00+01:00 +--- + +# Phase 46 Plan 01: Allegro Status Push Summary + +orderPRO to Allegro status synchronization was implemented end-to-end and enabled in UI, with tests and documentation updates. + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~4h | +| Started | 2026-03-28T12:20:50+01:00 | +| Completed | 2026-03-28T13:20:00+01:00 | +| Tasks | 3 completed | +| Files modified | 10 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Cron pushes orderPRO manual status changes to Allegro | Pass | Implemented in AllegroStatusSyncService with API call, counters, and cursor updates. | +| AC-2: Missing mapping does not stop processing | Pass | Unmapped statuses are counted as skipped; batch continues. | +| AC-3: orderPRO -> Allegro direction active in UI | Pass | Disabled flag removed, option is selectable and persisted by existing settings flow. | + +## Verification Results + +| Command | Result | +|--------|--------| +| `php -l src/Modules/Settings/AllegroStatusSyncService.php` | PASS | +| `php -l src/Modules/Settings/AllegroApiClient.php` | PASS | +| `php -l src/Modules/Settings/AllegroStatusMappingRepository.php` | PASS | +| `php -l src/Modules/Settings/AllegroOrderSyncStateRepository.php` | PASS | +| `php -l src/Modules/Cron/CronHandlerFactory.php` | PASS | +| `php -l tests/Unit/AllegroStatusSyncServiceTest.php` | PASS | +| `C:/xampp/php/php.exe vendor/bin/phpunit --filter AllegroStatusSyncServiceTest --testdox` | PASS (4 tests, 39 assertions) | +| `sonar-scanner` | PASS (analysis successful; Quality Gate failed due existing/new issues tracked in DOCS/todo.md) | + +## Accomplishments + +- Implemented real push path `orderpro_to_allegro` in sync service. +- Added Allegro API fulfillment status update method and integrated it into cron flow. +- Added reverse status mapping and push cursor support. +- Enabled the direction in Allegro settings UI and adjusted PL hint text. +- Added unit tests for success, skipped, failure, and 401 retry behavior. +- Updated architecture and technical changelog docs. + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `src/Modules/Settings/AllegroStatusSyncService.php` | Modified | Added orderPRO->Allegro push execution and result aggregation. | +| `src/Modules/Settings/AllegroApiClient.php` | Modified | Added PUT fulfillment status endpoint wrapper. | +| `src/Modules/Settings/AllegroStatusMappingRepository.php` | Modified | Added reverse map builder for push direction. | +| `src/Modules/Settings/AllegroOrderSyncStateRepository.php` | Modified | Added read/write support for `last_status_pushed_at`. | +| `src/Modules/Cron/CronHandlerFactory.php` | Modified | Injected dependencies for new push logic and shared sync state repository. | +| `resources/views/settings/allegro.php` | Modified | Enabled orderPRO->Allegro direction option. | +| `resources/lang/pl.php` | Modified | Updated direction hint text (no "soon" wording). | +| `tests/Unit/AllegroStatusSyncServiceTest.php` | Created | Added tests for key push flow behaviors. | +| `DOCS/ARCHITECTURE.md` | Modified | Documented active push direction and API client method. | +| `DOCS/TECH_CHANGELOG.md` | Modified | Logged technical change and rationale. | + +## Deviations from Plan + +| Type | Count | Impact | +|------|-------|--------| +| Scope additions | 1 | Low | +| Deferred | 1 | Low | + +- Scope addition: `src/Modules/Cron/CronHandlerFactory.php` was updated to wire dependencies for the new service behavior (not listed as a direct plan file, but required to activate runtime flow). +- Plan file listed `src/Modules/Settings/AllegroIntegrationController.php` as a target, but no code change was needed there because existing save flow already persisted `allegro_status_sync_direction`. +- Deferred: Sonar Quality Gate remediation intentionally postponed; full issue list captured in `DOCS/todo.md`. + +## Key Patterns and Decisions + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Manual-only push (`change_source=manual`) | Prevent import/sync feedback loops | Safe bidirectional architecture | +| Cursor-based push (`last_status_pushed_at`) | Incremental processing and bounded batches | Better cron performance and idempotence | +| Retry once on 401 after token refresh | Recover from expired access token | Improved operational resilience | + +## Next Phase Readiness + +Ready: +- v1.8 milestone scope delivered for phase 46. +- Operational status push path to Allegro can be validated in production cron logs. + +Concerns: +- Sonar issues remain open and are tracked in `DOCS/todo.md`. + +Blockers: +- None. + diff --git a/.paul/phases/48-email-template-shipment-variables/48-01-PLAN.md b/.paul/phases/48-email-template-shipment-variables/48-01-PLAN.md new file mode 100644 index 0000000..a6ce82c --- /dev/null +++ b/.paul/phases/48-email-template-shipment-variables/48-01-PLAN.md @@ -0,0 +1,197 @@ +--- +phase: 48-email-template-shipment-variables +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Modules/Email/VariableResolver.php + - src/Modules/Email/EmailSendingService.php + - src/Modules/Cron/CronHandlerFactory.php + - src/Modules/Settings/EmailTemplateController.php + - resources/views/settings/email-templates.php + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md + - DOCS/todo.md +autonomous: false +--- + + +## Goal +Dodac w szablonach e-mail nowe zmienne: `{{przesylka.numer}}` oraz `{{przesylka.link_sledzenia}}`, tak aby link sledzenia byl budowany zaleznie od provider/carrier danej przesylki. + +## Purpose +Szablony e-mail maja przekazywac klientowi realny numer paczki i klikalny URL sledzenia bez recznego dopisywania danych przez operatora. + +## Output +Rozszerzony resolver zmiennych e-mail o dane najnowszej paczki, aktualny katalog zmiennych w UI (`/settings/email-templates`) oraz zaktualizowana dokumentacja techniczna. + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md +@DOCS/ARCHITECTURE.md +@DOCS/DB_SCHEMA.md +@DOCS/todo.md + +## Prior Work (only if genuinely needed) +@.paul/phases/14-email-templates/14-02-SUMMARY.md +@.paul/phases/15-email-sending/15-01-SUMMARY.md +@.paul/phases/27-shipment-tracking-backend/27-01-SUMMARY.md + +## Source Files +@src/Modules/Email/VariableResolver.php +@src/Modules/Email/EmailSendingService.php +@src/Modules/Settings/EmailTemplateController.php +@resources/views/settings/email-templates.php +@src/Modules/Shipments/ShipmentPackageRepository.php +@src/Modules/Shipments/DeliveryStatus.php +@src/Modules/Cron/CronHandlerFactory.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| `sonar-scanner` | required | Po APPLY, przed UNIFY | o | +| /feature-dev | optional | Przed wdrazaniem nowej funkcjonalnosci | o | +| /code-review | optional | Po APPLY, przed UNIFY | o | + +**BLOCKING:** Required skills MUST be loaded before APPLY proceeds. + +## Skill Invocation Checklist +- [ ] `sonar-scanner` uruchomiony po APPLY +- [ ] /feature-dev (opcjonalnie) +- [ ] /code-review (opcjonalnie) + + + + + +## AC-1: Szablony e-mail obsluguja nowe zmienne przesylki +```gherkin +Given uzytkownik edytuje szablon na `/settings/email-templates` +When wybierze zmienne `{{przesylka.numer}}` i `{{przesylka.link_sledzenia}}` +Then zmienne sa dostepne na liscie i poprawnie podstawiane w preview +``` + +## AC-2: Resolver pobiera dane przesylki z zamowienia +```gherkin +Given zamowienie ma co najmniej jedna paczke w `shipment_packages` +When system renderuje temat i tresc e-maila +Then `przesylka.numer` zawiera tracking number najnowszej paczki +And `przesylka.link_sledzenia` zawiera URL sledzenia dla tej paczki +``` + +## AC-3: Link sledzenia jest zalezny od provider/carrier +```gherkin +Given paczka ma `provider` oraz opcjonalnie `carrier_id` +When system oblicza `przesylka.link_sledzenia` +Then korzysta z logiki `DeliveryStatus::trackingUrl(provider, tracking_number, carrier_id)` +And dla braku numeru przesylki zwraca pusty string zamiast blednego URL +``` + +## AC-4: Dokumentacja odzwierciedla nowy kontrakt zmiennych +```gherkin +Given wdrozone zmienne przesylki w module e-mail +When zespol czyta dokumentacje techniczna +Then ARCHITECTURE i TECH_CHANGELOG opisuja nowe zmienne oraz zasady budowy linku +And todo/changelog zawiera wpis o tym zakresie +``` + + + + + + + Task 1: Rozszerz katalog zmiennych szablonow i dane podgladu + src/Modules/Settings/EmailTemplateController.php, resources/views/settings/email-templates.php + + Dodaj nowa grupe/lub rozszerzenie grupy zmiennych o `przesylka.numer` i `przesylka.link_sledzenia`. + Uaktualnij `SAMPLE_DATA`, aby preview na stronie ustawien pokazywal realistyczny numer oraz przykladowy URL sledzenia. + Zachowaj aktualny format placeholderow `{{grupa.pole}}` oraz zgodnosc z endpointem `getVariables`. + + rg -n "przesylka|link_sledzenia|tracking" src/Modules/Settings/EmailTemplateController.php resources/views/settings/email-templates.php + AC-1 satisfied: nowe zmienne sa widoczne i podstawiane w preview. + + + + Task 2: Dodaj resolve danych paczki do zmiennych e-mail + src/Modules/Email/VariableResolver.php, src/Modules/Email/EmailSendingService.php, src/Modules/Cron/CronHandlerFactory.php, src/Modules/Shipments/ShipmentPackageRepository.php + + Rozszerz przeplyw budowy mapy zmiennych tak, by resolver mial dostep do najnowszej paczki dla zamowienia. + Wykorzystaj repozytorium paczek do pobrania ostatniej paczki (`order_id`) i wypelnij: + - `przesylka.numer` = tracking number, + - `przesylka.link_sledzenia` = wynik `DeliveryStatus::trackingUrl(provider, tracking_number, carrier_id)`. + Zapewnij bezpieczny fallback na pusty string, gdy paczka lub tracking nie istnieje. + Dostosuj konstrukcje zaleznosci serwisu e-mail (factory/DI) bez zmian zachowania innych funkcji. + + rg -n "przesylka\\.numer|przesylka\\.link_sledzenia|trackingUrl|findLatestByOrderId" src/Modules/Email src/Modules/Cron src/Modules/Shipments + AC-2 satisfied i AC-3 satisfied: resolver zwraca numer i provider-aware link sledzenia. + + + + Task 3: Zaktualizuj dokumentacje techniczna po wdrozeniu + DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md, DOCS/todo.md + + Dodaj opis nowych zmiennych e-mail i ich zrodla danych (shipment_packages + DeliveryStatus::trackingUrl). + Opisz ograniczenia (brak paczki/brak trackingu = puste wartosci) i brak zmian schematu DB. + Uzupelnij wpis changelog/todo zgodnie z praktyka projektu. + + rg -n "przesylka\\.numer|przesylka\\.link_sledzenia|DeliveryStatus::trackingUrl|email template" DOCS/ARCHITECTURE.md DOCS/TECH_CHANGELOG.md DOCS/todo.md + AC-4 satisfied: dokumentacja techniczna odzwierciedla wdrozenie. + + + + Nowe zmienne szablonow e-mail z numerem i linkiem sledzenia przesylki. + + 1. Otworz: `https://orderpro.projectpro.pl/settings/email-templates`. + 2. Edytuj dowolny szablon i wstaw `{{przesylka.numer}}` oraz `{{przesylka.link_sledzenia}}`. + 3. Uruchom preview i potwierdz, ze obie zmienne sa podstawione. + 4. Wyslij testowy e-mail dla zamowienia z paczka i sprawdz, czy link prowadzi do poprawnego sledzenia dla danego kuriera/providera. + 5. Powtorz dla zamowienia bez paczki i potwierdz brak blednych placeholderow/URL. + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- `database/migrations/*` (bez zmian schematu) +- Moduly automatyzacji i synchronizacji statusow niezwiązane z e-mail template variables +- Widoki i style spoza `settings/email-templates` + +## SCOPE LIMITS +- Zakres obejmuje tylko zmienne szablonow e-mail zwiazane z przesylka. +- Bez dodawania nowych endpointow API. +- Bez zmian logiki providerow trackingu poza wykorzystaniem istniejacego `DeliveryStatus::trackingUrl`. + + + + +Before declaring plan complete: +- [ ] `C:\xampp\php\php.exe -l src/Modules/Settings/EmailTemplateController.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Email/VariableResolver.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Email/EmailSendingService.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Cron/CronHandlerFactory.php` +- [ ] `C:\xampp\php\php.exe -l resources/views/settings/email-templates.php` +- [ ] Manual checkpoint wykonany (preview + realna wysylka) +- [ ] Dokumentacja zaktualizowana (`DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`, `DOCS/todo.md`) +- [ ] All acceptance criteria met + + + +- `przesylka.numer` i `przesylka.link_sledzenia` sa dostepne w szablonach e-mail. +- Resolver buduje link sledzenia zalezny od provider/carrier. +- Brak regresji w preview i wysylce e-mail. +- Dokumentacja techniczna opisuje nowe zmienne i przeplyw danych. + + + +After completion, create `.paul/phases/48-email-template-shipment-variables/48-01-SUMMARY.md` + diff --git a/.paul/phases/48-email-template-shipment-variables/48-01-SUMMARY.md b/.paul/phases/48-email-template-shipment-variables/48-01-SUMMARY.md new file mode 100644 index 0000000..5244e38 --- /dev/null +++ b/.paul/phases/48-email-template-shipment-variables/48-01-SUMMARY.md @@ -0,0 +1,137 @@ +--- +phase: 48-email-template-shipment-variables +plan: 01 +subsystem: ui +tags: [email-templates, shipment-tracking, variable-resolver] +requires: + - phase: 14-email-templates + provides: bazowy mechanizm zmiennych i edytor Quill + - phase: 27-shipment-tracking-backend + provides: model paczek i generator linkow sledzenia +provides: + - zmienne `{{przesylka.numer}}` i `{{przesylka.link_sledzenia}}` w szablonach e-mail + - resolver danych paczki oparty o `shipment_packages` + `DeliveryStatus::trackingUrl` +affects: [email-sending, settings-email-templates, shipments] +tech-stack: + added: [] + patterns: [provider-aware tracking link w mapowaniu zmiennych e-mail] +key-files: + created: [.paul/phases/48-email-template-shipment-variables/48-01-SUMMARY.md] + modified: [src/Modules/Email/VariableResolver.php, src/Modules/Settings/EmailTemplateController.php, resources/views/settings/email-templates.php, resources/scss/app.scss, public/assets/css/app.css, routes/web.php, src/Modules/Cron/CronHandlerFactory.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md, DOCS/todo.md] +key-decisions: + - "Dodanie zmiennych przesylki bez zmian schematu DB - dane pobierane z najnowszej paczki" + - "Link sledzenia liczony centralnie przez DeliveryStatus::trackingUrl(provider, tracking, carrier)" +patterns-established: + - "Nowe zmienne e-mail powinny miec preview (`SAMPLE_DATA`) i wpis w katalogu VARIABLE_GROUPS" +duration: 45min +started: 2026-03-28T15:05:00+01:00 +completed: 2026-03-28T15:43:37+01:00 +--- + +# Phase 48 Plan 01: Email Template Shipment Variables Summary + +Dodano obsluge numeru przesylki i linku sledzenia w szablonach e-mail wraz z poprawka UI dropdownu, aby lista zmiennych nie byla ucinana. + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | 45min | +| Started | 2026-03-28T15:05:00+01:00 | +| Completed | 2026-03-28T15:43:37+01:00 | +| Tasks | 3 completed + 1 checkpoint | +| Files modified | 10 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Szablony e-mail obsluguja nowe zmienne przesylki | Pass | `VARIABLE_GROUPS` i `SAMPLE_DATA` rozszerzone o `przesylka.*`; widoczne w pickerze | +| AC-2: Resolver pobiera dane przesylki z zamowienia | Pass | `VariableResolver` pobiera najnowsza paczke przez `findLatestByOrderId()` | +| AC-3: Link sledzenia jest zalezny od provider/carrier | Pass | Link liczony przez `DeliveryStatus::trackingUrl(...)`; fallback do pustego stringu | +| AC-4: Dokumentacja odzwierciedla nowy kontrakt zmiennych | Pass | Zaktualizowano `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`, `DOCS/todo.md` | + +## Accomplishments + +- Dodano dwie nowe zmienne szablonow e-mail: numer paczki i link sledzenia. +- Podlaczono resolver do danych paczki i logiki linkow provider/carrier. +- Naprawiono clipping dropdownu `Wstaw zmienna` (styl `overflow` + `z-index`). + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `.paul/phases/48-email-template-shipment-variables/48-01-SUMMARY.md` | Created | Podsumowanie wykonania planu | +| `src/Modules/Settings/EmailTemplateController.php` | Modified | Katalog zmiennych i dane preview (`przesylka.*`) | +| `src/Modules/Email/VariableResolver.php` | Modified | Pobranie najnowszej paczki i mapowanie zmiennych przesylki | +| `routes/web.php` | Modified | Wstrzykniecie `ShipmentPackageRepository` do `VariableResolver` | +| `src/Modules/Cron/CronHandlerFactory.php` | Modified | Wstrzykniecie `ShipmentPackageRepository` do `VariableResolver` w cronie | +| `resources/views/settings/email-templates.php` | Modified | Utrzymanie renderu zmiennych + usuniecie tymczasowego fallbacku | +| `resources/scss/app.scss` | Modified | Naprawa ucinania panelu zmiennych (`overflow: visible`, wyzszy `z-index`) | +| `public/assets/css/app.css` | Modified | Build CSS po zmianie SCSS | +| `DOCS/ARCHITECTURE.md` | Modified | Opis nowych zmiennych przesylki i fallbackow | +| `DOCS/TECH_CHANGELOG.md` | Modified | Log techniczny Phase 48 | +| `DOCS/todo.md` | Modified | Oznaczenie zakresu jako wykonany | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Resolver zmiennych pobiera ostatnia paczke po `order_id` | Najprostszy i stabilny punkt prawdy dla numeru/linku | Spójne podstawianie danych w send/preview | +| Uzycie `DeliveryStatus::trackingUrl` zamiast duplikacji mapowania URL | Jedno miejsce mapowania provider/carrier | Mniejsza duplikacja i latwiejsze utrzymanie | +| Poprawka UI dropdownu przez CSS (`overflow`, `z-index`) | Problem byl wizualny (clipping), nie danych | Picker zmiennych widoczny dla uzytkownika | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 1 | Niewielki, UX-only | +| Scope additions | 1 | Dodatkowa poprawka CSS konieczna do uzycia funkcji | +| Deferred | 0 | None | + +**Total impact:** Funkcjonalnosc dostarczona zgodnie z celem, z dodatkowa poprawka prezentacji panelu zmiennych. + +### Auto-fixed Issues + +**1. UI clipping panelu zmiennych** +- **Found during:** checkpoint manual verify +- **Issue:** Dropdown `Wstaw zmienna` byl ucinany przez kolejny element formularza. +- **Fix:** Zmieniono `overflow` kontenera edytora na `visible` oraz podniesiono `z-index` panelu. +- **Files:** `resources/scss/app.scss`, `public/assets/css/app.css` +- **Verification:** Potwierdzenie manualne uzytkownika ("Jst ok") + +### Deferred Items + +None. + +## Verification Results + +- `C:\xampp\php\php.exe -l src/Modules/Settings/EmailTemplateController.php` OK +- `C:\xampp\php\php.exe -l src/Modules/Email/VariableResolver.php` OK +- `C:\xampp\php\php.exe -l src/Modules/Email/EmailSendingService.php` OK +- `C:\xampp\php\php.exe -l src/Modules/Cron/CronHandlerFactory.php` OK +- `C:\xampp\php\php.exe -l resources/views/settings/email-templates.php` OK +- `C:\xampp\php\php.exe -l routes/web.php` OK +- Manual checkpoint: potwierdzony przez uzytkownika +## Skill Audit + +- Required `sonar-scanner`: not invoked in tym APPLY (gap zarejestrowany w STATE.md) + +## Next Phase Readiness + +**Ready:** +- Modul e-mail ma komplet zmiennych przesylki do szablonow i preview. +- UI picker zmiennych jest czytelny i nieuciety. + +**Concerns:** +- Brak uruchomionego `sonar-scanner` dla tej petli - do nadrobienia przy najblizszym cyklu jakosci. + +**Blockers:** +- None. + +--- +*Phase: 48-email-template-shipment-variables, Plan: 01* +*Completed: 2026-03-28* + diff --git a/.paul/phases/49-automation-history-tab/49-01-PLAN.md b/.paul/phases/49-automation-history-tab/49-01-PLAN.md new file mode 100644 index 0000000..8dde052 --- /dev/null +++ b/.paul/phases/49-automation-history-tab/49-01-PLAN.md @@ -0,0 +1,251 @@ +--- +phase: 49-automation-history-tab +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - database/migrations/20260328_000072_create_automation_execution_logs_table.sql + - src/Modules/Automation/AutomationExecutionLogRepository.php + - src/Modules/Automation/AutomationRepository.php + - src/Modules/Automation/AutomationService.php + - src/Modules/Automation/AutomationController.php + - resources/views/automation/form.php + - public/assets/js/modules/automation-form.js + - resources/views/automation/index.php + - resources/scss/modules/_automation.scss + - public/assets/css/app.css + - src/Modules/Cron/CronHandlerFactory.php + - src/Modules/Cron/AutomationHistoryCleanupHandler.php + - routes/web.php + - DOCS/DB_SCHEMA.md + - DOCS/ARCHITECTURE.md + - DOCS/TECH_CHANGELOG.md +autonomous: false +--- + + +## Goal +Rozdzielic ekran `/settings/automation` na dwa taby: `Ustawienia` (obecna lista i zarzadzanie regulami) oraz `Historia` (log wykonan automatyzacji z filtrami i paginacja), oraz dodac nowa akcje automatyzacji `zmien status zamowienia` z wyborem statusu docelowego. + +## Purpose +Operator musi widziec kiedy i na jakim zamowieniu uruchomila sie automatyzacja (audyt) oraz miec mozliwosc automatycznej zmiany statusu zamowienia przez regule bez recznej ingerencji. + +## Output +Nowa historia wykonan automatyzacji zapisywana w dedykowanej tabeli, UI historii w tabie `Historia` z filtrowaniem i paginacja, automatyczne czyszczenie wpisow starszych niz 30 dni oraz nowa akcja reguly `update_order_status` w formularzu automatyzacji. + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md +@DOCS/DB_SCHEMA.md +@DOCS/ARCHITECTURE.md + +## Prior Work (only if genuinely needed) +@.paul/phases/16-automated-tasks/16-02-SUMMARY.md +@.paul/phases/47-shipment-created-automation/47-01-SUMMARY.md + +## Source Files +@src/Modules/Automation/AutomationController.php +@src/Modules/Automation/AutomationRepository.php +@src/Modules/Automation/AutomationService.php +@resources/views/automation/form.php +@public/assets/js/modules/automation-form.js +@resources/views/automation/index.php +@resources/scss/modules/_automation.scss +@src/Modules/Cron/CronHandlerFactory.php +@src/Modules/Cron/CronRepository.php +@resources/views/settings/cron.php +@routes/web.php + + + +## Required Skills (from SPECIAL-FLOWS.md) + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| `sonar-scanner` | required | Po APPLY, przed UNIFY | o | +| /frontend-design | optional | Przy dopracowaniu ukladu tabow i tabeli historii | o | +| /code-review | optional | Po APPLY, przed UNIFY | o | + +**BLOCKING:** Required skills MUST be loaded before APPLY proceeds. + +## Skill Invocation Checklist +- [ ] `sonar-scanner` uruchomiony po APPLY +- [ ] /frontend-design (opcjonalnie) +- [ ] /code-review (opcjonalnie) + + + + + +## AC-1: Zakladka automatyzacji ma dwa taby z rozdzielonym zakresem +```gherkin +Given uzytkownik otwiera `/settings/automation` +When laduje sie ekran +Then widzi tab `Ustawienia` z obecna lista regul i akcjami CRUD +And widzi tab `Historia` z lista wykonan automatyzacji +``` + +## AC-2: Historia pokazuje co, kiedy i dla jakiego zamowienia +```gherkin +Given regula automatyzacji zostala wywolana przez trigger +When wykonanie reguly zakonczy sie sukcesem lub bledem +Then system zapisuje wpis historii zawierajacy co najmniej: event_type, rule_id/rule_name, order_id, status wykonania i timestamp +And wpis jest widoczny w tabie `Historia` +``` + +## AC-3: Historia wspiera filtrowanie i paginacje +```gherkin +Given tab `Historia` zawiera wpisy z wielu regul i zdarzen +When uzytkownik uzyje filtrow (np. event, status, rule, order_id, zakres dat) i zmieni strone +Then widok pokazuje tylko pasujace rekordy +And paginacja zachowuje aktywne filtry +``` + +## AC-4: Wpisy starsze niz 30 dni sa usuwane automatycznie +```gherkin +Given historia zawiera wpisy starsze niz 30 dni +When uruchomi sie harmonogram czyszczenia +Then wpisy starsze niz 30 dni zostana usuniete +And nowsze wpisy pozostaja bez zmian +``` + +## AC-5: Dokumentacja odzwierciedla nowy kontrakt historii automatyzacji +```gherkin +Given wdrozono nowa tabele i flow zapisu historii automatyzacji +When zespol czyta dokumentacje techniczna +Then `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md` i `DOCS/TECH_CHANGELOG.md` opisuja nowe elementy +``` + +## AC-6: Automatyzacja obsluguje akcje zmiany statusu zamowienia +```gherkin +Given uzytkownik tworzy lub edytuje regule na `/settings/automation/create` +When wybierze akcje `Zmien status zamowienia` i wskaze status docelowy +Then konfiguracja zapisuje sie poprawnie +And po triggerze reguly status zamowienia jest zmieniany na wskazany status +And aktualizacja statusu jest zapisana zgodnie z obecnym flow historii statusow/activity log +``` + + + + + + + Task 1: Dodaj model danych historii wykonania automatyzacji i retencje 30 dni + database/migrations/20260328_000072_create_automation_execution_logs_table.sql, src/Modules/Automation/AutomationExecutionLogRepository.php, src/Modules/Cron/AutomationHistoryCleanupHandler.php, src/Modules/Cron/CronHandlerFactory.php + + Dodaj migracje tworzaca tabele historii wykonan automatyzacji (`automation_execution_logs`) z indeksami pod filtry i sortowanie po czasie. + Dodaj repozytorium historii z metodami: insert wpisu wykonania, paginowane pobieranie z filtrami, count, purgeOlderThanDays(30). + Dodaj handler crona do cyklicznego usuwania starych wpisow i podlacz go w `CronHandlerFactory`. + Dodaj seed harmonogramu cleanup (`automation_history_cleanup`) tak, aby dzialal automatycznie bez recznej konfiguracji. + Trzymaj sie medoo/prepared statements i bez laczenia SQL stringiem przez wartosci dynamiczne. + + rg -n "automation_execution_logs|automation_history_cleanup|purgeOlderThanDays|AutomationHistoryCleanupHandler" database/migrations src/Modules/Automation src/Modules/Cron + AC-2 satisfied i AC-4 satisfied: dane historii sa zapisywane i maja automatyczna retencje 30 dni. + + + + Task 2: Rozszerz AutomationService i kontroler o zapis historii oraz endpoint listowania + src/Modules/Automation/AutomationService.php, src/Modules/Automation/AutomationController.php, src/Modules/Automation/AutomationRepository.php, routes/web.php + + W `AutomationService::trigger` dodaj rejestracje wykonania per regula (co najmniej: event_type, rule_id, rule_name, order_id, result_status, result_message, executed_at), z logowaniem sukcesu i bledow bez zatrzymywania kolejnych regul. + Rozszerz `AutomationController::index` o obsluge query filtrow i paginacji historii oraz przekazanie danych do widoku. + Dodaj potrzebne metody repository do listowania filtrow pomocniczych (np. aktywne reguly/eventy) jesli sa potrzebne do UI. + Zadbaj, aby filtry/paginacja historii nie ingerowaly w istniejaca liste regul i zachowaly kompatybilnosc obecnych akcji CRUD. + + rg -n "execution log|history|history_page|history_filters|logExecution|automation history" src/Modules/Automation routes/web.php + AC-2 satisfied i AC-3 satisfied: backend dostarcza pelna historie z filtrowaniem i paginacja. + + + + Task 3: Dodaj akcje `update_order_status` do automatyzacji (backend + formularz) + src/Modules/Automation/AutomationController.php, src/Modules/Automation/AutomationService.php, resources/views/automation/form.php, public/assets/js/modules/automation-form.js + + Rozszerz `ALLOWED_ACTION_TYPES` o nowa akcje `update_order_status` oraz walidacje konfiguracji tej akcji. + W `AutomationService` dodaj wykonanie akcji aktualizacji statusu zamowienia przez istniejacy, centralny flow domenowy (z zachowaniem wpisu historii statusow i activity log). + W formularzu automatyzacji dodaj nowa opcje akcji `Zmien status zamowienia` oraz selector statusu docelowego oparty o aktywne statusy orderPRO. + Utrzymaj zgodnosc create/edit i dynamicznego JS dodajacego kolejne akcje. + + rg -n "update_order_status|Zmien status zamowienia|order_status|action_type" src/Modules/Automation resources/views/automation public/assets/js/modules/automation-form.js + AC-6 satisfied: nowa akcja jest konfigurowalna i zmienia status zamowienia. + + + + Task 4: Zbuduj UI dwoch tabow (Ustawienia/Historia) i zaktualizuj dokumentacje + resources/views/automation/index.php, resources/scss/modules/_automation.scss, public/assets/css/app.css, DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md + + Przebuduj widok `/settings/automation` na taby zgodne ze stylem projektu (`content-tabs-nav` / `content-tab-panel`): + - tab `Ustawienia`: zachowuje obecna liste regul i akcje, + - tab `Historia`: tabela historii + formularz filtrow + paginacja. + Zachowaj kompaktowy layout (male odstepy, duza gestosc informacji) i brak natywnych `alert()/confirm()`. + Dodaj/uzupelnij style SCSS tylko w plikach SCSS, a nastepnie przebuduj CSS. + Zaktualizuj dokumentacje techniczna o nowa tabele historii oraz nowa akcje `update_order_status`. + + rg -n "content-tabs-nav|Historia|Ustawienia|automation history|30 dni|automation_execution_logs|update_order_status" resources/views/automation/index.php resources/scss/modules/_automation.scss DOCS/DB_SCHEMA.md DOCS/ARCHITECTURE.md DOCS/TECH_CHANGELOG.md + AC-1 satisfied i AC-5 satisfied: UI i dokumentacja sa zgodne z nowa funkcjonalnoscia. + + + + Tab `Historia` na `/settings/automation` z filtrowaniem i paginacja, automatyczna retencja wpisow 30 dni oraz akcja `Zmien status zamowienia`. + + 1. Otworz: `https://orderpro.projectpro.pl/settings/automation`. + 2. Potwierdz obecnosc tabow: `Ustawienia` i `Historia`. + 3. W tabie `Historia` ustaw filtry (event, status, order ID lub zakres dat) i sprawdz czy lista oraz licznik/paginacja sa zgodne. + 4. Wywolaj testowo regule automatyzacji i potwierdz pojawienie sie nowego wpisu (co, kiedy, jakie zamowienie). + 5. W `Ustawienia > Zadania automatyczne > Dodaj zadanie` wybierz akcje `Zmien status zamowienia`, ustaw status docelowy i zapisz regule. + 6. Wyzwol regule i potwierdz realna zmiane statusu zamowienia na wybrany status. + 7. Potwierdz dzialanie cleanupu wpisow >30 dni (przez uruchomienie joba cron lub test na danych historycznych). + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- Logika tworzenia/edycji reguly automatyzacji poza koniecznymi zmianami pod nowe taby. +- Istniejace flow wysylki e-mail/paragonow/statusow przesylki poza dodaniem logowania wykonan. +- Istniejace flow zmiany statusu zamowienia poza rozszerzeniem go jako nowej akcji automatyzacji. +- Konfiguracja runtime DB host (`DB_HOST` pozostaje runtime; bez podpinania `DB_HOST_REMOTE`). + +## SCOPE LIMITS +- Zakres dotyczy tylko ekranu `/settings/automation` i warstwy historii automatyzacji. +- Zakres obejmuje dodanie jednej nowej akcji automatyzacji: `update_order_status`. +- Bez zmian w innych ekranach ustawien. +- Bez dodatkowych funkcji biznesowych automatyzacji niezwizanych z historia. + + + + +Before declaring plan complete: +- [ ] `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationController.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationRepository.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationExecutionLogRepository.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationService.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Cron/CronHandlerFactory.php` +- [ ] `C:\xampp\php\php.exe -l src/Modules/Cron/AutomationHistoryCleanupHandler.php` +- [ ] `C:\xampp\php\php.exe -l resources/views/automation/form.php` +- [ ] `C:\xampp\php\php.exe -l resources/views/automation/index.php` +- [ ] Migracja wykonuje sie poprawnie i tworzy indeksy historii +- [ ] Test paginacji/filtrow historii przechodzi +- [ ] Test akcji `update_order_status` przechodzi (zapis + wykonanie + zmiana statusu) +- [ ] Test cleanupu wpisow >30 dni przechodzi +- [ ] Dokumentacja zaktualizowana (`DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`) +- [ ] All acceptance criteria met + + + +- `/settings/automation` ma dwa taby: `Ustawienia` i `Historia`. +- Historia pokazuje wykonania regul (co, kiedy, order) i ma filtry + paginacje. +- Akcja `Zmien status zamowienia` jest dostepna i poprawnie zmienia status na wybrany przez uzytkownika. +- Wpisy starsze niz 30 dni sa usuwane automatycznie. +- Dokumentacja techniczna odzwierciedla nowy model i przeplyw. + + + +After completion, create `.paul/phases/49-automation-history-tab/49-01-SUMMARY.md` + diff --git a/.paul/phases/49-automation-history-tab/49-01-SUMMARY.md b/.paul/phases/49-automation-history-tab/49-01-SUMMARY.md new file mode 100644 index 0000000..bd11692 --- /dev/null +++ b/.paul/phases/49-automation-history-tab/49-01-SUMMARY.md @@ -0,0 +1,147 @@ +--- +phase: 49-automation-history-tab +plan: 01 +subsystem: automation +tags: [automation, history, cron, tabs, update_order_status] + +requires: + - phase: 47-shipment-created-automation + provides: automation trigger shipment.created i akcje automatyzacji +provides: + - historia wykonan automatyzacji z filtrami i paginacja + - retencja historii 30 dni przez cron cleanup + - akcja automatyzacji update_order_status +affects: [settings-automation, cron, automation-service] + +tech-stack: + added: [] + patterns: [execution-log-per-rule, tabbed-settings-view, safe-automation-action-validation] + +key-files: + created: + - database/migrations/20260328_000072_create_automation_execution_logs_table.sql + - src/Modules/Automation/AutomationExecutionLogRepository.php + - src/Modules/Cron/AutomationHistoryCleanupHandler.php + modified: + - src/Modules/Automation/AutomationController.php + - src/Modules/Automation/AutomationRepository.php + - src/Modules/Automation/AutomationService.php + - resources/views/automation/index.php + - resources/views/automation/form.php + - public/assets/js/modules/automation-form.js + +key-decisions: + - "Historia automatyzacji zapisywana per regula (success/failed) bez blokowania glownego flow" + - "Nowa akcja update_order_status korzysta z centralnego OrdersRepository::updateOrderStatus" + - "Retencja historii realizowana cronem automation_history_cleanup (30 dni)" + +patterns-established: + - "Tab settings/history z zapamietywaniem aktywnej zakladki" + - "Walidacja configu akcji automatyzacji po stronie kontrolera" + +duration: ~55min +started: 2026-03-28T13:50:03+01:00 +completed: 2026-03-28T14:45:16+01:00 +--- + +# Phase 49 Plan 01: Automation History + Update Order Status Summary + +**Dostarczono rozdzielenie `/settings/automation` na `Ustawienia/Historia`, audyt historii wykonan regul i akcje `update_order_status`, a dodatkowo po testach wdrozono hotfix fallbacku daty pickup dla Apaczka.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~55 min | +| Started | 2026-03-28T13:50:03+01:00 | +| Completed | 2026-03-28T14:45:16+01:00 | +| Tasks | 4 completed | +| Files modified | 16+ | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Dwa taby na `/settings/automation` | Pass | `resources/views/automation/index.php` ma taby `Ustawienia` i `Historia` | +| AC-2: Log co/kiedy/jakie zamowienie | Pass | `AutomationService::logExecution()` + `automation_execution_logs` | +| AC-3: Filtry + paginacja historii | Pass | `AutomationController::index` + `AutomationExecutionLogRepository::paginate/count` | +| AC-4: Auto-usuwanie wpisow >30 dni | Pass | cron `automation_history_cleanup` + `purgeOlderThanDays()` | +| AC-5: Dokumentacja techniczna | Pass | zaktualizowane `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md` | +| AC-6: Akcja `update_order_status` | Pass | formularz + walidacja + wykonanie przez `OrdersRepository::updateOrderStatus` | + +## Verification Results + +- `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationController.php` -> OK +- `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationRepository.php` -> OK +- `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationExecutionLogRepository.php` -> OK +- `C:\xampp\php\php.exe -l src/Modules/Automation/AutomationService.php` -> OK +- `C:\xampp\php\php.exe -l src/Modules/Cron/CronHandlerFactory.php` -> OK +- `C:\xampp\php\php.exe -l src/Modules/Cron/AutomationHistoryCleanupHandler.php` -> OK +- `C:\xampp\php\php.exe -l resources/views/automation/form.php` -> OK +- `C:\xampp\php\php.exe -l resources/views/automation/index.php` -> OK +- `sonar-scanner` uruchomiony pomyslnie (project `orderPRO`, dashboard updated) + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `database/migrations/20260328_000072_create_automation_execution_logs_table.sql` | Created | tabela historii + seed cleanup cron | +| `src/Modules/Automation/AutomationExecutionLogRepository.php` | Created | zapis/listowanie/paginacja/purge historii | +| `src/Modules/Cron/AutomationHistoryCleanupHandler.php` | Created | czyszczenie wpisow >30 dni | +| `src/Modules/Cron/CronHandlerFactory.php` | Modified | podpiety handler cleanup | +| `src/Modules/Automation/AutomationController.php` | Modified | backend tabu historii + nowa akcja update_order_status | +| `src/Modules/Automation/AutomationRepository.php` | Modified | sortowanie nazw regul + statusy zamowien do akcji | +| `src/Modules/Automation/AutomationService.php` | Modified | log execution + wykonanie update_order_status | +| `resources/views/automation/index.php` | Modified | taby, filtry i paginacja historii | +| `resources/views/automation/form.php` | Modified | UI akcji `Zmiana statusu zamowienia` | +| `public/assets/js/modules/automation-form.js` | Modified | dynamiczna obsluga nowej akcji | +| `DOCS/DB_SCHEMA.md` | Modified | kontrakt tabeli historii i zmiany flow | +| `DOCS/ARCHITECTURE.md` | Modified | opis nowego flow i akcji | +| `DOCS/TECH_CHANGELOG.md` | Modified | chronologia wdrozenia | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Log historii per regula (success/failed) | pelny audyt bez przerywania triggera | szybsza diagnostyka automatyzacji | +| `update_order_status` przez `OrdersRepository::updateOrderStatus` | reuse centralnego flow status history + activity log | brak duplikacji logiki i spojnosc domenowa | +| Cleanup historii jako cron | retencja bez manualnej obslugi | stabilny rozmiar danych | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 0 | None | +| Scope additions | 1 | Niski, uzasadniony produkcyjnym bledem | +| Deferred | 0 | None | + +### Scope addition + +- Po APPLY i testach produkcyjnych dodano hotfix `ApaczkaShipmentService`: + - fallback retry dla bledow niedostepnego dnia pickup (`Pickup not available...` oraz `you can't place an order today`), + - automatyczne przesuwanie `pickup.date` na kolejny dzien roboczy (max 7 prob). +- Powod: rzeczywisty blad operacyjny blokujacy tworzenie przesylek. + +## Skill Audit + +- Required `sonar-scanner`: invoked ✓ +- Optional `/code-review`: not invoked +- Optional `/frontend-design`: not invoked + +## Next Phase Readiness + +**Ready:** +- Automatyzacja ma audytowalna historie z retencja. +- Reguly obsluguja automatyczna zmiane statusu zamowienia. + +**Concerns:** +- Warto rozwazyc logowanie rowniez przypadkow `skipped` (warunki niespelnione), aby ulatwic debug bez diagnostyki DB. + +**Blockers:** +- None + +--- +*Phase: 49-automation-history-tab, Plan: 01* +*Completed: 2026-03-28* diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt index fd2d494..d17bf77 100644 --- a/.scannerwork/report-task.txt +++ b/.scannerwork/report-task.txt @@ -2,5 +2,5 @@ projectKey=orderPRO serverUrl=https://sonar.project-pro.pl serverVersion=26.3.0.120487 dashboardUrl=https://sonar.project-pro.pl/dashboard?id=orderPRO -ceTaskId=37b32633-2562-4240-8b42-c6c993262727 -ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=37b32633-2562-4240-8b42-c6c993262727 +ceTaskId=995489dc-a44a-4b15-a2e9-d992a8884994 +ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=995489dc-a44a-4b15-a2e9-d992a8884994 diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index a316531..5ac03a9 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -19,358 +19,6 @@ "lmtime": 1772490697553, "modified": false }, - "archive": { - "2026-03-02_users-only-reset": { - "resources": { - "views": { - "dashboard": { - "index.php": { - "type": "-", - "size": 315, - "lmtime": 1771866989000, - "modified": false - } - }, - "marketplace": { - "index.php": { - "type": "-", - "size": 1669, - "lmtime": 1771922314000, - "modified": false - }, - "offers.php": { - "type": "-", - "size": 19158, - "lmtime": 1772397952604, - "modified": false - } - }, - "orders": { - "index.php": { - "type": "-", - "size": 710, - "lmtime": 1772490033013, - "modified": false - } - }, - "products": { - "create.php": { - "type": "-", - "size": 7204, - "lmtime": 1771868875000, - "modified": false - }, - "edit.php": { - "type": "-", - "size": 27948, - "lmtime": 1772397133535, - "modified": false - }, - "index.php": { - "type": "-", - "size": 11064, - "lmtime": 1771956268000, - "modified": false - }, - "links.php": { - "type": "-", - "size": 13765, - "lmtime": 1771954576000, - "modified": false - }, - "show.php": { - "type": "-", - "size": 9854, - "lmtime": 1772220108000, - "modified": false - } - }, - "settings": { - "cron.php": { - "type": "-", - "size": 7180, - "lmtime": 1772485558106, - "modified": false - }, - "database.php": { - "type": "-", - "size": 4478, - "lmtime": 1772485529509, - "modified": false - }, - "gs1.php": { - "type": "-", - "size": 3499, - "lmtime": 1772485576494, - "modified": false - }, - "integrations.php": { - "type": "-", - "size": 11056, - "lmtime": 1772488994330, - "modified": false - }, - "order-statuses.php": { - "type": "-", - "size": 5566, - "lmtime": 1772485520769, - "modified": false - }, - "products.php": { - "type": "-", - "size": 2225, - "lmtime": 1772485593115, - "modified": false - } - } - } - }, - "src": { - "Modules": { - "Cron": { - "CronJobProcessor.php": { - "type": "-", - "size": 6385, - "lmtime": 1771954453000, - "modified": false - }, - "CronJobRepository.php": { - "type": "-", - "size": 17045, - "lmtime": 1771954938000, - "modified": false - }, - "CronJobType.php": { - "type": "-", - "size": 1231, - "lmtime": 1772489146286, - "modified": false - }, - "ProductLinksHealthCheckHandler.php": { - "type": "-", - "size": 5247, - "lmtime": 1771954535000, - "modified": false - }, - "ShopProOfferTitlesRefreshHandler.php": { - "type": "-", - "size": 3788, - "lmtime": 1772397918784, - "modified": false - }, - "ShopProOrdersImportHandler.php": { - "type": "-", - "size": 536, - "lmtime": 1772484067565, - "modified": false - }, - "ShopProOrderStatusSyncHandler.php": { - "type": "-", - "size": 528, - "lmtime": 1772489139382, - "modified": false - } - }, - "GS1": { - "GS1Service.php": { - "type": "-", - "size": 2412, - "lmtime": 1772132619000, - "modified": false - }, - "MojeGS1Client.php": { - "type": "-", - "size": 6727, - "lmtime": 1771961979000, - "modified": false - } - }, - "Marketplace": { - "MarketplaceController.php": { - "type": "-", - "size": 28819, - "lmtime": 1772398277623, - "modified": false - }, - "MarketplaceRepository.php": { - "type": "-", - "size": 10298, - "lmtime": 1772398268053, - "modified": false - } - }, - "Orders": { - "OrderImportService.php": { - "type": "-", - "size": 21009, - "lmtime": 1772490222940, - "modified": false - }, - "OrdersController.php": { - "type": "-", - "size": 35423, - "lmtime": 1772490255436, - "modified": false - }, - "OrdersRepository.php": { - "type": "-", - "size": 25665, - "lmtime": 1772489045864, - "modified": false - }, - "OrderStatusSyncService.php": { - "type": "-", - "size": 17295, - "lmtime": 1772489130897, - "modified": false - } - }, - "ProductLinks": { - "ChannelOffersRepository.php": { - "type": "-", - "size": 10755, - "lmtime": 1771954497000, - "modified": false - }, - "LinkMatcherService.php": { - "type": "-", - "size": 1893, - "lmtime": 1771882685000, - "modified": false - }, - "OfferImportService.php": { - "type": "-", - "size": 8091, - "lmtime": 1771954510000, - "modified": false - }, - "ProductLinksController.php": { - "type": "-", - "size": 5392, - "lmtime": 1771882733000, - "modified": false - }, - "ProductLinksRepository.php": { - "type": "-", - "size": 20901, - "lmtime": 1771954562000, - "modified": false - }, - "ProductLinksService.php": { - "type": "-", - "size": 14754, - "lmtime": 1771927037000, - "modified": false - } - }, - "Products": { - "ProductRepository.php": { - "type": "-", - "size": 29887, - "lmtime": 1772395707501, - "modified": false - }, - "ProductsController.php": { - "type": "-", - "size": 49058, - "lmtime": 1772395718310, - "modified": false - }, - "ProductService.php": { - "type": "-", - "size": 17193, - "lmtime": 1772395136766, - "modified": false - }, - "ProductSkuGenerator.php": { - "type": "-", - "size": 3044, - "lmtime": 1772395702627, - "modified": false - }, - "ProductValidator.php": { - "type": "-", - "size": 3675, - "lmtime": 1771868735000, - "modified": false - }, - "ShopProExportService.php": { - "type": "-", - "size": 45644, - "lmtime": 1772395159115, - "modified": false - } - }, - "Settings": { - "AppSettingsRepository.php": { - "type": "-", - "size": 1905, - "lmtime": 1771954924000, - "modified": false - }, - "IntegrationRepository.php": { - "type": "-", - "size": 25754, - "lmtime": 1772488971508, - "modified": false - }, - "OrderStatusMappingRepository.php": { - "type": "-", - "size": 4135, - "lmtime": 1772489019745, - "modified": false - }, - "SettingsController.php": { - "type": "-", - "size": 73812, - "lmtime": 1772488985859, - "modified": false - }, - "ShopProClient.php": { - "type": "-", - "size": 40035, - "lmtime": 1772490209403, - "modified": false - } - } - } - }, - "bin": { - "cron.php": { - "type": "-", - "size": 4062, - "lmtime": 1772489168039, - "modified": false - } - }, - "tests": { - "Unit": { - "Cron": { - "CronJobTypeTest.php": { - "type": "-", - "size": 603, - "lmtime": 1772489500486, - "modified": false - } - }, - "Settings": { - "OrderStatusMappingRepositoryTest.php": { - "type": "-", - "size": 2415, - "lmtime": 1772489512491, - "modified": false - }, - "ShopProClientTest.php": { - "type": "-", - "size": 972, - "lmtime": 1772489519995, - "modified": false - } - } - } - } - } - }, "bin": { "build-assets.php": { "type": "-", @@ -914,6 +562,12 @@ "size": 538, "lmtime": 1774304095531, "modified": false + }, + "20260327_000071_add_last_status_pushed_at_to_sync_state.sql": { + "type": "-", + "size": 319, + "lmtime": 1774611787688, + "modified": false } }, "seeders": {}, @@ -956,6 +610,12 @@ "lmtime": 1772490689218, "modified": false }, + "delivery-tab-bug.png": { + "type": "-", + "size": 124327, + "lmtime": 1774565855738, + "modified": false + }, "deploy-vendor.php": { "type": "-", "size": 2097, @@ -965,14 +625,14 @@ "DOCS": { "ARCHITECTURE.md": { "type": "-", - "size": 35120, - "lmtime": 1774475884811, + "size": 35558, + "lmtime": 1774612062257, "modified": false }, "DB_SCHEMA.md": { "type": "-", - "size": 29737, - "lmtime": 1773789666999, + "size": 29871, + "lmtime": 1774612041000, "modified": false }, "ORDERS_SCHEMA_APILO_DRAFT.md": { @@ -995,8 +655,8 @@ }, "TECH_CHANGELOG.md": { "type": "-", - "size": 57304, - "lmtime": 1774475891183, + "size": 57684, + "lmtime": 1774612077539, "modified": false }, "todo.md": { @@ -2258,8 +1918,8 @@ "css": { "app.css": { "type": "-", - "size": 43993, - "lmtime": 1774474931663, + "size": 44903, + "lmtime": 1774600385594, "modified": false }, "app.css.map": { @@ -2298,6 +1958,12 @@ "lmtime": 1774475530521, "modified": false }, + "inline-status-change.js": { + "type": "-", + "size": 6603, + "lmtime": 1774600361548, + "modified": false + }, "jquery-alerts.js": { "type": "-", "size": 5768, @@ -2361,8 +2027,8 @@ }, "app.scss": { "type": "-", - "size": 42711, - "lmtime": 1774304207044, + "size": 43794, + "lmtime": 1774600368218, "modified": false }, "login.css": { @@ -2499,8 +2165,8 @@ }, "list.php": { "type": "-", - "size": 1603, - "lmtime": 1774473665048, + "size": 2015, + "lmtime": 1774599283649, "modified": false }, "partials": { @@ -2957,8 +2623,8 @@ }, "CronHandlerFactory.php": { "type": "-", - "size": 7691, - "lmtime": 1774475612061, + "size": 7970, + "lmtime": 1774612020782, "modified": false }, "CronJobProcessor.php": { @@ -3103,8 +2769,8 @@ }, "OrdersController.php": { "type": "-", - "size": 33087, - "lmtime": 1774473628426, + "size": 34671, + "lmtime": 1774599151246, "modified": false }, "OrdersRepository.php": { @@ -3433,9 +3099,9 @@ }, "ShopproApiClient.php": { "type": "-", - "size": 9991, - "lmtime": 1772996784239, - "modified": true + "size": 12582, + "lmtime": 1774612664232, + "modified": false }, "ShopProClient.php": { "type": "-", @@ -3475,8 +3141,8 @@ }, "ShopproOrderSyncStateRepository.php": { "type": "-", - "size": 8941, - "lmtime": 0, + "size": 10418, + "lmtime": 1774611852363, "modified": false }, "ShopproPaymentStatusSyncService.php": { @@ -3499,8 +3165,8 @@ }, "ShopproStatusSyncService.php": { "type": "-", - "size": 2075, - "lmtime": 1773397552472, + "size": 9585, + "lmtime": 1774611928171, "modified": false } }, @@ -5644,6 +5310,12 @@ "lmtime": 1772995312041, "modified": false }, + "psd_personalize.py": { + "type": "-", + "size": 42084, + "lmtime": 1774652966314, + "modified": false + }, "resync_shoppro_6_once.php": { "type": "-", "size": 2930, @@ -5856,12 +5528,6 @@ "phpmailer": { "phpmailer": {} } - }, - "delivery-tab-bug.png": { - "type": "-", - "size": 124327, - "lmtime": 1774565855738, - "modified": false } } }, diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index 34f1c80..bf9a5f2 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -8,6 +8,7 @@ - Automatyzacja obsluguje zdarzenia `shipment.created` (natychmiast po utworzeniu paczki) i `shipment.status_changed` (po realnej zmianie statusu dostawy), oraz warunek `shipment_status` oparty o statusy biznesowe. - Automatyzacja obsluguje akcje `issue_receipt` (Wystaw paragon) z parametrami: `receipt_config_id`, `issue_date_mode`, `duplicate_policy`. - Automatyzacja obsluguje akcje `update_shipment_status` (Zmiana statusu przesylki) z parametrem `status_key` mapowanym na techniczny `delivery_status`. +- Automatyzacja obsluguje akcje `update_order_status` (Zmiana statusu zamowienia) z parametrem `status_code` (aktywny kod z `order_statuses`). - Orkiestracja automatyzacji obsluguje chain events: akcja moze emitowac kolejne zdarzenie (`emitEvent`), a engine propaguje wspolny kontekst lancucha. - Zabezpieczenia chain automation (dla obecnych i przyszlych eventow): - limit glebokosci lancucha (`MAX_CHAIN_DEPTH`), @@ -15,6 +16,10 @@ - limit historii wykonan w kontekście (`MAX_CHAIN_EXECUTIONS`). - `ShipmentTrackingHandler` triggeruje automatyzacje tylko po zmianie `delivery_status` i przekazuje kontekst (`package_id`, `provider`, `delivery_status`, `delivery_status_raw`, `previous_status`). - Kolejka wydruku ma akcje usuwania wpisu przez route `POST /settings/printing/jobs/delete` (CSRF + `OrderProAlerts.confirm`). +- Szablony e-mail obsluguja zmienne przesylki: + - `{{przesylka.numer}}` -> `shipment_packages.tracking_number` (najnowsza paczka zamowienia), + - `{{przesylka.link_sledzenia}}` -> `DeliveryStatus::trackingUrl(provider, tracking_number, carrier_id)`, + - fallback: gdy brak paczki lub tracking number, wartosci sa puste. ## Moduly aktywne - `App\Modules\Auth` @@ -43,6 +48,7 @@ - `GET /settings` (redirect do `/settings/users`) - `GET /settings/database` - `POST /settings/database/migrate` +- `GET /cron` (publiczny trigger crona HTTPS, autoryzacja tokenem) - `GET /settings/statuses` - `POST /settings/status-groups` - `POST /settings/status-groups/update` @@ -158,7 +164,7 @@ - `App\Modules\Accounting\AccountingController` (index — lista paragonow, export — XLSX) - `App\Modules\Automation\AutomationController` (index, create, store, edit, update, destroy, toggleStatus) - `App\Modules\Automation\AutomationRepository` (findAll, findById, create, update, delete, toggleActive, findActiveByEvent) -- `App\Modules\Automation\AutomationService` (trigger, evaluateConditions, executeActions — watcher/executor regul automatyzacji; flow: ReceiptController::store() -> trigger('receipt.created'), ShipmentController::create()/createManual() -> trigger('shipment.created', context), ShipmentTrackingHandler::handle() -> trigger('shipment.status_changed', context) -> ewaluacja warunkow -> akcje: EmailSendingService::send() / auto issue_receipt / update_shipment_status) +- `App\Modules\Automation\AutomationService` (trigger, evaluateConditions, executeActions — watcher/executor regul automatyzacji; flow: ReceiptController::store() -> trigger('receipt.created'), ShipmentController::create()/createManual() -> trigger('shipment.created', context), ShipmentTrackingHandler::handle() -> trigger('shipment.status_changed', context) -> ewaluacja warunkow -> akcje: EmailSendingService::send() / auto issue_receipt / update_shipment_status / update_order_status) - `App\Modules\Shipments\ShipmentProviderInterface` - `App\Modules\Shipments\ShipmentProviderRegistry` - `App\Modules\Shipments\ApaczkaShipmentService` @@ -294,8 +300,13 @@ ## Przeplyw wykonania crona - `bin/cron.php`: - laduje aplikacje i uruchamia `CronRunner::run($limit)`. +- `GET /cron?token=...`: + - publiczny trigger uruchamiany z zewnetrznego crona HTTPS, + - waliduje token `CRON_PUBLIC_TOKEN`, + - uruchamia `CronRunner::run($limit)` z limitem opartym o `cron_web_limit`. - `App\Core\Application::maybeRunCronOnWeb(Request): void`: - przy wlaczonej opcji `cron_run_on_web=1` uruchamia `CronRunner` podczas requestu HTTP, + - pomija endpointy `/cron` i `/cron/*` (dedykowany trigger uruchamia cron recznie, bez podwojenia), - stosuje throttling sesyjny i lock DB (`GET_LOCK`) zeby uniknac wielu rownoleglych workerow. - `CronRunner`: - dispatchuje due schedule z `cron_schedules` do `cron_jobs`, @@ -322,6 +333,7 @@ - wybiera providera dynamicznie po `provider_code` i deleguje do `ShipmentProviderInterface::createShipment(...)`, - po sukcesie tworzenia paczki triggeruje automatyzacje `shipment.created` z kontekstem paczki (`package_id`, `provider`, `tracking_number`, `package_status`, `delivery_status`), - dla `apaczka` waliduje wymagane punkty odbioru/nadania wg definicji uslugi (`service_structure`) i przy bledzie wyceny zwraca rozszerzona diagnostyke parametrow, + - dla `apaczka` przy bledzie API niedostepnego dnia nadania (np. `Pickup not available for selected day` lub `you can't place an order today`) w trybie `COURIER` serwis automatycznie ponawia `order_send` z `pickup.date` przesunieta na kolejny dzien roboczy (max 7 przesuniec), - `apaczka` uzupelnia i wysyla `contact_person` dla nadawcy (z `Ustawienia > Dane firmy`) i odbiorcy (fallback z danych zamowienia), - `apaczka` ustawia jawnie `pickup.type` (`SELF`/`COURIER`) na podstawie uslugi i obecnosci `sender_point_id`; dla `COURIER` dopelnia tez `pickup.date`, `pickup.hours_from`, `pickup.hours_to`, - dla uslug punktowych `apaczka` payload adresu zawiera aliasy identyfikatora punktu (`point`, `foreign_address_id`, `point_id`) dla nadania i odbioru, @@ -548,3 +560,24 @@ - laduje formy dostawy wykryte w zamowieniach danej instancji (`orders.source=shoppro` + `orders.integration_id`), - laduje uslugi dostawy z Allegro API (`delivery-services`) z fallbackiem na odswiezenie tokenu OAuth, - zapisuje mapowanie: forma dostawy shopPRO -> usluga Allegro/InPost WZA. + +## Przeplyw Ustawienia > Zadania automatyczne (aktualizacja 2026-03-28) +- `GET /settings/automation`: + - `AutomationController::index(Request): Response` renderuje taby `Ustawienia` i `Historia`. + - tab `Ustawienia` zawiera istniejacy CRUD regul automatyzacji. + - tab `Historia` pokazuje wpisy z `automation_execution_logs` z filtrami (`event_type`, `execution_status`, `rule_id`, `order_id`, `date_from`, `date_to`) i paginacja (`history_page`, 25/strona). +- `AutomationService::trigger(...)` zapisuje historie wykonania reguly: + - status `success` po wykonaniu akcji, + - status `failed` przy wyjatku podczas wykonania reguly, + - zapisywany kontrakt: `event_type`, `rule_id`, `rule_name`, `order_id`, `execution_status`, `result_message`, `executed_at`, `context_json`. +- Akcja `update_order_status`: + - konfiguracja reguly przechowuje `status_code`, + - wykonanie delegowane do `OrdersRepository::updateOrderStatus(...)` (wspolny flow historii statusow i activity logu), + - aktor zmiany: `system` / `Automatyzacja: `. +- Retencja historii: + - cron job `automation_history_cleanup` wywoluje `AutomationHistoryCleanupHandler::handle(...)`, + - handler usuwa wpisy starsze niz N dni (domyslnie 30) przez `AutomationExecutionLogRepository::purgeOlderThanDays(...)`. + +## Klasy (aktualizacja 2026-03-28) +- `App\Modules\Automation\AutomationExecutionLogRepository` (create, paginate, count, listEventTypes, purgeOlderThanDays). +- `App\Modules\Cron\AutomationHistoryCleanupHandler` (cleanup retencji historii automatyzacji). diff --git a/DOCS/DB_SCHEMA.md b/DOCS/DB_SCHEMA.md index d16b471..ca33459 100644 --- a/DOCS/DB_SCHEMA.md +++ b/DOCS/DB_SCHEMA.md @@ -97,6 +97,13 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane - 2026-03-15: Dodano migracje `20260315_000056_create_email_logs_table.sql` — tabela logow wyslanych wiadomosci z FK do email_templates, email_mailboxes i indeksami na order_id, status, sent_at. - 2026-03-16: Dodano migracje `20260316_000001_add_attachment1_to_email_templates.sql` — kolumna attachment_1 VARCHAR(50) w email_templates (typ zalacznika, np. 'receipt'). - 2026-03-17: Nowa zaleznosc `phpmailer/phpmailer` v7.0.2. Modul `App\Modules\Email` — wysylka e-mail z zamowien, resolwer zmiennych, generowanie zalacznikow PDF. Tabela `email_logs` wykorzystywana do logowania wysylek (bez nowych migracji). +- 2026-03-28: Rozszerzono automatyzacje o akcje `update_order_status` (zmiana statusu zamowienia) - bez zmian schematu (wykorzystuje istniejace `order_statuses`, `orders`, `order_status_history`, `order_activity_log`). +- 2026-03-28: Hotfix Apaczka bledow niedostepnego dnia nadania (`Pickup not available for selected day` oraz `you can't place an order today`) - bez zmian schematu (retry `order_send` z automatycznym przesuwaniem `pickup.date` dla `pickup.type=COURIER`). +- 2026-03-28: Dodano publiczny endpoint triggera crona HTTPS (`/cron`) z tokenem `CRON_PUBLIC_TOKEN` - bez zmian schematu bazy. +- 2026-03-28: Dodano migracje `20260328_000072_create_automation_execution_logs_table.sql`: + - nowa tabela `automation_execution_logs` (historia wykonan regul automatyzacji: co, kiedy, na jakim zamowieniu, wynik), + - indeksy pod filtrowanie po czasie/zdarzeniu/statusie/regule/zamowieniu, + - seed harmonogramu `cron_schedules` dla joba `automation_history_cleanup` (retencja historii starszej niz 30 dni). ## Tabele @@ -474,6 +481,29 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane - Klucze obce: - `auto_act_rule_fk`: `rule_id` -> `automation_rules.id` (ON DELETE CASCADE) +### `automation_execution_logs` +- Historia wykonan automatyzacji pokazywana w `Ustawienia > Zadania automatyczne > Historia`. +- Kolumny: + - `id` BIGINT UNSIGNED PK AUTO_INCREMENT + - `event_type` VARCHAR(64) NOT NULL — typ triggera (np. `receipt.created`, `shipment.created`) + - `rule_id` INT UNSIGNED NULL — FK do `automation_rules.id` (NULL gdy regula zostala usunieta) + - `rule_name` VARCHAR(128) NOT NULL — snapshot nazwy reguly w momencie wykonania + - `order_id` INT UNSIGNED NOT NULL — FK do `orders.id` + - `execution_status` VARCHAR(16) NOT NULL — wynik (`success`/`failed`) + - `result_message` VARCHAR(500) NULL — komunikat wykonania lub bledu + - `context_json` JSON NULL — zrzut kontekstu triggera (sanityzowany) + - `executed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +- Indeksy: + - `auto_exec_logs_executed_idx` (`executed_at`) + - `auto_exec_logs_event_idx` (`event_type`) + - `auto_exec_logs_status_idx` (`execution_status`) + - `auto_exec_logs_rule_idx` (`rule_id`) + - `auto_exec_logs_order_idx` (`order_id`) +- Klucze obce: + - `auto_exec_logs_rule_fk`: `rule_id` -> `automation_rules.id` (`ON DELETE SET NULL`, `ON UPDATE CASCADE`) + - `auto_exec_logs_order_fk`: `order_id` -> `orders.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`) + ## Zasady aktualizacji - Po kazdej migracji dopisz: - nowe/zmienione tabele i kolumny, diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md index 5f2bcd3..3cb4fe6 100644 --- a/DOCS/TECH_CHANGELOG.md +++ b/DOCS/TECH_CHANGELOG.md @@ -1,5 +1,67 @@ # Tech Changelog +## 2026-03-28 (Public HTTPS cron endpoint) +- Dodano publiczny endpoint triggera crona: + - `GET /cron?token=` + - dodatkowo kompatybilny wariant sciezki: `GET /cron/token=`. +- Token jest walidowany przez `hash_equals` i pochodzi z nowej zmiennej srodowiskowej `CRON_PUBLIC_TOKEN`. +- Endpoint uruchamia `CronRunner` z limitem z ustawienia `cron_web_limit`. +- `Application::maybeRunCronOnWeb(...)` ignoruje teraz sciezki `/cron` i `/cron/*`, aby uniknac podwojnego triggera. +- Zaktualizowano `.env.example` o `CRON_PUBLIC_TOKEN`. + +## 2026-03-28 (Hotfix - Apaczka pickup day fallback) +- `ApaczkaShipmentService`: + - dodano automatyczny retry `order_send` dla bledu API `Pickup not available for selected day`, + - rozszerzono detekcje bledu o wariant komunikatu: `We're sorry, you can't place an order today. Change its date to another working day.`, + - fallback dotyczy tylko `pickup.type=COURIER`, + - kazdy retry przesuwa `pickup.date` na kolejny dzien roboczy (`normalizeCourierPickupDate`) i ponawia wysylke, + - limit fallbacku: do 7 kolejnych dni, potem zwracany jest oryginalny blad API. + +## 2026-03-28 (Phase 49 - Automation History Tab, Plan 01 - rozszerzenie akcji) +- Rozszerzono automatyzacje o nowy typ akcji `update_order_status` (UI: `Zmiana statusu zamowienia`). +- `AutomationController`: + - `ALLOWED_ACTION_TYPES` zawiera `update_order_status`, + - waliduje i parsuje `order_status_code` tylko do aktywnych statusow z `order_statuses`. +- `AutomationRepository`: + - nowa metoda `listActiveOrderStatuses()` zwracajaca aktywne statusy (`code`, `name`) sortowane rosnaco po nazwie. +- `resources/views/automation/form.php` i `public/assets/js/modules/automation-form.js`: + - nowa opcja akcji z wyborem docelowego statusu zamowienia. +- `AutomationService`: + - nowy handler `handleUpdateOrderStatus(...)`, + - wykonanie zmiany przez `OrdersRepository::updateOrderStatus(...)` z aktorem systemowym `Automatyzacja: `, + - fallback log aktywnosci `automation_order_status_failed` gdy zmiana nie powiedzie sie. + +## 2026-03-28 (Phase 49 - Automation History Tab, Plan 01) +- `Ustawienia > Zadania automatyczne` (`/settings/automation`) rozdzielone na 2 taby: + - `Ustawienia` - obecne zarzadzanie regulami, + - `Historia` - log wykonan automatyzacji z filtrowaniem i paginacja. +- Dodano migracje `20260328_000072_create_automation_execution_logs_table.sql`: + - nowa tabela `automation_execution_logs` (event, regula, order, status, wynik, context, timestamp), + - indeksy pod filtry historii, + - seed harmonogramu crona `automation_history_cleanup` (co 24h). +- Nowe klasy backend: + - `AutomationExecutionLogRepository` - zapis/listowanie/paginacja/licznik historii + purge retencji, + - `AutomationHistoryCleanupHandler` - usuwanie wpisow starszych niz konfigurowalna liczba dni (domyslnie 30). +- `AutomationService::trigger(...)` zapisuje wpis historii per wykonana regula: + - `success` po poprawnym wykonaniu akcji, + - `failed` przy wyjatku w wykonaniu reguly. +- `AutomationController::index(...)` obsluguje filtry historii (`history_*`) i paginacje (`history_page`), zachowujac kompatybilnosc listy regul. +- UI historii wykorzystuje kompaktowy formularz filtrow i paginacje z zachowaniem aktywnych parametrow. + +## 2026-03-28 (Phase 48 - Email Template Shipment Variables, Plan 01) +- Email templates (`/settings/email-templates`): + - dodano zmienne `{{przesylka.numer}}` i `{{przesylka.link_sledzenia}}` w `EmailTemplateController::VARIABLE_GROUPS`, + - rozszerzono `SAMPLE_DATA` do preview o przykladowy numer i URL sledzenia. +- `VariableResolver`: + - otrzymuje zaleznosc `ShipmentPackageRepository`, + - pobiera najnowsza paczke (`findLatestByOrderId(order_id)`), + - mapuje `przesylka.numer` i `przesylka.link_sledzenia`, + - link jest liczony przez `DeliveryStatus::trackingUrl(provider, tracking_number, carrier_id)`. +- DI: + - `routes/web.php` i `CronHandlerFactory` przekazuja `ShipmentPackageRepository` do `VariableResolver`. +- Zachowanie brzegowe: + - brak paczki lub brak numeru trackingowego nie psuje renderu - zmienne przesylki zwracaja pusty string. + ## 2026-03-28 (Phase 47 - Shipment Creation Automation, Plan 01) - Automatyzacja: - dodano nowe zdarzenie `shipment.created` (UI: `Utworzenie przesylki`), @@ -739,3 +801,4 @@ + diff --git a/DOCS/todo.md b/DOCS/todo.md index 72e2e3a..8cf6a90 100644 --- a/DOCS/todo.md +++ b/DOCS/todo.md @@ -226,3 +226,4 @@ 47. [x] Zadania automatyczne: nowe zdarzenie Utworzenie przesylki uruchamiane od razu po utworzeniu paczki oraz nowa akcja Zmiana statusu przesylki. +48. [x] Szablony e-mail: dodane zmienne `{{przesylka.numer}}` i `{{przesylka.link_sledzenia}}` z linkiem zaleznym od kuriera/providera. diff --git a/config/app.php b/config/app.php index 96720f3..32a0cda 100644 --- a/config/app.php +++ b/config/app.php @@ -19,6 +19,7 @@ return [ 'cron' => [ 'run_on_web_default' => Env::bool('CRON_RUN_ON_WEB', false), 'web_limit_default' => max(1, min(100, (int) Env::get('CRON_WEB_LIMIT', '5'))), + 'public_token' => Env::get('CRON_PUBLIC_TOKEN', ''), ], 'view_path' => dirname(__DIR__) . '/resources/views', 'lang_path' => dirname(__DIR__) . '/resources/lang', diff --git a/database/migrations/20260328_000072_create_automation_execution_logs_table.sql b/database/migrations/20260328_000072_create_automation_execution_logs_table.sql new file mode 100644 index 0000000..5b0abfc --- /dev/null +++ b/database/migrations/20260328_000072_create_automation_execution_logs_table.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS `automation_execution_logs` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `event_type` VARCHAR(64) NOT NULL, + `rule_id` INT UNSIGNED DEFAULT NULL, + `rule_name` VARCHAR(128) NOT NULL, + `order_id` INT UNSIGNED NOT NULL, + `execution_status` VARCHAR(16) NOT NULL, + `result_message` VARCHAR(500) DEFAULT NULL, + `context_json` JSON DEFAULT NULL, + `executed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `auto_exec_logs_executed_idx` (`executed_at`), + INDEX `auto_exec_logs_event_idx` (`event_type`), + INDEX `auto_exec_logs_status_idx` (`execution_status`), + INDEX `auto_exec_logs_rule_idx` (`rule_id`), + INDEX `auto_exec_logs_order_idx` (`order_id`), + CONSTRAINT `auto_exec_logs_rule_fk` FOREIGN KEY (`rule_id`) REFERENCES `automation_rules` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `auto_exec_logs_order_fk` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO cron_schedules (job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at) +VALUES ('automation_history_cleanup', 86400, 70, 1, JSON_OBJECT('days', 30), 1, NULL, NOW(), NOW(), NOW()) +ON DUPLICATE KEY UPDATE + interval_seconds = VALUES(interval_seconds), + priority = VALUES(priority), + max_attempts = VALUES(max_attempts), + payload = VALUES(payload), + enabled = VALUES(enabled), + updated_at = VALUES(updated_at); diff --git a/public/assets/css/app.css b/public/assets/css/app.css index f0b1405..6bf698a 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1 +1 @@ -:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-action-primary: #0f766e;--c-action-primary-dark: #0b5f59;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #b0bec5;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--focus-ring-action: 0 0 0 3px rgba(15, 118, 110, 0.18);--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.06)}.btn{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:6px 12px;border:1px solid rgba(0,0,0,0);border-radius:8px;font:inherit;font-weight:600;text-decoration:none;cursor:pointer;transition:background-color .2s ease,border-color .2s ease,color .2s ease,transform .1s ease}.btn--primary{color:#fff;background:var(--c-action-primary)}.btn--primary:hover{background:var(--c-action-primary-dark)}.btn--secondary{color:var(--c-text-strong);border-color:var(--c-border);background:var(--c-surface)}.btn--secondary:hover{border-color:#cbd5e0;background:#f8fafc}.btn--danger{color:#fff;border-color:#b91c1c;background:#dc2626}.btn--danger:hover{border-color:#991b1b;background:#b91c1c}.btn--sm{min-height:28px;padding:3px 10px;font-size:12px}.btn--block{width:100%}.btn--disabled{opacity:.3;cursor:not-allowed;pointer-events:none}.btn:active{transform:translateY(1px)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring-action);border-color:var(--c-action-primary)}.form-control{width:100%;min-height:30px;border:1px solid var(--c-border);border-radius:6px;padding:4px 8px;font:inherit;color:var(--c-text-strong);background:#fff;transition:border-color .2s ease,box-shadow .2s ease}.form-control:focus{outline:none;border-color:var(--c-primary);box-shadow:var(--focus-ring)}.input{min-height:34px;border:1px solid var(--c-border);border-radius:8px;padding:5px 10px;font:inherit;color:var(--c-text-strong);background:#fff}.input--sm{min-height:28px;padding:3px 8px;font-size:12px}.flash{padding:8px 12px;border-radius:6px;font-size:13px}.flash--success{border:1px solid #b7ebcf;background:#f0fff6;color:#0f6b39}.flash--error{border:1px solid #fed7d7;background:#fff5f5;color:var(--c-danger)}.alert{padding:12px 14px;border-radius:8px;border:1px solid rgba(0,0,0,0);font-size:13px;min-height:44px}.alert--danger{border-color:#fed7d7;background:#fff5f5;color:var(--c-danger)}.alert--success{border-color:#b7ebcf;background:#f0fff6;color:#0f6b39}.alert--warning{border-color:#f7dd8b;background:#fff8e8;color:#815500}.form-field{display:grid;gap:5px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}.table-wrap{width:100%;overflow-x:auto}.table-wrap--visible{overflow:visible !important;overflow-x:visible !important}.table{width:100%;border-collapse:collapse;background:var(--c-surface)}.table th,.table td{padding:10px 12px;border-bottom:1px solid var(--c-border);text-align:left}.table th{color:var(--c-text-strong);font-weight:700;background:#f8fafc}.table--details th{white-space:nowrap}.table--details th:first-child,.table--details td:first-child{width:36px;text-align:center}.pagination{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.pagination__item{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 10px;border-radius:8px;border:1px solid var(--c-border);color:var(--c-text-strong);background:var(--c-surface);text-decoration:none;font-weight:600}.pagination__item:hover{border-color:#cbd5e0;background:#f8fafc}.pagination__item.is-active{border-color:var(--c-primary);color:var(--c-primary);background:#edf2ff}.receipt-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;padding-bottom:12px;border-bottom:2px solid var(--c-text-strong)}.receipt-header__seller{flex:1}.receipt-header__seller strong{font-size:14px;display:block;margin-bottom:4px}.receipt-header__title{text-align:right}.receipt-header__title h1{font-size:18px;font-weight:700;margin-bottom:4px}.receipt-print{max-width:700px;margin:0 auto}@media print{.receipt-print{max-width:100%}}.email-send-overlay{position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center}.email-send-modal{background:var(--c-card-bg, #fff);border-radius:8px;width:580px;max-width:95vw;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.2)}.email-send-modal__header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--c-border, #e0e0e0)}.email-send-modal__header h3{margin:0;font-size:15px}.email-send-modal__close{background:none;border:none;font-size:20px;cursor:pointer;color:var(--c-text-muted, #888);padding:0 4px}.email-send-modal__close:hover{color:var(--c-text, #333)}.email-send-modal__body{padding:16px;overflow-y:auto;flex:1}.email-send-modal__field{margin-bottom:10px}.email-send-modal__field label{display:block;font-size:12px;font-weight:600;margin-bottom:4px;color:var(--c-text-muted, #666)}.email-send-modal__field .input{width:100%}.email-send-modal__actions-top{margin-bottom:10px}.email-send-modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--c-border, #e0e0e0)}.email-send-preview{border:1px solid var(--c-border, #e0e0e0);border-radius:4px;padding:12px;max-height:280px;overflow-y:auto;background:var(--c-bg, #fafafa)}.email-send-preview__subject{font-weight:600;font-size:13px;margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid var(--c-border, #e0e0e0)}.email-send-preview__body{font-size:13px;line-height:1.5}.email-send-preview__body p{margin:0 0 8px}.email-send-preview__attachments{margin-top:8px;padding-top:8px;border-top:1px solid var(--c-border, #e0e0e0);font-size:12px;color:var(--c-text-muted, #666)}.section-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.automation-row{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;background:var(--c-surface, #f8f9fa);border:1px solid var(--c-border, #dee2e6);border-radius:6px}.automation-row__fields{flex:1;display:flex;flex-direction:column;gap:6px}.automation-row__type{max-width:280px}.automation-row__config{display:flex;flex-wrap:wrap;gap:8px}.automation-row__config .form-control{min-width:200px;max-width:300px}.automation-row__remove{flex-shrink:0;margin-top:2px;line-height:1;font-size:16px;padding:2px 8px}.checkbox-group{display:flex;flex-wrap:wrap;gap:4px 16px}.checkbox-label{display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;white-space:nowrap}.checkbox-label input[type=checkbox]{margin:0}.print-status-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:.75rem;font-weight:600;line-height:1.4}.print-status-badge--pending{background-color:#fff3cd;color:#856404}.print-status-badge--completed{background-color:#d4edda;color:#155724}.print-status-badge--failed{background-color:#f8d7da;color:#721c24}.print-queue-filters{display:flex;gap:4px}.print-queue-table td,.print-queue-table th{padding:6px 8px;font-size:.85rem}.print-queue-actions{display:inline-flex;align-items:center;gap:6px}.print-queue-delete-form{margin:0}.btn--outline-primary{background:rgba(0,0,0,0);border:1px solid var(--c-action-primary);color:var(--c-action-primary);cursor:pointer;border-radius:3px;font-size:.75rem;padding:3px 8px;transition:background-color .15s,color .15s}.btn--outline-primary:hover{background-color:var(--c-action-primary);color:#fff}.btn--outline-primary:disabled{opacity:.6;cursor:not-allowed}.btn--outline-primary.is-success{border-color:#28a745;color:#28a745}.shipment-presets{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:16px}.shipment-presets__btn{display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border:none;border-radius:6px;background:var(--preset-color, #3b82f6);color:#fff;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .15s;line-height:1.4}.shipment-presets__btn:hover{opacity:.85}.shipment-presets__add{display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border:1px dashed #ccc;border-radius:6px;background:rgba(0,0,0,0);color:#666;font-size:13px;cursor:pointer;transition:border-color .15s,color .15s;line-height:1.4}.shipment-presets__add:hover{border-color:#999;color:#444}.preset-modal{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:1000;display:flex;align-items:center;justify-content:center}.preset-modal__content{background:#fff;border-radius:8px;padding:24px;min-width:360px;max-width:420px;box-shadow:0 8px 32px rgba(0,0,0,.2)}.preset-modal__content h3{margin:0 0 4px;font-size:16px}.preset-modal__colors{display:flex;gap:8px;flex-wrap:wrap}.preset-modal__color-swatch{width:28px;height:28px;border-radius:50%;cursor:pointer;border:2px solid rgba(0,0,0,0);transition:border-color .15s}.preset-modal__color-swatch:hover{border-color:#aaa}.preset-modal__color-swatch.is-selected{border-color:#333}.shipment-presets__btn-wrap{position:relative;display:inline-flex}.shipment-presets__btn-wrap:hover .shipment-presets__edit-icon{opacity:1}.shipment-presets__edit-icon{position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:#fff;border:1px solid #ddd;font-size:10px;line-height:16px;text-align:center;cursor:pointer;opacity:0;transition:opacity .15s;padding:0;color:#666;z-index:2}.shipment-presets__edit-icon:hover{background:#f3f4f6;border-color:#999}.shipment-presets__dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.12);z-index:100;min-width:200px;padding:4px 0}.shipment-presets__dropdown-item{padding:6px 14px;font-size:13px;cursor:pointer;white-space:nowrap}.shipment-presets__dropdown-item:hover{background:#f3f4f6}.shipment-presets__dropdown-item.is-danger{color:#ef4444}.shipment-presets__dropdown-item.is-danger:hover{background:#fef2f2}.delivery-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:.8em;font-weight:500;white-space:nowrap}.delivery-badge--unknown{background:#f5f5f5;color:#999}.delivery-badge--created{background:#e3f2fd;color:#1565c0}.delivery-badge--confirmed{background:#bbdefb;color:#0d47a1}.delivery-badge--in_transit{background:#fff3e0;color:#e65100}.delivery-badge--out_for_delivery{background:#ffe0b2;color:#bf360c}.delivery-badge--ready_for_pickup{background:#f3e5f5;color:#6a1b9a}.delivery-badge--delivered{background:#e8f5e9;color:#2e7d32}.delivery-badge--returned{background:#ffebee;color:#c62828}.delivery-badge--cancelled{background:#e0e0e0;color:#616161}.delivery-badge--problem{background:#fff8e1;color:#f57f17}.tracking-link{margin-left:4px;text-decoration:none;font-size:.85em}.dsm-row--custom{background:rgba(59,130,246,.06)}.dsm-raw-status{font-size:.82rem;background:var(--surface-alt, #f1f5f9);padding:2px 6px;border-radius:3px;white-space:nowrap}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;font-size:13px;color:var(--c-text);background:var(--c-bg)}a{color:var(--c-primary)}.app-shell{min-height:100vh;display:flex}.sidebar{width:260px;min-width:260px;flex-shrink:0;overflow:hidden;transition:width .22s ease,min-width .22s ease;border-right:1px solid #243041;background:#111a28;padding:18px 10px;display:flex;flex-direction:column}.sidebar.is-collapsed{width:52px;min-width:52px}.sidebar__brand{display:flex;align-items:center;justify-content:space-between;margin:4px 4px 16px;gap:6px;min-width:0}.sidebar__brand-text{color:#e9f0ff;font-size:24px;font-weight:300;letter-spacing:-0.02em;white-space:nowrap;overflow:hidden;flex:1;min-width:0}.sidebar__brand-text strong{font-weight:700}.sidebar__collapse-btn{flex-shrink:0;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0);border:1px solid #2a3a54;border-radius:6px;color:#64748b;cursor:pointer;padding:0;transition:background .15s,color .15s}.sidebar__collapse-btn:hover{background:#1b2a3f;color:#cbd5e1}.sidebar__collapse-icon{display:block;transition:transform .22s ease;flex-shrink:0}.sidebar.is-collapsed .sidebar__collapse-icon{transform:rotate(180deg)}.sidebar__nav{display:grid;gap:4px}.sidebar__link{display:flex;align-items:center;gap:9px;white-space:nowrap;border-radius:8px;padding:9px 10px;text-decoration:none;color:#cbd5e1;font-weight:600}.sidebar__link:hover{color:#f8fafc;background:#1b2a3f}.sidebar__link.is-active{color:#fff;background:#2e4f93}.sidebar__group{display:grid;gap:2px}.sidebar__group-toggle{list-style:none;border-radius:8px;padding:9px 10px;color:#cbd5e1;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:9px;white-space:nowrap;user-select:none}.sidebar__group-toggle::-webkit-details-marker{display:none}.sidebar__group:hover .sidebar__group-toggle,.sidebar__group-toggle:hover{color:#f8fafc;background:#1b2a3f}.sidebar__group.is-active .sidebar__group-toggle{color:#fff;background:#2e4f93}.sidebar__icon{flex-shrink:0;width:18px;height:18px;display:flex;align-items:center;justify-content:center;opacity:.85}.sidebar__label{flex:1;min-width:0;overflow:hidden}.sidebar__toggle-arrow{flex-shrink:0;margin-left:auto;opacity:.5;transition:transform .18s ease}details[open]>.sidebar__group-toggle .sidebar__toggle-arrow{transform:rotate(180deg)}.sidebar__group-links{display:grid;gap:2px;padding-left:12px;overflow:hidden}.sidebar__sublink{border-radius:6px;padding:7px 10px 7px 8px;text-decoration:none;color:#94a3b8;font-size:12.5px;font-weight:500;display:flex;align-items:center;gap:8px;white-space:nowrap}.sidebar__sublink::before{content:"";flex-shrink:0;width:5px;height:5px;border-radius:50%;background:rgba(148,163,184,.3);transition:background .15s}.sidebar__sublink:hover{color:#e2e8f0;background:#1b2a3f}.sidebar__sublink:hover::before{background:rgba(148,163,184,.65)}.sidebar__sublink.is-active{color:#fff;background:rgba(46,79,147,.55)}.sidebar__sublink.is-active::before{background:#93c5fd}.app-main{flex:1;min-width:0}.topbar{height:50px;border-bottom:1px solid var(--c-border);background:var(--c-surface);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100}.brand{font-size:22px;font-weight:300;letter-spacing:-0.02em;color:var(--c-text-strong)}.brand strong{font-weight:700}.container{max-width:none;width:calc(100% - 20px);margin:12px 10px;padding:0 4px 14px}.card{background:var(--c-surface);border-radius:10px;box-shadow:var(--shadow-card);padding:14px}.card h1{margin:0 0 10px;color:var(--c-text-strong);font-size:24px;font-weight:700}.muted{color:var(--c-muted)}.accent{color:var(--c-primary);font-weight:600}.users-form{display:grid;gap:14px;max-width:460px}.form-field{margin-bottom:12px}.section-title{margin:0;color:var(--c-text-strong);font-size:18px;font-weight:700}h2.section-title,h3.section-title,h4.section-title{display:flex;align-items:center;gap:6px;font-weight:600;padding:6px 0;border-bottom:1px solid #e2e8f0;color:var(--c-primary, #2563eb)}h2.section-title::before,h3.section-title::before,h4.section-title::before{content:"■";font-size:.55em;opacity:.5}h3.section-title,h4.section-title{font-size:15px}h3.section-title::before,h4.section-title::before{content:"◆";font-size:.5em}.mt-0{margin-top:0}.mt-4{margin-top:4px}.mt-12{margin-top:8px}.mt-16{margin-top:12px}.settings-grid{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.settings-nav{display:flex;gap:8px;flex-wrap:wrap}.settings-nav__link{text-decoration:none;border:1px solid var(--c-border);border-radius:8px;padding:8px 12px;color:var(--c-text-strong);font-weight:600}.settings-nav__link:hover{background:#f8fafc}.settings-nav__link.is-active{border-color:var(--c-primary);color:var(--c-primary);background:#edf2ff}.settings-stat{border:1px solid var(--c-border);border-radius:8px;padding:12px;background:#f8fafc}.settings-stat__label{display:block;color:var(--c-muted);font-size:12px;margin-bottom:4px}.settings-stat__value{color:var(--c-text-strong);font-size:20px}.settings-logs{margin:0;padding:12px;border-radius:8px;border:1px solid var(--c-border);background:#0b1220;color:#d1d5db;font-size:12px;line-height:1.5;overflow:auto}.settings-allegro-callback{display:block;width:100%;padding:8px 10px;border:1px solid var(--c-border);border-radius:8px;background:#f8fafc;color:var(--c-text-strong);font-size:12px;line-height:1.45;word-break:break-all}.page-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.filters-grid{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.filters-actions{display:flex;align-items:center;gap:8px}.product-form .form-control{width:100%}.form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px}.form-grid-2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px;align-items:start}.form-grid-3{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px;align-items:start}.form-grid-4{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:12px;align-items:start}.form-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-start}.form-actions .btn{align-self:flex-start}.statuses-form{display:grid;gap:8px;grid-template-columns:repeat(2, minmax(0, 1fr))}.statuses-form .form-actions{grid-column:1/-1}.statuses-color-input{min-height:32px;padding:2px}.statuses-hint{grid-column:1/-1;margin:0}.statuses-group-block{border:1px solid var(--c-border);border-radius:10px;padding:8px;background:#fbfdff}.statuses-group-block__head{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap}.statuses-group-block__title{margin:0;display:inline-flex;align-items:center;gap:6px;color:var(--c-text-strong);font-size:14px}.statuses-color-dot{width:12px;height:12px;border-radius:999px;border:1px solid rgba(15,23,42,.15)}.statuses-dnd-list{margin:6px 0 0;padding:0;list-style:none;display:grid;gap:6px}.statuses-dnd-item{display:grid;grid-template-columns:24px 1fr;gap:6px;border:1px solid #dce4f0;border-radius:8px;background:#fff;padding:6px}.statuses-dnd-item__content{display:flex;align-items:center;gap:6px;min-width:0}.statuses-dnd-item.is-dragging{opacity:.6}.statuses-dnd-item__drag{display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;border-radius:6px;color:#64748b;cursor:grab;user-select:none;font-weight:700;font-size:12px}.statuses-dnd-item__drag:active{cursor:grabbing}.statuses-inline-form{display:grid;gap:6px}.statuses-inline-form--row{grid-template-columns:minmax(180px, 1.4fr) minmax(150px, 1fr) auto auto auto;align-items:center;flex:1 1 auto;min-width:0}.statuses-inline-form--row-group{grid-template-columns:minmax(180px, 1.5fr) 56px auto auto auto;align-items:center;flex:1 1 auto;min-width:0}.statuses-inline-form--row .form-control,.statuses-inline-form--row-group .form-control{min-height:30px;padding:4px 8px}.statuses-inline-form--row .btn,.statuses-inline-form--row-group .btn,.statuses-inline-delete .btn{min-height:30px;padding:4px 10px;font-size:12px}.statuses-inline-check{margin-top:0;white-space:nowrap;font-size:12px}.statuses-inline-delete{margin:0;flex:0 0 auto}.statuses-code-label{font-size:12px;color:var(--c-muted)}.statuses-code-readonly{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;font-size:12px}.statuses-code-readonly code{background:#eef2f7;border-radius:6px;padding:1px 6px;color:#1f2937;font-size:12px}.field-inline{display:flex;align-items:center;gap:8px;margin-top:2px}.modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.5);display:flex;align-items:center;justify-content:center;padding:16px;z-index:200}.modal-backdrop[hidden]{display:none}.modal{width:min(560px,100%);background:#fff;border-radius:10px;box-shadow:0 20px 40px rgba(15,23,42,.35);overflow:hidden}.modal__header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:16px 18px;border-bottom:1px solid var(--c-border)}.modal__header h3{margin:0;font-size:18px;color:var(--c-text-strong)}.modal__body{padding:16px 18px 18px}.status-pill{display:inline-flex;align-items:center;justify-content:center;border:1px solid #fed7d7;background:#fff5f5;color:#9b2c2c;padding:2px 8px;border-radius:999px;font-size:12px;font-weight:600}.status-pill.is-active{border-color:#b7ebcf;background:#f0fff6;color:#0f6b39}.table-row-actions{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.table-row-actions form{margin:0}.table-list{display:grid;gap:14px}.table-list__header{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.table-list__left{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.table-list-header-actions{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap}.js-filter-toggle-btn.is-active{border-color:#cbd5e0;background:#edf2ff;color:var(--c-primary-dark)}.table-filter-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;font-size:11px;font-weight:700;color:#fff;background:var(--c-primary);border-radius:999px}.table-filters-wrapper{display:none}.table-filters-wrapper.is-open{display:block}.table-list-filters{display:grid;gap:12px;grid-template-columns:repeat(auto-fit, minmax(170px, 1fr));align-items:end}.table-col-toggle-wrapper{position:relative}.table-col-toggle-dropdown{display:none;position:absolute;right:0;top:calc(100% + 6px);z-index:30;width:260px;max-height:360px;overflow:auto;border:1px solid var(--c-border);border-radius:10px;background:#fff;box-shadow:0 10px 25px rgba(15,23,42,.12)}.table-col-toggle-dropdown.is-open{display:block}.table-col-toggle-header{padding:10px 12px;border-bottom:1px solid var(--c-border);font-size:12px;font-weight:700;color:var(--c-muted)}.table-col-toggle-item{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13px;color:var(--c-text-strong)}.table-col-toggle-item:hover{background:#f8fafc}.table-col-toggle-footer{border-top:1px solid var(--c-border);padding:8px 12px}.table-col-hidden{display:none}.table-col-switch{position:relative;display:inline-block;width:34px;min-width:34px;height:18px}.table-col-switch input{opacity:0;width:0;height:0;position:absolute}.table-col-switch-slider{position:absolute;top:0;left:0;right:0;bottom:0;background:#cbd5e1;border-radius:999px;transition:background-color .2s ease}.table-col-switch-slider::before{content:"";position:absolute;height:14px;width:14px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:transform .2s ease}.table-col-switch input:checked+.table-col-switch-slider{background:#16a34a}.table-col-switch input:checked+.table-col-switch-slider::before{transform:translateX(16px)}.table-sort-link{display:inline-flex;align-items:center;gap:6px;color:var(--c-text-strong);text-decoration:none}.table-sort-link:hover{color:var(--c-primary-dark)}.table-sort-icon.is-muted{color:#a0aec0}.table-list__footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.table-list-per-page-form{display:inline-flex;align-items:center;gap:8px}.table-list-per-page-form .form-control{min-width:90px}.table-select-col{width:44px;text-align:center}.table-select-toggle{display:inline-flex;align-items:center;justify-content:center}.table-select-toggle input[type=checkbox]{width:16px;height:16px}.orders-page .orders-head{background:linear-gradient(120deg, #f8fbff 0%, #eef5ff 100%);border:1px solid #dbe7fb}.orders-page .table-list{border:1px solid #dde5f2;border-radius:12px;box-shadow:0 6px 16px rgba(20,44,86,.08)}.orders-page .table-list__header{padding:10px 6px 2px}.orders-page .table-list-filters{padding:6px 6px 2px;border-top:1px solid #ebf0f7;border-bottom:1px solid #ebf0f7;background:#f9fbff}.orders-page .table-wrap{border-radius:10px;overflow:hidden;border:1px solid #e7edf6}.orders-page .table thead th{background:#f3f7fd;color:#30435f;font-size:12px;text-transform:uppercase;letter-spacing:.03em}.orders-page .table tbody td{vertical-align:middle;padding-top:10px;padding-bottom:10px;border-bottom-color:#edf2f8}.orders-page .table tbody tr:hover td{background:#f9fcff}.orders-list-page{padding:10px;margin-bottom:10px}.orders-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.orders-stats{display:inline-grid;grid-template-columns:repeat(3, minmax(86px, auto));gap:8px}.orders-stat{border:1px solid #d8e2f0;background:#f8fbff;border-radius:8px;padding:6px 8px;line-height:1.15}.orders-stat__label{display:block;color:#5f6f83;font-size:11px;margin-bottom:2px}.orders-stat__value{color:#12233a;font-size:16px;font-weight:700}.orders-ref{display:grid;gap:2px;min-width:170px}.orders-ref__main{font-weight:700;color:#0f1f35;font-size:14px}.orders-ref__meta{display:inline-flex;flex-wrap:wrap;gap:4px 10px;color:#64748b;font-size:12px}.orders-buyer{display:grid;gap:2px}.orders-buyer__name{color:#0f172a;font-weight:600;font-size:14px}.orders-buyer__meta{display:inline-flex;flex-wrap:wrap;gap:4px 10px;color:#64748b;font-size:12px}.orders-status-wrap{display:inline-flex;align-items:center;gap:5px;flex-wrap:wrap;cursor:pointer}.orders-status-wrap .order-tag{cursor:pointer}.orders-status-dropdown{position:fixed;z-index:9999;min-width:180px;max-height:280px;overflow-y:auto;background:#fff;border:1px solid #d8e1ef;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.12);padding:4px 0}.orders-status-dropdown__group-header{padding:6px 12px 2px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}.orders-status-dropdown__group-header:not(:first-child){border-top:1px solid #f1f5f9;margin-top:2px;padding-top:8px}.orders-status-dropdown__item{display:flex;align-items:center;gap:8px;padding:5px 12px;font-size:13px;color:#334155;cursor:pointer;white-space:nowrap}.orders-status-dropdown__item:hover{background:#f1f5f9}.orders-status-dropdown__item.is-current{font-weight:700;background:#f8fafc}.orders-status-dropdown__color-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.order-tag{display:inline-flex;align-items:center;justify-content:center;border:1px solid #d8e1ef;background:#f8fafc;color:#334155;border-radius:999px;padding:2px 8px;font-size:12px;font-weight:700;line-height:1.1}.order-tag.is-info{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}.order-tag.is-success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}.order-tag.is-danger{border-color:#fecaca;background:#fef2f2;color:#b91c1c}.order-tag.is-warn{border-color:#fde68a;background:#fffbeb;color:#92400e}.order-tag.is-cod{border-color:#f9a8d4;background:#fdf2f8;color:#9d174d}.order-tag.is-unpaid{border-color:#fca5a5;background:#fef2f2;color:#b91c1c}.orders-mini{font-size:14px;color:#223247;line-height:1.25}.orders-mini__delivery{font-size:12px;color:#64748b;margin-bottom:2px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.orders-products{display:grid;gap:4px;min-width:240px}.orders-products__meta,.orders-products__more{font-size:12px;color:#64748b}.orders-product{display:grid;grid-template-columns:48px 1fr;gap:6px;align-items:center}.orders-product__thumb{width:48px;height:48px;border-radius:4px;border:1px solid #dbe3ef;object-fit:cover;background:#fff}.orders-product__thumb--empty{display:inline-block;background:#eef2f7;border-style:dashed}.orders-product__txt{min-width:0;display:grid;gap:1px}.orders-product__name{font-size:14px;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.orders-product__qty{font-size:12px;color:#64748b}.orders-image-hover-wrap{position:relative;display:inline-flex;align-items:center;justify-content:center;cursor:zoom-in}.orders-image-hover-popup{display:none;position:fixed;left:auto;top:auto;width:350px;max-height:350px;object-fit:contain;border-radius:8px;background:#fff;box-shadow:0 8px 24px rgba(0,0,0,.18);border:1px solid #dfe3ea;z-index:100;pointer-events:none}.orders-image-hover-wrap:hover .orders-image-hover-popup{display:block}.activity-type-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;white-space:nowrap;background:#e2e8f0;color:#334155}.activity-type-badge--status_change{background:#dbeafe;color:#1e40af}.activity-type-badge--payment{background:#dcfce7;color:#166534}.activity-type-badge--invoice{background:#fef3c7;color:#92400e}.activity-type-badge--shipment{background:#e0e7ff;color:#3730a3}.activity-type-badge--message{background:#f3e8ff;color:#6b21a8}.activity-type-badge--document{background:#fce7f3;color:#9d174d}.activity-type-badge--import{background:#f1f5f9;color:#475569}.activity-type-badge--note{background:#ecfdf5;color:#065f46}.text-nowrap{white-space:nowrap}.orders-money{display:grid;gap:2px}.orders-money__main{color:#0f172a;font-weight:700;font-size:14px}.orders-money__meta{color:#64748b;font-size:12px}.table-list[data-table-list-id=orders]{gap:8px}.table-list[data-table-list-id=orders] .table-list__header{padding:2px 0 0}.table-list[data-table-list-id=orders] .table-list-filters{gap:8px;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr))}.table-list[data-table-list-id=orders] .table th,.table-list[data-table-list-id=orders] .table td{padding:6px 8px}.table-list[data-table-list-id=orders] .table thead th{font-size:12px;text-transform:uppercase;letter-spacing:.02em;white-space:nowrap}.table-list[data-table-list-id=orders] .table tbody td{vertical-align:top;font-size:14px;line-height:1.25}.order-show-layout{display:grid;grid-template-columns:220px minmax(0, 1fr);gap:12px;align-items:start}.order-statuses-side{position:sticky;top:60px;padding:10px}.order-statuses-side__title{font-size:13px;font-weight:700;color:#0f172a;margin-bottom:8px}.order-status-group{margin-bottom:10px}.order-status-group__name{font-size:12px;color:#475569;font-weight:700;margin-bottom:5px}.order-status-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 6px;border-radius:6px;color:#334155;font-size:12px;text-decoration:none}.order-status-row__count{min-width:24px;text-align:center;border-radius:999px;background:var(--status-color, #64748b);padding:1px 6px;font-weight:700;font-size:11px;color:#fff}.order-status-row:hover{background:#f1f5f9}.order-status-row.is-active{background:rgba(15,23,42,.06);color:#0f172a;font-weight:700}.order-show-main{min-width:0}.order-details-actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.order-details-page{padding:12px}.order-details-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.order-back-link{color:#475569;text-decoration:none;font-weight:600}.order-back-link:hover{color:#1d4ed8}.order-details-sub{display:inline-flex;gap:10px;flex-wrap:wrap;color:#64748b;font-size:12px}.order-details-pill{border-radius:999px;padding:5px 10px;background:#eef6ff;border:1px solid #cfe2ff;color:#1d4ed8;font-size:12px;font-weight:700}.order-status-change{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.order-status-change__form{display:flex;align-items:center;gap:6px}.order-status-change__select{min-width:180px}.order-details-tabs{display:flex;gap:6px;flex-wrap:wrap}.order-details-tab{border:1px solid #d6deea;border-radius:8px;padding:5px 10px;color:#475569;font-size:12px;background:#f8fafc;cursor:pointer}.order-details-tab.is-active{border-color:#bfdbfe;color:#1d4ed8;background:#eff6ff;font-weight:700}.order-item-cell{display:grid;grid-template-columns:44px 1fr;gap:8px;align-items:center;min-width:260px}.order-item-thumb{width:44px;height:44px;border-radius:6px;border:1px solid #dbe3ef;object-fit:cover}.order-item-thumb--empty{display:inline-block;background:#eef2f7;border-style:dashed}.order-item-name{font-weight:600;color:#0f172a}.order-grid-2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px}.order-grid-3{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.order-kv{margin:0;display:grid;grid-template-columns:150px 1fr;gap:6px 10px;font-size:12px}.payment-summary{display:grid;gap:6px;max-width:420px}.payment-summary__row{display:flex;align-items:center;gap:10px;font-size:12px}.payment-summary__label{width:150px;flex-shrink:0;color:#64748b}.payment-summary__value{font-weight:600;color:#0f172a}.order-kv dt{color:#64748b}.order-kv dd{margin:0;color:#0f172a;font-weight:600}.order-address{display:grid;gap:3px;font-size:12px;color:#0f172a}.order-events{display:grid;gap:8px}.order-event{border:1px solid #e2e8f0;border-radius:8px;padding:8px;background:#fbfdff}.order-event__head{color:#64748b;font-size:11px}.order-event__body{margin-top:4px;color:#0f172a;font-size:12px}.order-tab-panel{display:none}.order-tab-panel.is-active{display:block}.manual-tracking-form{display:flex;gap:8px;align-items:center}.manual-tracking-form .form-control{max-width:220px}.order-empty-placeholder{border:1px dashed #cbd5e1;border-radius:8px;min-height:180px;background:#f8fafc}.order-status-badge{display:inline-flex;align-items:center;justify-content:center;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:700;border:1px solid #cbd5e1;color:#334155;background:#f8fafc}.order-status-badge.is-info{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}.order-status-badge.is-success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}.order-status-badge.is-danger{border-color:#fecaca;background:#fef2f2;color:#b91c1c}.order-status-badge.is-warn{border-color:#fde68a;background:#fffbeb;color:#92400e}.order-status-badge.is-empty{color:#94a3b8}.order-buyer{display:grid;gap:2px}.order-buyer__name{color:#0f172a;font-weight:600}.order-buyer__email{color:#64748b;font-size:12px}.table-inline-action{display:inline-block;margin-right:6px}.product-name-cell{display:inline-flex;align-items:center;gap:10px}.product-name-thumb{width:60px;height:60px;border-radius:6px;object-fit:cover;border:1px solid var(--c-border);background:#f8fafc}.product-name-thumb--empty{display:inline-block;width:60px;height:60px;border-radius:6px;border:1px dashed #cbd5e0;background:#f8fafc}.product-name-thumb-btn{border:0;padding:0;background:rgba(0,0,0,0);cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.product-name-thumb-btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:8px}.modal--image-preview{width:min(760px,100%)}.product-image-preview__img{display:block;width:100%;max-height:70vh;object-fit:contain;border-radius:8px;background:#f8fafc}.product-images-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));gap:12px}.product-image-card{border:1px solid #dfe3ea;border-radius:10px;padding:10px;background:#fff}.product-image-card__thumb-wrap{position:relative;border-radius:8px;overflow:hidden;background:#f2f5f8}.product-image-card__thumb{width:100%;height:160px;object-fit:cover;display:block}.product-image-card__thumb.is-empty{height:160px;display:grid;place-items:center;color:#6b7785;font-size:12px}.product-image-card__badge{display:none;position:absolute;top:8px;left:8px;background:#1f7a43;color:#fff;padding:3px 8px;border-radius:999px;font-size:11px}.product-image-card.is-main .product-image-card__badge{display:inline-block}.product-image-card__meta{margin-top:8px;font-size:11px;line-height:1.25;color:#5f6b79;overflow-wrap:anywhere}.product-image-card__actions{margin-top:10px;display:grid;grid-template-columns:1fr;gap:8px}.product-image-card__actions .btn{min-height:34px;font-size:12px;line-height:1.2;padding:6px 10px}.product-links-search-form{display:grid;gap:12px;grid-template-columns:minmax(220px, 320px) minmax(220px, 1fr) auto;align-items:end}.product-links-head{display:grid;gap:8px;grid-template-columns:repeat(3, minmax(0, 1fr))}.product-tabs-nav{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.product-links-inline-form{display:grid;gap:8px;grid-template-columns:minmax(140px, 1fr) minmax(140px, 1fr) auto;align-items:center}.product-links-actions-row{display:flex;align-items:center;gap:8px;flex-wrap:nowrap}.product-links-actions-row .product-links-relink-form{flex:1 1 auto}.product-links-unlink-form{margin:0;flex:0 0 auto}.product-link-status-cell{display:inline-flex;align-items:center;gap:6px}.product-link-alert-indicator{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;border:1px solid #f59e0b;background:#fffbeb;color:#b45309;font-size:12px;font-weight:700;cursor:help}.product-link-events-list{margin:0;padding:0;list-style:none;display:grid;gap:4px}.product-link-events-list li{display:grid;gap:2px}.product-link-events-type{font-weight:600;color:var(--c-text-strong)}.product-link-events-date{color:var(--c-muted);font-size:12px}.product-show-images-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));gap:12px}.product-show-image-card{border:1px solid var(--c-border);border-radius:10px;background:#fff;padding:10px;overflow:hidden}.product-show-image-card__meta{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;min-width:0}.product-show-image-path{font-size:12px;min-width:0;overflow:hidden}.product-show-image-path summary{cursor:pointer;color:var(--c-muted, #888);list-style:none;user-select:none;white-space:nowrap}.product-show-image-path summary::-webkit-details-marker{display:none}.product-show-image-path summary::after{content:" ▾"}.product-show-image-path[open] summary::after{content:" ▴"}.product-show-image-path__url{margin-top:4px;word-break:break-all;overflow-wrap:break-word;font-size:11px}.product-show-image{width:100%;max-height:260px;object-fit:cover;border-radius:8px;border:1px solid #d9e0ea}.shipment-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px}.searchable-select{position:relative}.searchable-select__trigger{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;min-height:34px}.searchable-select__trigger::after{content:"";width:0;height:0;border-left:4px solid rgba(0,0,0,0);border-right:4px solid rgba(0,0,0,0);border-top:5px solid var(--c-text-muted, #6b7280);margin-left:8px;flex-shrink:0}.searchable-select__trigger--placeholder{color:var(--c-text-muted, #6b7280)}.searchable-select__dropdown{display:none;position:absolute;left:0;right:0;top:100%;z-index:50;max-height:280px;overflow:auto;background:#fff;border:1px solid var(--c-border);border-top:0;border-radius:0 0 8px 8px;box-shadow:0 8px 20px rgba(15,23,42,.12)}.searchable-select__dropdown.is-open{display:block}.searchable-select__search{position:sticky;top:0;border:none !important;border-bottom:1px solid var(--c-border) !important;border-radius:0 !important;box-shadow:none !important;font-size:13px;background:#fff;z-index:1}.searchable-select__option{padding:7px 10px;font-size:13px;cursor:pointer;color:var(--c-text-strong)}.searchable-select__option:hover{background:#f1f5f9}.searchable-select__option.is-selected{background:#edf2ff;font-weight:600}.flash{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.flash--success{background:#f0fdf4;border:1px solid #bbf7d0;color:#166534}.flash--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.content-tabs-card{margin-top:0}.content-tabs-nav{display:flex;gap:4px;border-bottom:2px solid var(--c-border);margin-bottom:16px;flex-wrap:wrap}.content-tab-btn{padding:8px 16px;border:none;background:none;cursor:pointer;font-size:14px;font-weight:500;color:var(--c-text-muted, #6b7280);border-bottom:2px solid rgba(0,0,0,0);margin-bottom:-2px;border-radius:4px 4px 0 0;transition:color .15s,border-color .15s}.content-tab-btn:hover{color:var(--c-text-strong, #111827)}.content-tab-btn.is-active{color:var(--c-primary, #2563eb);border-bottom-color:var(--c-primary, #2563eb)}.content-tab-panel{display:none}.content-tab-panel.is-active{display:block}.shoppro-tabs-toolbar{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-bottom:10px;flex-wrap:wrap}.shoppro-tabs-toolbar__field{margin:0;min-width:260px;max-width:420px;flex:1 1 320px}.shoppro-tabs-toolbar__field .form-control{width:100%}.shoppro-tabs-toolbar__actions{display:inline-flex;align-items:center;gap:8px}.dm-carrier-select{min-width:140px}.dm-service-wrap{min-width:200px}.dm-service-wrap .dm-inpost-panel .form-control,.dm-service-wrap .dm-apaczka-panel .form-control{width:100%}.integration-settings-group{grid-column:1/-1;border:1px solid var(--c-border);border-radius:10px;background:#f8fbff;padding:10px}.integration-settings-group__head{margin-bottom:8px;padding:4px 0;border-bottom:1px solid #e2e8f0}.integration-settings-group__title{margin:0;font-size:14px;font-weight:600;letter-spacing:.01em;color:var(--c-text-strong, #1e293b)}.integration-settings-group__desc{margin:4px 0 0;color:#4b5563}.integration-settings-group__grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:10px 12px;align-items:start}.integration-settings-group__full{grid-column:1/-1}.integration-settings-group__grid .form-field{margin:0;align-self:start}.integration-settings-group__grid .form-control{min-height:34px;height:34px}.integration-settings-group__grid input[type=date].form-control{line-height:1.2}.integration-settings-checkboxes{border:0;padding:0;margin:0}.integration-settings-checkboxes .field-label{display:block;margin-bottom:2px}.integration-settings-checkboxes__list{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:6px 12px}.integration-settings-checkboxes__item{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#334155}@media(max-width: 768px){.app-shell{flex-direction:column}.sidebar{width:100% !important;min-width:0 !important;border-right:0;border-bottom:1px solid #243041;padding:14px;overflow-x:auto}.sidebar__brand{margin:0 0 10px;font-size:22px}.sidebar__collapse-btn{display:none}.sidebar__nav{display:flex;gap:8px;overflow-x:auto}.sidebar__link{white-space:nowrap}.topbar{padding:0 14px}.container{margin-top:16px;width:calc(100% - 16px);margin-left:8px;margin-right:8px;padding:0 3px 12px}.settings-grid{grid-template-columns:1fr}.page-head{flex-direction:column;align-items:flex-start}.orders-stats{grid-template-columns:1fr;width:100%}.order-show-layout{grid-template-columns:1fr}.order-statuses-side{position:static;top:auto}.order-details-actions{justify-content:flex-start}.order-grid-2,.order-grid-3{grid-template-columns:1fr}.order-kv{grid-template-columns:1fr;gap:2px}.filters-grid,.form-grid,.form-grid-2,.form-grid-3,.form-grid-4,.shipment-grid,.statuses-form,.statuses-inline-form,.table-list-filters,.product-links-search-form,.product-links-inline-form{grid-template-columns:1fr}.statuses-dnd-item__content{display:block}.statuses-inline-delete{margin-top:6px}.filters-actions{align-items:center}.table-list__header,.table-list__footer{align-items:flex-start}.product-links-head{grid-template-columns:1fr}.integration-settings-group__grid{grid-template-columns:1fr}.integration-settings-checkboxes__list{grid-template-columns:1fr}.card{padding:12px}.modal--image-preview{width:min(92vw,100%)}.email-tpl-editor-wrap{flex-direction:column}.email-tpl-var-panel{min-width:200px}.modal-box{width:95vw;max-height:90vh}}.email-tpl-editor-wrap{display:flex;flex-direction:column;border:1px solid var(--c-border);border-radius:6px;overflow:hidden}.email-tpl-toolbar{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--c-bg-subtle, #f8f9fa);border-bottom:1px solid var(--c-border)}.email-tpl-var-dropdown{position:relative}.email-tpl-var-panel{position:absolute;top:100%;left:0;z-index:50;min-width:260px;max-height:320px;overflow-y:auto;background:var(--c-bg);border:1px solid var(--c-border);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.12);padding:6px;margin-top:4px}.email-var-group:not(:first-child){margin-top:6px;padding-top:6px;border-top:1px solid var(--c-border)}.email-var-group__label{font-size:11px;font-weight:600;text-transform:uppercase;color:var(--c-text-muted);padding:2px 4px;letter-spacing:.03em}.email-var-item{display:block;width:100%;text-align:left;padding:3px 6px;margin:1px 0;border:none;background:none;font-size:12px;font-family:"Roboto Mono",monospace;color:var(--c-text);border-radius:3px;cursor:pointer}.email-var-item:hover{background:var(--c-primary);color:#fff}#js-quill-editor{min-height:200px}#js-quill-editor .ql-editor{min-height:200px;font-size:13px}.modal-overlay{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45)}.modal-box{width:min(680px,90vw);max-height:80vh;background:var(--c-bg);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.2);display:flex;flex-direction:column;overflow:hidden}.modal-box__header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--c-border)}.modal-box__title{margin:0;font-size:15px;font-weight:600}.modal-box__close{background:none;border:none;font-size:22px;line-height:1;cursor:pointer;color:var(--c-text-muted);padding:0 4px}.modal-box__close:hover{color:var(--c-text)}.modal-box__body{padding:12px 16px;overflow-y:auto;flex:1} +:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-action-primary: #0f766e;--c-action-primary-dark: #0b5f59;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #b0bec5;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--focus-ring-action: 0 0 0 3px rgba(15, 118, 110, 0.18);--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.06)}.btn{display:inline-flex;align-items:center;justify-content:center;min-height:34px;padding:6px 12px;border:1px solid rgba(0,0,0,0);border-radius:8px;font:inherit;font-weight:600;text-decoration:none;cursor:pointer;transition:background-color .2s ease,border-color .2s ease,color .2s ease,transform .1s ease}.btn--primary{color:#fff;background:var(--c-action-primary)}.btn--primary:hover{background:var(--c-action-primary-dark)}.btn--secondary{color:var(--c-text-strong);border-color:var(--c-border);background:var(--c-surface)}.btn--secondary:hover{border-color:#cbd5e0;background:#f8fafc}.btn--danger{color:#fff;border-color:#b91c1c;background:#dc2626}.btn--danger:hover{border-color:#991b1b;background:#b91c1c}.btn--sm{min-height:28px;padding:3px 10px;font-size:12px}.btn--block{width:100%}.btn--disabled{opacity:.3;cursor:not-allowed;pointer-events:none}.btn:active{transform:translateY(1px)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring-action);border-color:var(--c-action-primary)}.form-control{width:100%;min-height:30px;border:1px solid var(--c-border);border-radius:6px;padding:4px 8px;font:inherit;color:var(--c-text-strong);background:#fff;transition:border-color .2s ease,box-shadow .2s ease}.form-control:focus{outline:none;border-color:var(--c-primary);box-shadow:var(--focus-ring)}.input{min-height:34px;border:1px solid var(--c-border);border-radius:8px;padding:5px 10px;font:inherit;color:var(--c-text-strong);background:#fff}.input--sm{min-height:28px;padding:3px 8px;font-size:12px}.flash{padding:8px 12px;border-radius:6px;font-size:13px}.flash--success{border:1px solid #b7ebcf;background:#f0fff6;color:#0f6b39}.flash--error{border:1px solid #fed7d7;background:#fff5f5;color:var(--c-danger)}.alert{padding:12px 14px;border-radius:8px;border:1px solid rgba(0,0,0,0);font-size:13px;min-height:44px}.alert--danger{border-color:#fed7d7;background:#fff5f5;color:var(--c-danger)}.alert--success{border-color:#b7ebcf;background:#f0fff6;color:#0f6b39}.alert--warning{border-color:#f7dd8b;background:#fff8e8;color:#815500}.form-field{display:grid;gap:5px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}.table-wrap{width:100%;overflow-x:auto}.table-wrap--visible{overflow:visible !important;overflow-x:visible !important}.table{width:100%;border-collapse:collapse;background:var(--c-surface)}.table th,.table td{padding:10px 12px;border-bottom:1px solid var(--c-border);text-align:left}.table th{color:var(--c-text-strong);font-weight:700;background:#f8fafc}.table--details th{white-space:nowrap}.table--details th:first-child,.table--details td:first-child{width:36px;text-align:center}.pagination{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.pagination__item{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 10px;border-radius:8px;border:1px solid var(--c-border);color:var(--c-text-strong);background:var(--c-surface);text-decoration:none;font-weight:600}.pagination__item:hover{border-color:#cbd5e0;background:#f8fafc}.pagination__item.is-active{border-color:var(--c-primary);color:var(--c-primary);background:#edf2ff}.receipt-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;padding-bottom:12px;border-bottom:2px solid var(--c-text-strong)}.receipt-header__seller{flex:1}.receipt-header__seller strong{font-size:14px;display:block;margin-bottom:4px}.receipt-header__title{text-align:right}.receipt-header__title h1{font-size:18px;font-weight:700;margin-bottom:4px}.receipt-print{max-width:700px;margin:0 auto}@media print{.receipt-print{max-width:100%}}.email-send-overlay{position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center}.email-send-modal{background:var(--c-card-bg, #fff);border-radius:8px;width:580px;max-width:95vw;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,.2)}.email-send-modal__header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--c-border, #e0e0e0)}.email-send-modal__header h3{margin:0;font-size:15px}.email-send-modal__close{background:none;border:none;font-size:20px;cursor:pointer;color:var(--c-text-muted, #888);padding:0 4px}.email-send-modal__close:hover{color:var(--c-text, #333)}.email-send-modal__body{padding:16px;overflow-y:auto;flex:1}.email-send-modal__field{margin-bottom:10px}.email-send-modal__field label{display:block;font-size:12px;font-weight:600;margin-bottom:4px;color:var(--c-text-muted, #666)}.email-send-modal__field .input{width:100%}.email-send-modal__actions-top{margin-bottom:10px}.email-send-modal__footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--c-border, #e0e0e0)}.email-send-preview{border:1px solid var(--c-border, #e0e0e0);border-radius:4px;padding:12px;max-height:280px;overflow-y:auto;background:var(--c-bg, #fafafa)}.email-send-preview__subject{font-weight:600;font-size:13px;margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid var(--c-border, #e0e0e0)}.email-send-preview__body{font-size:13px;line-height:1.5}.email-send-preview__body p{margin:0 0 8px}.email-send-preview__attachments{margin-top:8px;padding-top:8px;border-top:1px solid var(--c-border, #e0e0e0);font-size:12px;color:var(--c-text-muted, #666)}.section-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.automation-row{display:flex;align-items:flex-start;gap:8px;padding:10px 12px;background:var(--c-surface, #f8f9fa);border:1px solid var(--c-border, #dee2e6);border-radius:6px}.automation-row__fields{flex:1;display:flex;flex-direction:column;gap:6px}.automation-row__type{max-width:280px}.automation-row__config{display:flex;flex-wrap:wrap;gap:8px}.automation-row__config .form-control{min-width:200px;max-width:300px}.automation-row__remove{flex-shrink:0;margin-top:2px;line-height:1;font-size:16px;padding:2px 8px}.checkbox-group{display:flex;flex-wrap:wrap;gap:4px 16px}.checkbox-label{display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;white-space:nowrap}.checkbox-label input[type=checkbox]{margin:0}.automation-actions-cell{white-space:nowrap}.automation-inline-form{display:inline}.automation-history-filters{display:grid;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));gap:8px;align-items:end}.automation-history-filters .form-field{margin:0}.automation-history-filters .field-label{font-size:12px;margin-bottom:4px}.automation-history-filters .form-control{min-height:34px}.automation-history-filters__actions{display:flex;gap:6px;align-items:center;justify-content:flex-start;padding-bottom:1px}.print-status-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:.75rem;font-weight:600;line-height:1.4}.print-status-badge--pending{background-color:#fff3cd;color:#856404}.print-status-badge--completed{background-color:#d4edda;color:#155724}.print-status-badge--failed{background-color:#f8d7da;color:#721c24}.print-queue-filters{display:flex;gap:4px}.print-queue-table td,.print-queue-table th{padding:6px 8px;font-size:.85rem}.print-queue-actions{display:inline-flex;align-items:center;gap:6px}.print-queue-delete-form{margin:0}.btn--outline-primary{background:rgba(0,0,0,0);border:1px solid var(--c-action-primary);color:var(--c-action-primary);cursor:pointer;border-radius:3px;font-size:.75rem;padding:3px 8px;transition:background-color .15s,color .15s}.btn--outline-primary:hover{background-color:var(--c-action-primary);color:#fff}.btn--outline-primary:disabled{opacity:.6;cursor:not-allowed}.btn--outline-primary.is-success{border-color:#28a745;color:#28a745}.shipment-presets{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:16px}.shipment-presets__btn{display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border:none;border-radius:6px;background:var(--preset-color, #3b82f6);color:#fff;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .15s;line-height:1.4}.shipment-presets__btn:hover{opacity:.85}.shipment-presets__add{display:inline-flex;align-items:center;gap:4px;padding:6px 14px;border:1px dashed #ccc;border-radius:6px;background:rgba(0,0,0,0);color:#666;font-size:13px;cursor:pointer;transition:border-color .15s,color .15s;line-height:1.4}.shipment-presets__add:hover{border-color:#999;color:#444}.preset-modal{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:1000;display:flex;align-items:center;justify-content:center}.preset-modal__content{background:#fff;border-radius:8px;padding:24px;min-width:360px;max-width:420px;box-shadow:0 8px 32px rgba(0,0,0,.2)}.preset-modal__content h3{margin:0 0 4px;font-size:16px}.preset-modal__colors{display:flex;gap:8px;flex-wrap:wrap}.preset-modal__color-swatch{width:28px;height:28px;border-radius:50%;cursor:pointer;border:2px solid rgba(0,0,0,0);transition:border-color .15s}.preset-modal__color-swatch:hover{border-color:#aaa}.preset-modal__color-swatch.is-selected{border-color:#333}.shipment-presets__btn-wrap{position:relative;display:inline-flex}.shipment-presets__btn-wrap:hover .shipment-presets__edit-icon{opacity:1}.shipment-presets__edit-icon{position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:#fff;border:1px solid #ddd;font-size:10px;line-height:16px;text-align:center;cursor:pointer;opacity:0;transition:opacity .15s;padding:0;color:#666;z-index:2}.shipment-presets__edit-icon:hover{background:#f3f4f6;border-color:#999}.shipment-presets__dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.12);z-index:100;min-width:200px;padding:4px 0}.shipment-presets__dropdown-item{padding:6px 14px;font-size:13px;cursor:pointer;white-space:nowrap}.shipment-presets__dropdown-item:hover{background:#f3f4f6}.shipment-presets__dropdown-item.is-danger{color:#ef4444}.shipment-presets__dropdown-item.is-danger:hover{background:#fef2f2}.delivery-badge{display:inline-block;padding:2px 8px;border-radius:3px;font-size:.8em;font-weight:500;white-space:nowrap}.delivery-badge--unknown{background:#f5f5f5;color:#999}.delivery-badge--created{background:#e3f2fd;color:#1565c0}.delivery-badge--confirmed{background:#bbdefb;color:#0d47a1}.delivery-badge--in_transit{background:#fff3e0;color:#e65100}.delivery-badge--out_for_delivery{background:#ffe0b2;color:#bf360c}.delivery-badge--ready_for_pickup{background:#f3e5f5;color:#6a1b9a}.delivery-badge--delivered{background:#e8f5e9;color:#2e7d32}.delivery-badge--returned{background:#ffebee;color:#c62828}.delivery-badge--cancelled{background:#e0e0e0;color:#616161}.delivery-badge--problem{background:#fff8e1;color:#f57f17}.tracking-link{margin-left:4px;text-decoration:none;font-size:.85em}.dsm-row--custom{background:rgba(59,130,246,.06)}.dsm-raw-status{font-size:.82rem;background:var(--surface-alt, #f1f5f9);padding:2px 6px;border-radius:3px;white-space:nowrap}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;font-size:13px;color:var(--c-text);background:var(--c-bg)}a{color:var(--c-primary)}.app-shell{min-height:100vh;display:flex}.sidebar{width:260px;min-width:260px;flex-shrink:0;overflow:hidden;transition:width .22s ease,min-width .22s ease;border-right:1px solid #243041;background:#111a28;padding:18px 10px;display:flex;flex-direction:column}.sidebar.is-collapsed{width:52px;min-width:52px}.sidebar__brand{display:flex;align-items:center;justify-content:space-between;margin:4px 4px 16px;gap:6px;min-width:0}.sidebar__brand-text{color:#e9f0ff;font-size:24px;font-weight:300;letter-spacing:-0.02em;white-space:nowrap;overflow:hidden;flex:1;min-width:0}.sidebar__brand-text strong{font-weight:700}.sidebar__collapse-btn{flex-shrink:0;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0);border:1px solid #2a3a54;border-radius:6px;color:#64748b;cursor:pointer;padding:0;transition:background .15s,color .15s}.sidebar__collapse-btn:hover{background:#1b2a3f;color:#cbd5e1}.sidebar__collapse-icon{display:block;transition:transform .22s ease;flex-shrink:0}.sidebar.is-collapsed .sidebar__collapse-icon{transform:rotate(180deg)}.sidebar__nav{display:grid;gap:4px}.sidebar__link{display:flex;align-items:center;gap:9px;white-space:nowrap;border-radius:8px;padding:9px 10px;text-decoration:none;color:#cbd5e1;font-weight:600}.sidebar__link:hover{color:#f8fafc;background:#1b2a3f}.sidebar__link.is-active{color:#fff;background:#2e4f93}.sidebar__group{display:grid;gap:2px}.sidebar__group-toggle{list-style:none;border-radius:8px;padding:9px 10px;color:#cbd5e1;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:9px;white-space:nowrap;user-select:none}.sidebar__group-toggle::-webkit-details-marker{display:none}.sidebar__group:hover .sidebar__group-toggle,.sidebar__group-toggle:hover{color:#f8fafc;background:#1b2a3f}.sidebar__group.is-active .sidebar__group-toggle{color:#fff;background:#2e4f93}.sidebar__icon{flex-shrink:0;width:18px;height:18px;display:flex;align-items:center;justify-content:center;opacity:.85}.sidebar__label{flex:1;min-width:0;overflow:hidden}.sidebar__toggle-arrow{flex-shrink:0;margin-left:auto;opacity:.5;transition:transform .18s ease}details[open]>.sidebar__group-toggle .sidebar__toggle-arrow{transform:rotate(180deg)}.sidebar__group-links{display:grid;gap:2px;padding-left:12px;overflow:hidden}.sidebar__sublink{border-radius:6px;padding:7px 10px 7px 8px;text-decoration:none;color:#94a3b8;font-size:12.5px;font-weight:500;display:flex;align-items:center;gap:8px;white-space:nowrap}.sidebar__sublink::before{content:"";flex-shrink:0;width:5px;height:5px;border-radius:50%;background:rgba(148,163,184,.3);transition:background .15s}.sidebar__sublink:hover{color:#e2e8f0;background:#1b2a3f}.sidebar__sublink:hover::before{background:rgba(148,163,184,.65)}.sidebar__sublink.is-active{color:#fff;background:rgba(46,79,147,.55)}.sidebar__sublink.is-active::before{background:#93c5fd}.app-main{flex:1;min-width:0}.topbar{height:50px;border-bottom:1px solid var(--c-border);background:var(--c-surface);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100}.brand{font-size:22px;font-weight:300;letter-spacing:-0.02em;color:var(--c-text-strong)}.brand strong{font-weight:700}.container{max-width:none;width:calc(100% - 20px);margin:12px 10px;padding:0 4px 14px}.card{background:var(--c-surface);border-radius:10px;box-shadow:var(--shadow-card);padding:14px}.card h1{margin:0 0 10px;color:var(--c-text-strong);font-size:24px;font-weight:700}.muted{color:var(--c-muted)}.accent{color:var(--c-primary);font-weight:600}.users-form{display:grid;gap:14px;max-width:460px}.form-field{margin-bottom:12px}.section-title{margin:0;color:var(--c-text-strong);font-size:18px;font-weight:700}h2.section-title,h3.section-title,h4.section-title{display:flex;align-items:center;gap:6px;font-weight:600;padding:6px 0;border-bottom:1px solid #e2e8f0;color:var(--c-primary, #2563eb)}h2.section-title::before,h3.section-title::before,h4.section-title::before{content:"■";font-size:.55em;opacity:.5}h3.section-title,h4.section-title{font-size:15px}h3.section-title::before,h4.section-title::before{content:"◆";font-size:.5em}.mt-0{margin-top:0}.mt-4{margin-top:4px}.mt-12{margin-top:8px}.mt-16{margin-top:12px}.settings-grid{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.settings-nav{display:flex;gap:8px;flex-wrap:wrap}.settings-nav__link{text-decoration:none;border:1px solid var(--c-border);border-radius:8px;padding:8px 12px;color:var(--c-text-strong);font-weight:600}.settings-nav__link:hover{background:#f8fafc}.settings-nav__link.is-active{border-color:var(--c-primary);color:var(--c-primary);background:#edf2ff}.settings-stat{border:1px solid var(--c-border);border-radius:8px;padding:12px;background:#f8fafc}.settings-stat__label{display:block;color:var(--c-muted);font-size:12px;margin-bottom:4px}.settings-stat__value{color:var(--c-text-strong);font-size:20px}.settings-logs{margin:0;padding:12px;border-radius:8px;border:1px solid var(--c-border);background:#0b1220;color:#d1d5db;font-size:12px;line-height:1.5;overflow:auto}.settings-allegro-callback{display:block;width:100%;padding:8px 10px;border:1px solid var(--c-border);border-radius:8px;background:#f8fafc;color:var(--c-text-strong);font-size:12px;line-height:1.45;word-break:break-all}.page-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.filters-grid{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.filters-actions{display:flex;align-items:center;gap:8px}.product-form .form-control{width:100%}.form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px}.form-grid-2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px;align-items:start}.form-grid-3{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px;align-items:start}.form-grid-4{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:12px;align-items:start}.form-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-start}.form-actions .btn{align-self:flex-start}.statuses-form{display:grid;gap:8px;grid-template-columns:repeat(2, minmax(0, 1fr))}.statuses-form .form-actions{grid-column:1/-1}.statuses-color-input{min-height:32px;padding:2px}.statuses-hint{grid-column:1/-1;margin:0}.statuses-group-block{border:1px solid var(--c-border);border-radius:10px;padding:8px;background:#fbfdff}.statuses-group-block__head{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap}.statuses-group-block__title{margin:0;display:inline-flex;align-items:center;gap:6px;color:var(--c-text-strong);font-size:14px}.statuses-color-dot{width:12px;height:12px;border-radius:999px;border:1px solid rgba(15,23,42,.15)}.statuses-dnd-list{margin:6px 0 0;padding:0;list-style:none;display:grid;gap:6px}.statuses-dnd-item{display:grid;grid-template-columns:24px 1fr;gap:6px;border:1px solid #dce4f0;border-radius:8px;background:#fff;padding:6px}.statuses-dnd-item__content{display:flex;align-items:center;gap:6px;min-width:0}.statuses-dnd-item.is-dragging{opacity:.6}.statuses-dnd-item__drag{display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;border-radius:6px;color:#64748b;cursor:grab;user-select:none;font-weight:700;font-size:12px}.statuses-dnd-item__drag:active{cursor:grabbing}.statuses-inline-form{display:grid;gap:6px}.statuses-inline-form--row{grid-template-columns:minmax(180px, 1.4fr) minmax(150px, 1fr) auto auto auto;align-items:center;flex:1 1 auto;min-width:0}.statuses-inline-form--row-group{grid-template-columns:minmax(180px, 1.5fr) 56px auto auto auto;align-items:center;flex:1 1 auto;min-width:0}.statuses-inline-form--row .form-control,.statuses-inline-form--row-group .form-control{min-height:30px;padding:4px 8px}.statuses-inline-form--row .btn,.statuses-inline-form--row-group .btn,.statuses-inline-delete .btn{min-height:30px;padding:4px 10px;font-size:12px}.statuses-inline-check{margin-top:0;white-space:nowrap;font-size:12px}.statuses-inline-delete{margin:0;flex:0 0 auto}.statuses-code-label{font-size:12px;color:var(--c-muted)}.statuses-code-readonly{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;font-size:12px}.statuses-code-readonly code{background:#eef2f7;border-radius:6px;padding:1px 6px;color:#1f2937;font-size:12px}.field-inline{display:flex;align-items:center;gap:8px;margin-top:2px}.modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.5);display:flex;align-items:center;justify-content:center;padding:16px;z-index:200}.modal-backdrop[hidden]{display:none}.modal{width:min(560px,100%);background:#fff;border-radius:10px;box-shadow:0 20px 40px rgba(15,23,42,.35);overflow:hidden}.modal__header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:16px 18px;border-bottom:1px solid var(--c-border)}.modal__header h3{margin:0;font-size:18px;color:var(--c-text-strong)}.modal__body{padding:16px 18px 18px}.status-pill{display:inline-flex;align-items:center;justify-content:center;border:1px solid #fed7d7;background:#fff5f5;color:#9b2c2c;padding:2px 8px;border-radius:999px;font-size:12px;font-weight:600}.status-pill.is-active{border-color:#b7ebcf;background:#f0fff6;color:#0f6b39}.table-row-actions{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.table-row-actions form{margin:0}.table-list{display:grid;gap:14px}.table-list__header{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.table-list__left{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}.table-list-header-actions{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap}.js-filter-toggle-btn.is-active{border-color:#cbd5e0;background:#edf2ff;color:var(--c-primary-dark)}.table-filter-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;font-size:11px;font-weight:700;color:#fff;background:var(--c-primary);border-radius:999px}.table-filters-wrapper{display:none}.table-filters-wrapper.is-open{display:block}.table-list-filters{display:grid;gap:12px;grid-template-columns:repeat(auto-fit, minmax(170px, 1fr));align-items:end}.table-col-toggle-wrapper{position:relative}.table-col-toggle-dropdown{display:none;position:absolute;right:0;top:calc(100% + 6px);z-index:30;width:260px;max-height:360px;overflow:auto;border:1px solid var(--c-border);border-radius:10px;background:#fff;box-shadow:0 10px 25px rgba(15,23,42,.12)}.table-col-toggle-dropdown.is-open{display:block}.table-col-toggle-header{padding:10px 12px;border-bottom:1px solid var(--c-border);font-size:12px;font-weight:700;color:var(--c-muted)}.table-col-toggle-item{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13px;color:var(--c-text-strong)}.table-col-toggle-item:hover{background:#f8fafc}.table-col-toggle-footer{border-top:1px solid var(--c-border);padding:8px 12px}.table-col-hidden{display:none}.table-col-switch{position:relative;display:inline-block;width:34px;min-width:34px;height:18px}.table-col-switch input{opacity:0;width:0;height:0;position:absolute}.table-col-switch-slider{position:absolute;top:0;left:0;right:0;bottom:0;background:#cbd5e1;border-radius:999px;transition:background-color .2s ease}.table-col-switch-slider::before{content:"";position:absolute;height:14px;width:14px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:transform .2s ease}.table-col-switch input:checked+.table-col-switch-slider{background:#16a34a}.table-col-switch input:checked+.table-col-switch-slider::before{transform:translateX(16px)}.table-sort-link{display:inline-flex;align-items:center;gap:6px;color:var(--c-text-strong);text-decoration:none}.table-sort-link:hover{color:var(--c-primary-dark)}.table-sort-icon.is-muted{color:#a0aec0}.table-list__footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.table-list-per-page-form{display:inline-flex;align-items:center;gap:8px}.table-list-per-page-form .form-control{min-width:90px}.table-select-col{width:44px;text-align:center}.table-select-toggle{display:inline-flex;align-items:center;justify-content:center}.table-select-toggle input[type=checkbox]{width:16px;height:16px}.orders-page .orders-head{background:linear-gradient(120deg, #f8fbff 0%, #eef5ff 100%);border:1px solid #dbe7fb}.orders-page .table-list{border:1px solid #dde5f2;border-radius:12px;box-shadow:0 6px 16px rgba(20,44,86,.08)}.orders-page .table-list__header{padding:10px 6px 2px}.orders-page .table-list-filters{padding:6px 6px 2px;border-top:1px solid #ebf0f7;border-bottom:1px solid #ebf0f7;background:#f9fbff}.orders-page .table-wrap{border-radius:10px;overflow:hidden;border:1px solid #e7edf6}.orders-page .table thead th{background:#f3f7fd;color:#30435f;font-size:12px;text-transform:uppercase;letter-spacing:.03em}.orders-page .table tbody td{vertical-align:middle;padding-top:10px;padding-bottom:10px;border-bottom-color:#edf2f8}.orders-page .table tbody tr:hover td{background:#f9fcff}.orders-list-page{padding:10px;margin-bottom:10px}.orders-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.orders-stats{display:inline-grid;grid-template-columns:repeat(3, minmax(86px, auto));gap:8px}.orders-stat{border:1px solid #d8e2f0;background:#f8fbff;border-radius:8px;padding:6px 8px;line-height:1.15}.orders-stat__label{display:block;color:#5f6f83;font-size:11px;margin-bottom:2px}.orders-stat__value{color:#12233a;font-size:16px;font-weight:700}.orders-ref{display:grid;gap:2px;min-width:170px}.orders-ref__main{font-weight:700;color:#0f1f35;font-size:14px}.orders-ref__meta{display:inline-flex;flex-wrap:wrap;gap:4px 10px;color:#64748b;font-size:12px}.orders-buyer{display:grid;gap:2px}.orders-buyer__name{color:#0f172a;font-weight:600;font-size:14px}.orders-buyer__meta{display:inline-flex;flex-wrap:wrap;gap:4px 10px;color:#64748b;font-size:12px}.orders-status-wrap{display:inline-flex;align-items:center;gap:5px;flex-wrap:wrap;cursor:pointer}.orders-status-wrap .order-tag{cursor:pointer}.orders-status-dropdown{position:fixed;z-index:9999;min-width:180px;max-height:280px;overflow-y:auto;background:#fff;border:1px solid #d8e1ef;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.12);padding:4px 0}.orders-status-dropdown__group-header{padding:6px 12px 2px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#94a3b8}.orders-status-dropdown__group-header:not(:first-child){border-top:1px solid #f1f5f9;margin-top:2px;padding-top:8px}.orders-status-dropdown__item{display:flex;align-items:center;gap:8px;padding:5px 12px;font-size:13px;color:#334155;cursor:pointer;white-space:nowrap}.orders-status-dropdown__item:hover{background:#f1f5f9}.orders-status-dropdown__item.is-current{font-weight:700;background:#f8fafc}.orders-status-dropdown__color-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.order-tag{display:inline-flex;align-items:center;justify-content:center;border:1px solid #d8e1ef;background:#f8fafc;color:#334155;border-radius:999px;padding:2px 8px;font-size:12px;font-weight:700;line-height:1.1}.order-tag.is-info{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}.order-tag.is-success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}.order-tag.is-danger{border-color:#fecaca;background:#fef2f2;color:#b91c1c}.order-tag.is-warn{border-color:#fde68a;background:#fffbeb;color:#92400e}.order-tag.is-cod{border-color:#f9a8d4;background:#fdf2f8;color:#9d174d}.order-tag.is-unpaid{border-color:#fca5a5;background:#fef2f2;color:#b91c1c}.orders-mini{font-size:14px;color:#223247;line-height:1.25}.orders-mini__delivery{font-size:12px;color:#64748b;margin-bottom:2px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.orders-products{display:grid;gap:4px;min-width:240px}.orders-products__meta,.orders-products__more{font-size:12px;color:#64748b}.orders-product{display:grid;grid-template-columns:48px 1fr;gap:6px;align-items:center}.orders-product__thumb{width:48px;height:48px;border-radius:4px;border:1px solid #dbe3ef;object-fit:cover;background:#fff}.orders-product__thumb--empty{display:inline-block;background:#eef2f7;border-style:dashed}.orders-product__txt{min-width:0;display:grid;gap:1px}.orders-product__name{font-size:14px;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.orders-product__qty{font-size:12px;color:#64748b}.orders-image-hover-wrap{position:relative;display:inline-flex;align-items:center;justify-content:center;cursor:zoom-in}.orders-image-hover-popup{display:none;position:fixed;left:auto;top:auto;width:350px;max-height:350px;object-fit:contain;border-radius:8px;background:#fff;box-shadow:0 8px 24px rgba(0,0,0,.18);border:1px solid #dfe3ea;z-index:100;pointer-events:none}.orders-image-hover-wrap:hover .orders-image-hover-popup{display:block}.activity-type-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;white-space:nowrap;background:#e2e8f0;color:#334155}.activity-type-badge--status_change{background:#dbeafe;color:#1e40af}.activity-type-badge--payment{background:#dcfce7;color:#166534}.activity-type-badge--invoice{background:#fef3c7;color:#92400e}.activity-type-badge--shipment{background:#e0e7ff;color:#3730a3}.activity-type-badge--message{background:#f3e8ff;color:#6b21a8}.activity-type-badge--document{background:#fce7f3;color:#9d174d}.activity-type-badge--import{background:#f1f5f9;color:#475569}.activity-type-badge--note{background:#ecfdf5;color:#065f46}.text-nowrap{white-space:nowrap}.orders-money{display:grid;gap:2px}.orders-money__main{color:#0f172a;font-weight:700;font-size:14px}.orders-money__meta{color:#64748b;font-size:12px}.table-list[data-table-list-id=orders]{gap:8px}.table-list[data-table-list-id=orders] .table-list__header{padding:2px 0 0}.table-list[data-table-list-id=orders] .table-list-filters{gap:8px;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr))}.table-list[data-table-list-id=orders] .table th,.table-list[data-table-list-id=orders] .table td{padding:6px 8px}.table-list[data-table-list-id=orders] .table thead th{font-size:12px;text-transform:uppercase;letter-spacing:.02em;white-space:nowrap}.table-list[data-table-list-id=orders] .table tbody td{vertical-align:top;font-size:14px;line-height:1.25}.order-show-layout{display:grid;grid-template-columns:220px minmax(0, 1fr);gap:12px;align-items:start}.order-statuses-side{position:sticky;top:60px;padding:10px}.order-statuses-side__title{font-size:13px;font-weight:700;color:#0f172a;margin-bottom:8px}.order-status-group{margin-bottom:10px}.order-status-group__name{font-size:12px;color:#475569;font-weight:700;margin-bottom:5px}.order-status-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 6px;border-radius:6px;color:#334155;font-size:12px;text-decoration:none}.order-status-row__count{min-width:24px;text-align:center;border-radius:999px;background:var(--status-color, #64748b);padding:1px 6px;font-weight:700;font-size:11px;color:#fff}.order-status-row:hover{background:#f1f5f9}.order-status-row.is-active{background:rgba(15,23,42,.06);color:#0f172a;font-weight:700}.order-show-main{min-width:0}.order-details-actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.order-details-page{padding:12px}.order-details-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.order-back-link{color:#475569;text-decoration:none;font-weight:600}.order-back-link:hover{color:#1d4ed8}.order-details-sub{display:inline-flex;gap:10px;flex-wrap:wrap;color:#64748b;font-size:12px}.order-details-pill{border-radius:999px;padding:5px 10px;background:#eef6ff;border:1px solid #cfe2ff;color:#1d4ed8;font-size:12px;font-weight:700}.order-status-change{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.order-status-change__form{display:flex;align-items:center;gap:6px}.order-status-change__select{min-width:180px}.order-details-tabs{display:flex;gap:6px;flex-wrap:wrap}.order-details-tab{border:1px solid #d6deea;border-radius:8px;padding:5px 10px;color:#475569;font-size:12px;background:#f8fafc;cursor:pointer}.order-details-tab.is-active{border-color:#bfdbfe;color:#1d4ed8;background:#eff6ff;font-weight:700}.order-item-cell{display:grid;grid-template-columns:44px 1fr;gap:8px;align-items:center;min-width:260px}.order-item-thumb{width:44px;height:44px;border-radius:6px;border:1px solid #dbe3ef;object-fit:cover}.order-item-thumb--empty{display:inline-block;background:#eef2f7;border-style:dashed}.order-item-name{font-weight:600;color:#0f172a}.order-grid-2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px}.order-grid-3{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.order-kv{margin:0;display:grid;grid-template-columns:150px 1fr;gap:6px 10px;font-size:12px}.payment-summary{display:grid;gap:6px;max-width:420px}.payment-summary__row{display:flex;align-items:center;gap:10px;font-size:12px}.payment-summary__label{width:150px;flex-shrink:0;color:#64748b}.payment-summary__value{font-weight:600;color:#0f172a}.order-kv dt{color:#64748b}.order-kv dd{margin:0;color:#0f172a;font-weight:600}.order-address{display:grid;gap:3px;font-size:12px;color:#0f172a}.order-events{display:grid;gap:8px}.order-event{border:1px solid #e2e8f0;border-radius:8px;padding:8px;background:#fbfdff}.order-event__head{color:#64748b;font-size:11px}.order-event__body{margin-top:4px;color:#0f172a;font-size:12px}.order-tab-panel{display:none}.order-tab-panel.is-active{display:block}.manual-tracking-form{display:flex;gap:8px;align-items:center}.manual-tracking-form .form-control{max-width:220px}.order-empty-placeholder{border:1px dashed #cbd5e1;border-radius:8px;min-height:180px;background:#f8fafc}.order-status-badge{display:inline-flex;align-items:center;justify-content:center;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:700;border:1px solid #cbd5e1;color:#334155;background:#f8fafc}.order-status-badge.is-info{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}.order-status-badge.is-success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}.order-status-badge.is-danger{border-color:#fecaca;background:#fef2f2;color:#b91c1c}.order-status-badge.is-warn{border-color:#fde68a;background:#fffbeb;color:#92400e}.order-status-badge.is-empty{color:#94a3b8}.order-buyer{display:grid;gap:2px}.order-buyer__name{color:#0f172a;font-weight:600}.order-buyer__email{color:#64748b;font-size:12px}.table-inline-action{display:inline-block;margin-right:6px}.product-name-cell{display:inline-flex;align-items:center;gap:10px}.product-name-thumb{width:60px;height:60px;border-radius:6px;object-fit:cover;border:1px solid var(--c-border);background:#f8fafc}.product-name-thumb--empty{display:inline-block;width:60px;height:60px;border-radius:6px;border:1px dashed #cbd5e0;background:#f8fafc}.product-name-thumb-btn{border:0;padding:0;background:rgba(0,0,0,0);cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.product-name-thumb-btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:8px}.modal--image-preview{width:min(760px,100%)}.product-image-preview__img{display:block;width:100%;max-height:70vh;object-fit:contain;border-radius:8px;background:#f8fafc}.product-images-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));gap:12px}.product-image-card{border:1px solid #dfe3ea;border-radius:10px;padding:10px;background:#fff}.product-image-card__thumb-wrap{position:relative;border-radius:8px;overflow:hidden;background:#f2f5f8}.product-image-card__thumb{width:100%;height:160px;object-fit:cover;display:block}.product-image-card__thumb.is-empty{height:160px;display:grid;place-items:center;color:#6b7785;font-size:12px}.product-image-card__badge{display:none;position:absolute;top:8px;left:8px;background:#1f7a43;color:#fff;padding:3px 8px;border-radius:999px;font-size:11px}.product-image-card.is-main .product-image-card__badge{display:inline-block}.product-image-card__meta{margin-top:8px;font-size:11px;line-height:1.25;color:#5f6b79;overflow-wrap:anywhere}.product-image-card__actions{margin-top:10px;display:grid;grid-template-columns:1fr;gap:8px}.product-image-card__actions .btn{min-height:34px;font-size:12px;line-height:1.2;padding:6px 10px}.product-links-search-form{display:grid;gap:12px;grid-template-columns:minmax(220px, 320px) minmax(220px, 1fr) auto;align-items:end}.product-links-head{display:grid;gap:8px;grid-template-columns:repeat(3, minmax(0, 1fr))}.product-tabs-nav{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.product-links-inline-form{display:grid;gap:8px;grid-template-columns:minmax(140px, 1fr) minmax(140px, 1fr) auto;align-items:center}.product-links-actions-row{display:flex;align-items:center;gap:8px;flex-wrap:nowrap}.product-links-actions-row .product-links-relink-form{flex:1 1 auto}.product-links-unlink-form{margin:0;flex:0 0 auto}.product-link-status-cell{display:inline-flex;align-items:center;gap:6px}.product-link-alert-indicator{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;border:1px solid #f59e0b;background:#fffbeb;color:#b45309;font-size:12px;font-weight:700;cursor:help}.product-link-events-list{margin:0;padding:0;list-style:none;display:grid;gap:4px}.product-link-events-list li{display:grid;gap:2px}.product-link-events-type{font-weight:600;color:var(--c-text-strong)}.product-link-events-date{color:var(--c-muted);font-size:12px}.product-show-images-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));gap:12px}.product-show-image-card{border:1px solid var(--c-border);border-radius:10px;background:#fff;padding:10px;overflow:hidden}.product-show-image-card__meta{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;min-width:0}.product-show-image-path{font-size:12px;min-width:0;overflow:hidden}.product-show-image-path summary{cursor:pointer;color:var(--c-muted, #888);list-style:none;user-select:none;white-space:nowrap}.product-show-image-path summary::-webkit-details-marker{display:none}.product-show-image-path summary::after{content:" ▾"}.product-show-image-path[open] summary::after{content:" ▴"}.product-show-image-path__url{margin-top:4px;word-break:break-all;overflow-wrap:break-word;font-size:11px}.product-show-image{width:100%;max-height:260px;object-fit:cover;border-radius:8px;border:1px solid #d9e0ea}.shipment-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px}.searchable-select{position:relative}.searchable-select__trigger{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none;min-height:34px}.searchable-select__trigger::after{content:"";width:0;height:0;border-left:4px solid rgba(0,0,0,0);border-right:4px solid rgba(0,0,0,0);border-top:5px solid var(--c-text-muted, #6b7280);margin-left:8px;flex-shrink:0}.searchable-select__trigger--placeholder{color:var(--c-text-muted, #6b7280)}.searchable-select__dropdown{display:none;position:absolute;left:0;right:0;top:100%;z-index:50;max-height:280px;overflow:auto;background:#fff;border:1px solid var(--c-border);border-top:0;border-radius:0 0 8px 8px;box-shadow:0 8px 20px rgba(15,23,42,.12)}.searchable-select__dropdown.is-open{display:block}.searchable-select__search{position:sticky;top:0;border:none !important;border-bottom:1px solid var(--c-border) !important;border-radius:0 !important;box-shadow:none !important;font-size:13px;background:#fff;z-index:1}.searchable-select__option{padding:7px 10px;font-size:13px;cursor:pointer;color:var(--c-text-strong)}.searchable-select__option:hover{background:#f1f5f9}.searchable-select__option.is-selected{background:#edf2ff;font-weight:600}.flash{padding:10px 14px;border-radius:8px;font-size:13px;font-weight:500}.flash--success{background:#f0fdf4;border:1px solid #bbf7d0;color:#166534}.flash--error{background:#fef2f2;border:1px solid #fecaca;color:#b91c1c}.content-tabs-card{margin-top:0}.content-tabs-nav{display:flex;gap:4px;border-bottom:2px solid var(--c-border);margin-bottom:16px;flex-wrap:wrap}.content-tab-btn{padding:8px 16px;border:none;background:none;cursor:pointer;font-size:14px;font-weight:500;color:var(--c-text-muted, #6b7280);border-bottom:2px solid rgba(0,0,0,0);margin-bottom:-2px;border-radius:4px 4px 0 0;transition:color .15s,border-color .15s}.content-tab-btn:hover{color:var(--c-text-strong, #111827)}.content-tab-btn.is-active{color:var(--c-primary, #2563eb);border-bottom-color:var(--c-primary, #2563eb)}.content-tab-panel{display:none}.content-tab-panel.is-active{display:block}.shoppro-tabs-toolbar{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-bottom:10px;flex-wrap:wrap}.shoppro-tabs-toolbar__field{margin:0;min-width:260px;max-width:420px;flex:1 1 320px}.shoppro-tabs-toolbar__field .form-control{width:100%}.shoppro-tabs-toolbar__actions{display:inline-flex;align-items:center;gap:8px}.dm-carrier-select{min-width:140px}.dm-service-wrap{min-width:200px}.dm-service-wrap .dm-inpost-panel .form-control,.dm-service-wrap .dm-apaczka-panel .form-control{width:100%}.integration-settings-group{grid-column:1/-1;border:1px solid var(--c-border);border-radius:10px;background:#f8fbff;padding:10px}.integration-settings-group__head{margin-bottom:8px;padding:4px 0;border-bottom:1px solid #e2e8f0}.integration-settings-group__title{margin:0;font-size:14px;font-weight:600;letter-spacing:.01em;color:var(--c-text-strong, #1e293b)}.integration-settings-group__desc{margin:4px 0 0;color:#4b5563}.integration-settings-group__grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:10px 12px;align-items:start}.integration-settings-group__full{grid-column:1/-1}.integration-settings-group__grid .form-field{margin:0;align-self:start}.integration-settings-group__grid .form-control{min-height:34px;height:34px}.integration-settings-group__grid input[type=date].form-control{line-height:1.2}.integration-settings-checkboxes{border:0;padding:0;margin:0}.integration-settings-checkboxes .field-label{display:block;margin-bottom:2px}.integration-settings-checkboxes__list{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:6px 12px}.integration-settings-checkboxes__item{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#334155}@media(max-width: 768px){.app-shell{flex-direction:column}.sidebar{width:100% !important;min-width:0 !important;border-right:0;border-bottom:1px solid #243041;padding:14px;overflow-x:auto}.sidebar__brand{margin:0 0 10px;font-size:22px}.sidebar__collapse-btn{display:none}.sidebar__nav{display:flex;gap:8px;overflow-x:auto}.sidebar__link{white-space:nowrap}.topbar{padding:0 14px}.container{margin-top:16px;width:calc(100% - 16px);margin-left:8px;margin-right:8px;padding:0 3px 12px}.settings-grid{grid-template-columns:1fr}.page-head{flex-direction:column;align-items:flex-start}.orders-stats{grid-template-columns:1fr;width:100%}.order-show-layout{grid-template-columns:1fr}.order-statuses-side{position:static;top:auto}.order-details-actions{justify-content:flex-start}.order-grid-2,.order-grid-3{grid-template-columns:1fr}.order-kv{grid-template-columns:1fr;gap:2px}.filters-grid,.form-grid,.form-grid-2,.form-grid-3,.form-grid-4,.shipment-grid,.statuses-form,.statuses-inline-form,.table-list-filters,.product-links-search-form,.product-links-inline-form{grid-template-columns:1fr}.statuses-dnd-item__content{display:block}.statuses-inline-delete{margin-top:6px}.filters-actions{align-items:center}.table-list__header,.table-list__footer{align-items:flex-start}.product-links-head{grid-template-columns:1fr}.integration-settings-group__grid{grid-template-columns:1fr}.integration-settings-checkboxes__list{grid-template-columns:1fr}.card{padding:12px}.modal--image-preview{width:min(92vw,100%)}.email-tpl-editor-wrap{flex-direction:column}.email-tpl-var-panel{min-width:200px}.modal-box{width:95vw;max-height:90vh}}.email-tpl-editor-wrap{display:flex;flex-direction:column;border:1px solid var(--c-border);border-radius:6px;overflow:visible}.email-tpl-toolbar{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--c-bg-subtle, #f8f9fa);border-bottom:1px solid var(--c-border)}.email-tpl-var-dropdown{position:relative}.email-tpl-var-panel{position:absolute;top:100%;left:0;z-index:300;min-width:260px;max-height:320px;overflow-y:auto;background:var(--c-bg);border:1px solid var(--c-border);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.12);padding:6px;margin-top:4px}.email-var-group:not(:first-child){margin-top:6px;padding-top:6px;border-top:1px solid var(--c-border)}.email-var-group__label{font-size:11px;font-weight:600;text-transform:uppercase;color:var(--c-text-muted);padding:2px 4px;letter-spacing:.03em}.email-var-item{display:block;width:100%;text-align:left;padding:3px 6px;margin:1px 0;border:none;background:none;font-size:12px;font-family:"Roboto Mono",monospace;color:var(--c-text);border-radius:3px;cursor:pointer}.email-var-item:hover{background:var(--c-primary);color:#fff}#js-quill-editor{min-height:200px}#js-quill-editor .ql-editor{min-height:200px;font-size:13px}.modal-overlay{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45)}.modal-box{width:min(680px,90vw);max-height:80vh;background:var(--c-bg);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.2);display:flex;flex-direction:column;overflow:hidden}.modal-box__header{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--c-border)}.modal-box__title{margin:0;font-size:15px;font-weight:600}.modal-box__close{background:none;border:none;font-size:22px;line-height:1;cursor:pointer;color:var(--c-text-muted);padding:0 4px}.modal-box__close:hover{color:var(--c-text)}.modal-box__body{padding:12px 16px;overflow-y:auto;flex:1} diff --git a/public/assets/js/modules/automation-form.js b/public/assets/js/modules/automation-form.js index 8228755..177be70 100644 --- a/public/assets/js/modules/automation-form.js +++ b/public/assets/js/modules/automation-form.js @@ -109,6 +109,20 @@ return html; } + function buildOrderStatusActionConfig(namePrefix) { + var html = ''; + return html; + } + function addCondition() { var idx = getNextIndex(conditionsContainer); var namePrefix = 'conditions[' + idx + ']'; @@ -144,6 +158,7 @@ + '' + '' + '' + + '' + '' + '
' + buildEmailActionConfig(namePrefix) @@ -190,6 +205,10 @@ } if (select.value === 'update_shipment_status') { configDiv.innerHTML = buildShipmentStatusActionConfig(namePrefix); + return; + } + if (select.value === 'update_order_status') { + configDiv.innerHTML = buildOrderStatusActionConfig(namePrefix); } } diff --git a/resources/lang/pl.php b/resources/lang/pl.php index bcf2255..e754fb3 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -757,7 +757,7 @@ return [ 'status_sync_direction' => 'Kierunek synchronizacji statusow', 'status_sync_direction_allegro_to_orderpro' => 'Allegro -> orderPRO', 'status_sync_direction_orderpro_to_allegro' => 'orderPRO -> Allegro', - 'status_sync_direction_hint' => 'Aktualnie aktywny jest kierunek Allegro -> orderPRO. Ustawienie orderPRO -> Allegro jest przygotowane pod kolejny etap.', + 'status_sync_direction_hint' => 'Wybierz kierunek synchronizacji statusow pomiedzy Allegro i orderPRO.', 'status_sync_interval_minutes' => 'Interwal synchronizacji statusow (minuty)', 'status_sync_interval_hint' => 'Zakres: 1-1440 minut. Ustawienie zostanie uzyte przez zadanie synchronizacji statusow.', 'save' => 'Zapisz ustawienia', diff --git a/resources/scss/app.scss b/resources/scss/app.scss index 7da52c8..09a22ae 100644 --- a/resources/scss/app.scss +++ b/resources/scss/app.scss @@ -2404,7 +2404,7 @@ h4.section-title { flex-direction: column; border: 1px solid var(--c-border); border-radius: 6px; - overflow: hidden; + overflow: visible; } .email-tpl-toolbar { @@ -2424,7 +2424,7 @@ h4.section-title { position: absolute; top: 100%; left: 0; - z-index: 50; + z-index: 300; min-width: 260px; max-height: 320px; overflow-y: auto; diff --git a/resources/scss/modules/_automation.scss b/resources/scss/modules/_automation.scss index c88fe4b..132dda0 100644 --- a/resources/scss/modules/_automation.scss +++ b/resources/scss/modules/_automation.scss @@ -63,3 +63,39 @@ margin: 0; } } + +.automation-actions-cell { + white-space: nowrap; +} + +.automation-inline-form { + display: inline; +} + +.automation-history-filters { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 8px; + align-items: end; + + .form-field { + margin: 0; + } + + .field-label { + font-size: 12px; + margin-bottom: 4px; + } + + .form-control { + min-height: 34px; + } + + &__actions { + display: flex; + gap: 6px; + align-items: center; + justify-content: flex-start; + padding-bottom: 1px; + } +} diff --git a/resources/views/automation/form.php b/resources/views/automation/form.php index 0ef062e..fc44e1b 100644 --- a/resources/views/automation/form.php +++ b/resources/views/automation/form.php @@ -30,6 +30,7 @@ $receiptDuplicatePolicyLabels = [ 'allow_duplicates' => 'Wystawiaj kolejne paragony', ]; $shipmentStatusOptions = is_array($shipmentStatusOptions ?? null) ? $shipmentStatusOptions : []; +$orderStatusOptions = is_array($orderStatusOptions ?? null) ? $orderStatusOptions : []; ?>
@@ -127,6 +128,7 @@ $shipmentStatusOptions = is_array($shipmentStatusOptions ?? null) ? $shipmentSta +
+ + + + + +
+ + + +
+
+ + + +
+ + + + + +
+ +
+ +
+
+ + + + + + + + + + + + + + +
+ + Wyczysc +
+
+
- + - - + + - + - + - - - - - - + - + + + + + + + + + + + + +
NazwaKiedy ZdarzenieWarunkowAkcjiRegulaZamowienie StatusAkcjeWynik
- - Aktywne - - Nieaktywne - - - Edytuj -
- - - -
-
- - - -
-
- - - -
-
Brak wpisow historii dla wybranych filtrow.
+ 0): ?> + # + + - + + + + Sukces + + Blad + + + +
- + + 1): ?> + + +