diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index 114f8fc..ccdc56a 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -12,8 +12,8 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów | Attribute | Value | |-----------|-------| -| Version | 3.1.0 | -| Status | v3.1 shipped — all phases complete | +| Version | 3.2.0 | +| Status | v3.2 shipped — Delivery Status Management complete | | Last Updated | 2026-04-27 | ## Requirements @@ -111,6 +111,7 @@ Sprzedawca moĹĽe obsĹ‚ugiwać zamĂłwienia ze wszystkich kanałów - [x] Wersja mobilna — modul po module (v3.0) — shipped across phases 52–105 - [x] Alert o kliencie z historia zwrotow: badge w liscie zamowien (kolumna buyer) + czerwony banner u gory szczegolow zamowienia; matching OR po email/phone/name; `
` z lista zwroconych zamowien — Phase 106 - [x] Idempotentna jednorazowa wysylka e-mail per zamowienie: tabela deduplikacji `automation_email_once_deliveries` (UNIQUE KEY rule_id+action_id+order_id), checkbox "Wyslij tylko raz" w konfiguracji akcji, markSent() tylko po sukcesie — Phase 107 +- [x] Delivery Status Management: tabela `delivery_statuses` z CRUD panelem `/settings/delivery-statuses`, `DeliveryStatus::setRepository()` z DB fallbackiem, integracja DB-driven w dropdownach automatyzacji (warunek shipment_status + akcja update_shipment_status), osobna podstrona formularza CRUD (BREAKING: drop backward compat dla starych grupowych kluczy automatyzacji) — Phase 108 ### Deferred @@ -193,6 +194,9 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API | Statistics netto fallback `ROUND(gross / 1.23, 2)` gdy `total_without_tax` puste | shopPRO nie wysyla netto ani w zamowieniu ani w `order_items`; tymczasowy fallback — docelowy fix w `.paul/TODO.md` (STAT-NET) | 2026-04-19 | Active | | ON DUPLICATE KEY UPDATE created_at = created_at dla idempotentnego markSent() | Unikniece silent failure i race condition przy rownolegych cronach; thread-safe bez wyjatkow | 2026-04-25 | Active | | send_once_per_order opt-in przez checkbox (domyslnie off) | Wsteczna zgodnosc — istniejace reguly nie zmieniaja zachowania; markSent() tylko po sukcesie wysylki | 2026-04-25 | Active | +| DeliveryStatus::setRepository() pattern: DB fallback dla static final class | Operator dodaje status w UI bez zmian kodu; `getAllOptions()`/`label()`/`getColor()` ladują z DB gdy repo ustawione, fallback na hardcoded ALL_STATUSES/LABEL_PL | 2026-04-27 | Active | +| Drop backward compat dla starych grupowych kluczy automatyzacji (Phase 108-02) | Kolizja semantyczna: stary `picked_up` mapował na `delivered`, nowy klucz DB `picked_up` to "Odebrana przez kuriera" — odwrotne końce cyklu. Hybrid evaluation by silently dawała wrong matches | 2026-04-27 | Active | +| Path params w controllerach via `$request->input('id')` (nie jako argumenty metody) | Konwencja routera projektu: handler wywoływany z jednym argumentem `$request`, params siedzą jako attributes — `ReceiptController::show()` jako wzorzec | 2026-04-27 | Active | ## Success Metrics @@ -224,6 +228,6 @@ Quick Reference: --- *PROJECT.md — Updated when requirements or context change* -*Last updated: 2026-04-27 after v3.1 Operational Enhancements milestone completion (Phase 107 Automation Email Send Once)* +*Last updated: 2026-04-27 after v3.2 Delivery Status Management milestone completion (Phase 108)* diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 3ccbdd5..6e82873 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -6,7 +6,7 @@ orderPRO to narzedzie do wielokanalowego zarzadzania sprzedaza. Projekt przechod ## Current Milestone -Brak aktywnego milestone — v3.1 zamkniety. Nastepny milestone do zaplanowania. +Brak aktywnego milestone — v3.2 zamkniety. Nastepny milestone do zaplanowania. ## Next Milestone @@ -19,6 +19,19 @@ Kandydaci w kolejce: ## Completed Milestones +
+v3.2 Delivery Status Management - 2026-04-27 (1 phase, 2 plans) + +Wyniesienie znormalizowanych statusow przesylek do tabeli DB z CRUD panelem oraz pelna integracja DB-driven w dropdownach automatyzacji. + +| Phase | Name | Plans | Status | +|-------|------|-------|--------| +| 108 | Delivery Status Management | 2/2 | Complete | + +Archive: `.paul/phases/108-delivery-status-management/` + +
+
v3.1 Operational Enhancements - 2026-04-27 (2 phases, 2 plans) @@ -415,4 +428,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md` --- *Roadmap created: 2026-03-12* -*Last updated: 2026-04-19 - v3.0 Mobile Responsive milestone closed (52 phases shipped, 55 plans; phase 68 deferred, phase 99 cancelled)* +*Last updated: 2026-04-27 - v3.2 Delivery Status Management milestone closed (Phase 108, 2 plans)* diff --git a/.paul/STATE.md b/.paul/STATE.md index 7cf2b8d..e561cea 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -5,47 +5,55 @@ See: .paul/PROJECT.md (updated 2026-04-27) **Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami. -**Current focus:** v3.1 Operational Enhancements — COMPLETE. Oczekiwanie na kolejny milestone. +**Current focus:** Brak aktywnego milestone — v3.2 zamkniety ## Current Position -Milestone: v3.1 Operational Enhancements — COMPLETE -Phase: 107 of 107 (Automation Email Send Once) — Complete -Plan: 107-01 — Unified -Version: 3.1.0 -Status: Milestone zamkniety, gotowy do planowania kolejnego milestone +Milestone: v3.2 — COMPLETE (Delivery Status Management) +Phase: 108 of 108 — COMPLETE +Plan: 108-01 — COMPLETE / 108-02 — COMPLETE +Version: 3.2.0 +Status: v3.2 shipped — gotowy do nastepnego milestone -Last activity: 2026-04-27 — UNIFY phase 107, milestone v3.1 closed +Last activity: 2026-04-27 — TRANSITION Phase 108 / v3.2 milestone complete Progress: -- Milestone: [##########] 100% -- Phase 107: [##########] 100% +- Milestone v3.2: [##########] 100% (1/1 phases, 2/2 plans) ## Loop Position Current loop state: ``` -PLAN --> APPLY --> UNIFY - [x] [x] [x] [Loop complete — milestone complete] +v3.2 milestone: + Phase 108 (Delivery Status Management): + Plan 108-01: PLAN ✓ APPLY ✓ UNIFY ✓ + Plan 108-02: PLAN ✓ APPLY ✓ UNIFY ✓ + → Phase 108 closed +→ v3.2 milestone closed ``` ## Session Continuity Last session: 2026-04-27 -Stopped at: Milestone v3.1 complete — wszystkie fazy zamkniete -Next action: /paul:milestone — zaplanuj kolejny milestone (v3.2) +Stopped at: v3.2 milestone closed +Next action: /paul:milestone — wybor i zaplanowanie nastepnego milestone Resume file: .paul/ROADMAP.md +## Pending Actions + +- Uruchom migracje gdy XAMPP online: `php bin/migrate.php` (delivery_statuses) +- Recznie odtworzyc istniejace reguly automatyzacji z grupowymi kluczami (BREAKING z 108-02) + ## Deferred to Next Milestones - Phase 68 - Code Deduplication Refactor (0/2 Planning, nigdy nie rozpoczety) - STAT-NET - netto shopPRO z API lub z `order_items.tax_rate` (`.paul/TODO.md`) - Mobile Orders List / Mobile Order Details / Mobile Settings (TBD z poprzedniego roadmapu) -- sonar-scanner - skan dla phase 105, 106, 107 nie zostal uruchomiony (skill gap odnotowany) +- sonar-scanner - skan dla phase 105, 106, 107, 108 nie zostal uruchomiony (skill gap odnotowany) - INDEX-106-01 - indeksy DB dla query `customer_returned_count`: `order_addresses(order_id, address_type)`, `shipment_packages(order_id, delivery_status)` (gdy dataset >50k wierszy) -## Skill Audit (Phase 107) +## Skill Audit (Phase 108) | Expected | Invoked | Notes | |----------|---------|-------| -| sonar-scanner (required) | o | Nie uruchomiony — odlozony analogicznie do Phase 105/106 | +| sonar-scanner (required) | o | Wymagany po APPLY 108-01 i 108-02 — odlozony | diff --git a/.paul/changelog/2026-04-27.md b/.paul/changelog/2026-04-27.md new file mode 100644 index 0000000..9c9a1b7 --- /dev/null +++ b/.paul/changelog/2026-04-27.md @@ -0,0 +1,43 @@ +# 2026-04-27 + +## Co zrobiono + +- [Phase 108, Plan 01] Wyniesiono znormalizowane statusy przesyłek do tabeli DB z CRUD panelem i dynamicznym ładowaniem +- Migracja `20260427_000103_create_delivery_statuses_table.sql` — tabela delivery_statuses z seedem 11 statusów systemowych +- Nowy `DeliveryStatusRepository` — CRUD + per-request static cache, blokady systemowych i używanych statusów +- `DeliveryStatus::setRepository()` — transparent DB loading: label(), getAllOptions(), getColor() z DB, fallback na stałe +- Nowy panel `/settings/delivery-statuses` z zakładkami: Statusy (CRUD niebędacych systemowych) + Mapowanie dostawy (embedded) +- Sidebar przebudowany: "Statusy" → "Statusy zamówień", nowa pozycja "Statusy przesyłek" z badge niezmapowanych; usunięto osobną pozycję "Mapowanie statusów dostawy" +- Badge przesyłek: system statuses → CSS class; custom statuses → `.delivery-badge--custom` + CSS custom property `--status-color` + +- [Phase 108, Plan 02] Domknięcie integracji DB-driven statusów dla automatyzacji + refaktor UI listy +- `AutomationController` — usunięto stałą `SHIPMENT_STATUS_OPTIONS`; dropdown z `DeliveryStatus::getAllOptions()` +- `AutomationService` — usunięto `SHIPMENT_STATUS_OPTION_MAP`; ewaluacja porównuje klucze bezpośrednio (BREAKING dla starych reguł) +- Walidacja shipment_status / update_shipment_status przez `DeliveryStatus::getAllStatuses()` +- Nowa podstrona `/settings/delivery-statuses/new` i `/{id}/edit` — osobny formularz CRUD zamiast inline edit row +- Lista statusów: rename "Terminal" → "Końcowy", usunięta kolumna "Typ" (badge systemowy) +- Bug fix: path params w `DeliveryStatusesController::edit/update/destroy` przez `$request->input('id')` (pre-existing z Plan 01) + +## Zmienione pliki + +- `database/migrations/20260427_000103_create_delivery_statuses_table.sql` +- `src/Modules/Shipments/DeliveryStatusRepository.php` +- `src/Modules/Settings/DeliveryStatusesController.php` +- `resources/views/settings/delivery-statuses.php` +- `resources/views/settings/_delivery-status-mappings-content.php` +- `src/Modules/Shipments/DeliveryStatus.php` +- `src/Modules/Settings/DeliveryStatusMappingController.php` +- `resources/views/settings/delivery-status-mappings.php` +- `resources/views/layouts/app.php` +- `resources/scss/modules/_delivery-status.scss` +- `public/assets/css/app.css` +- `resources/lang/pl.php` +- `resources/views/orders/show.php` +- `resources/views/shipments/prepare.php` +- `.paul/docs/DB_SCHEMA.md` +- `.paul/docs/ARCHITECTURE.md` +- `.paul/docs/TECH_CHANGELOG.md` +- `src/Modules/Automation/AutomationController.php` +- `src/Modules/Automation/AutomationService.php` +- `routes/web.php` +- `resources/views/settings/delivery-status-form.php` diff --git a/.paul/docs/ARCHITECTURE.md b/.paul/docs/ARCHITECTURE.md index e76060e..ff596ba 100644 --- a/.paul/docs/ARCHITECTURE.md +++ b/.paul/docs/ARCHITECTURE.md @@ -1,3 +1,28 @@ # ARCHITECTURE > Struktura klas, modulow, przeplywow i zaleznosci w projekcie. + +## Phase 108 — Delivery Status Management + +### DeliveryStatusRepository (`src/Modules/Shipments/DeliveryStatusRepository.php`) +- CRUD dla tabeli `delivery_statuses` +- Per-request static cache (`private static ?array $cache`) +- Blokuje edycję/usunięcie statusów systemowych (`is_system=1`) +- Blokuje usunięcie statusów używanych w `delivery_status_mappings` lub `shipment_packages` + +### DeliveryStatusesController (`src/Modules/Settings/DeliveryStatusesController.php`) +- Panel `/settings/delivery-statuses` +- Dwie zakładki via `?tab=` param: `statuses` (CRUD) i `mapping` (embed mapowania) +- Wstrzykuje `DeliveryStatusRepository` i `DeliveryStatusMappingRepository` + +### DeliveryStatus::setRepository() (dynamic loading) +- Wywoływane raz w `routes/web.php` po bootstrap +- `label()`, `getAllOptions()`, `getAllStatuses()`, `getColor()` ladują z DB gdy repo ustawione +- Fallback na hardcoded stałe gdy repo niedostępne + +### AutomationController + AutomationService (Phase 108 Plan 02) +- `AutomationController::buildShipmentStatusOptions()` — buduje listę opcji `[key => ['label' => ...]]` z `DeliveryStatus::getAllOptions()` (DB-driven) +- Walidacja `shipment_status` warunku i `update_shipment_status` akcji w `parseConditionValue()`/`parseActionConfig()` używa `DeliveryStatus::getAllStatuses()` +- `AutomationService::evaluateShipmentStatusCondition()` — bezpośrednie porównanie kluczy DB (usunięto mapping grupowy `SHIPMENT_STATUS_OPTION_MAP`) +- `AutomationService::resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target +- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`) nie matchują się — operator musi je odtworzyć przy użyciu nowych kluczy DB diff --git a/.paul/docs/DB_SCHEMA.md b/.paul/docs/DB_SCHEMA.md index 737ad36..2b51a4f 100644 --- a/.paul/docs/DB_SCHEMA.md +++ b/.paul/docs/DB_SCHEMA.md @@ -1,3 +1,21 @@ # DB_SCHEMA > Schemat bazy danych — tabele, kolumny, FK, indeksy. + +## delivery_statuses + +Tabela znormalizowanych statusów przesyłek. Zastępuje hardcoded stałe z `DeliveryStatus.php` (od Phase 108). + +| Kolumna | Typ | Opis | +|---------|-----|------| +| id | INT UNSIGNED PK | Auto-increment | +| key | VARCHAR(50) UNIQUE | Klucz statusu (np. `in_transit`) | +| label_pl | VARCHAR(100) | Etykieta po polsku | +| color | VARCHAR(7) | Kolor tła w formacie hex (`#rrggbb`) | +| sort_order | TINYINT UNSIGNED | Kolejność sortowania | +| is_terminal | TINYINT(1) | 1 = status końcowy (nie można cofnąć) | +| is_system | TINYINT(1) | 1 = status systemowy (nieedytowalny z UI) | +| created_at | DATETIME | Data utworzenia | + +**Statusy systemowe** (is_system=1, is_terminal=1): `delivered`, `returned`, `cancelled`. +**Migracja**: `20260427_000103_create_delivery_statuses_table.sql` diff --git a/.paul/docs/TECH_CHANGELOG.md b/.paul/docs/TECH_CHANGELOG.md index 861d346..975277f 100644 --- a/.paul/docs/TECH_CHANGELOG.md +++ b/.paul/docs/TECH_CHANGELOG.md @@ -1,3 +1,31 @@ # TECH_CHANGELOG > Chronologiczny log zmian technicznych — co i dlaczego. + +## 2026-04-27 — Phase 108 Plan 02: Automation Dropdowns z DB + +**Co zrobiono:** +- `AutomationController` — usunięto stałą `SHIPMENT_STATUS_OPTIONS` (8 grupowych kluczy) +- Dropdown statusów w warunku `shipment_status` i akcji `update_shipment_status` ładuje statusy z DB przez `DeliveryStatus::getAllOptions()` +- Walidacja w `parseConditionValue()` i `parseActionConfig()` używa `DeliveryStatus::getAllStatuses()` +- `AutomationService` — usunięto stałą `SHIPMENT_STATUS_OPTION_MAP`; ewaluacja `evaluateShipmentStatusCondition()` porównuje klucze bezpośrednio +- `resolveStatusFromActionKey()` — bezpośredni klucz statusu z DB jako target (zamiast pierwszego z grupy) + +**Dlaczego:** +- Zamknięcie integracji z Plan 01 — operator dodaje status w `/settings/delivery-statuses` i jest on od razu dostępny w dropdownach automatyzacji bez deploymentu +- Eliminacja kolizji semantycznej: stary klucz grupowy `picked_up` mapował na `delivered` (paczka odebrana przez klienta), nowy klucz DB `picked_up` to "Odebrana przez kuriera" (od nadawcy) +- BREAKING: stare reguły z grupowymi kluczami (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`, oraz `picked_up`/`ready_for_pickup`/`cancelled` w starym znaczeniu) nie matchują — wymagają ręcznego odtworzenia z nowymi kluczami DB + +## 2026-04-27 — Phase 108 Plan 01: Delivery Status Management + +**Co zrobiono:** +- Tabela `delivery_statuses` z seedem 11 statusów (migracja `20260427_000103`) +- `DeliveryStatusRepository` — CRUD + per-request cache +- `DeliveryStatus.php` — dynamiczne ładowanie statusów z DB (`setRepository()`) +- Panel `/settings/delivery-statuses` z CRUD (zakładka "Statusy") i mapowaniem (zakładka "Mapowanie dostawy") +- Sidebar: "Statusy" → "Statusy zamówień", nowe "Statusy przesyłek" z badge niezmapowanych +- Badge przesyłek: inline CSS custom property `--status-color` dla niestandardowych statusów + +**Dlaczego:** +- Dodanie nowego statusu wymagało zmiany kodu + deploymentu; teraz z UI +- Operator może definiować własne statusy znormalizowane bez ingerencji w kod diff --git a/.paul/governance/governance_2026-04-27.jsonl b/.paul/governance/governance_2026-04-27.jsonl index ae33bea..571d4b5 100644 --- a/.paul/governance/governance_2026-04-27.jsonl +++ b/.paul/governance/governance_2026-04-27.jsonl @@ -89,3 +89,101 @@ {"ts":"2026-04-27T12:55:15Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && python tools/generowanie/email_message_fetcher.py --email \"kozix_x@onet.pl\" --days 60 --json 2>/dev/null | head -50\",\"description\":\"Sprawdź mai","cwd":"/c/visual studio code/projekty/orderPRO"} {"ts":"2026-04-27T12:55:46Z","tool":"Bash","cmd":"cd \"C:/visual studio code/projekty/orderPRO\" && PYTHONIOENCODING=utf-8 python tools/generowanie/pudelko_komunia_dziewczynka.py \\\\\\n --klient \"Natalia Barczyk\" \\\\\\n --imie \"Oliwii\" \\\\\\","cwd":"/c/visual studio code/projekty/orderPRO"} {"ts":"2026-04-27T12:55:57Z","tool":"Bash","cmd":"\"C:/xampp/mysql/bin/mysql.exe\" --default-character-set=utf8mb4 -h host700513.hostido.net.pl -P 3306 -u host700513_orderpro -phrDNtUBg9grwZ7syN77S host700513_orderpro -e \"UPDATE order_items SET proj","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:10:36Z","tool":"Bash","cmd":"git log --oneline -10\",\"description\":\"Recent commits\"},\"tool_response\":{\"stdout\":\"5aca417 update\\n32d7ae9 update\\n34dac15 update\\nf5c5201 chore: move TODO to .paul/codebase/todo.md\\nc3b6","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:10:45Z","tool":"Bash","cmd":"git show --stat 5aca417 && git show --stat 32d7ae9 && git show --stat 34dac15\",\"description\":\"Show changed files in last 3 commits\"},\"tool_response\":{\"stdout\":\"commit 5aca41750ca5b34c5b6d816","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:16:17Z","tool":"Bash","cmd":"git log --oneline --since=\"2026-04-25\" | head -20\",\"description\":\"Commits since plan creation\"},\"tool_response\":{\"stdout\":\"5aca417 update\\n32d7ae9 update\\n34dac15 update\\nf5c5201 chore:","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:16:22Z","tool":"Bash","cmd":"git log --all --oneline --since=\"2026-04-24\" --until=\"2026-04-26\" -- \"src/Modules/Automation/*\" \"database/migrations/*\" \"tests/Unit/*\" | head -20\",\"description\":\"Commits touching phase 1","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:16:28Z","tool":"Bash","cmd":"git show --stat 4b998ea\",\"description\":\"Show files in the apply commit\"},\"tool_response\":{\"stdout\":\"commit 4b998ea5be917df1aad90bba87da50f800b46fe4\\nAuthor: Jacek Pyziak &1\",\"description\":\"Build SCSS assets\"},\"tool_response\":{\"stdout\":\"> orderpro@1.0.0 build:assets\\n> npm run build:css","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:47:33Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\108-delivery-status-management\\\\108-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:47:49Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-27.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:55:47Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:55:53Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:55:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:56:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:56:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:56:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:56:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationService.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:56:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationService.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:56:35Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Automation\\\\AutomationService.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:56:39Z","tool":"Bash","cmd":"php -l \"src/Modules/Automation/AutomationController.php\" && php -l \"src/Modules/Automation/AutomationService.php\"\",\"description\":\"PHP syntax check on modified files\"},\"tool_response\":{\"std","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:57:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\docs\\\\ARCHITECTURE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:57:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\docs\\\\TECH_CHANGELOG.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:57:30Z","tool":"Bash","cmd":"php vendor/bin/phpunit tests/Unit/AutomationServiceTest.php 2>&1 | tail -30\",\"description\":\"Run automation service tests\"},\"tool_response\":{\"stdout\":\"PHPUnit 11.5.55 by Sebastian Bergmann an","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:57:38Z","tool":"Bash","cmd":"php vendor/bin/phpunit 2>&1 | tail -15\",\"description\":\"Run full PHPUnit suite\"},\"tool_response\":{\"stdout\":\"Failed asserting that Array &0 [\\n 'status' => 'created',\\n 'shipment_id' =","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:57:45Z","tool":"Bash","cmd":"git stash && php vendor/bin/phpunit tests/Unit/AllegroShipmentServiceTest.php 2>&1 | tail -5 && git stash pop\",\"description\":\"Check if AllegroShipmentService failures are pre-existing\"},\"tool_re","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T19:58:04Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:00:42Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\routes\\\\web.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:00:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\DeliveryStatusesController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:00:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\DeliveryStatusesController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:01:13Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\settings\\\\delivery-status-form.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:01:47Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\resources\\\\views\\\\settings\\\\delivery-statuses.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:01:53Z","tool":"Bash","cmd":"php -l \"src/Modules/Settings/DeliveryStatusesController.php\" && php -l \"resources/views/settings/delivery-statuses.php\" && php -l \"resources/views/settings/delivery-status-form.php\" && php -l \"","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:04:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\DeliveryStatusesController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:04:08Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\DeliveryStatusesController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:04:13Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\src\\\\Modules\\\\Settings\\\\DeliveryStatusesController.php","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:04:17Z","tool":"Bash","cmd":"php -l \"src/Modules/Settings/DeliveryStatusesController.php\"\",\"description\":\"Syntax check controller\"},\"tool_response\":{\"stdout\":\"No syntax errors detected in src/Modules/Settings/Delivery","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:05:48Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\phases\\\\108-delivery-status-management\\\\108-02-SUMMARY.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:05:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-27.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:06:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\changelog\\\\2026-04-27.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:06:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:08:36Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:08:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:08:59Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:09:03Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:09:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:09:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:09:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:09:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\orderPRO\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/orderPRO"} +{"ts":"2026-04-27T20:09:50Z","tool":"Bash","cmd":"git branch --list \"feature/108*\" && git status --short | head -30\",\"description\":\"Check feature branches and git status\"},\"tool_response\":{\"stdout\":\" M .paul/PROJECT.md\\n M .paul/ROADMAP.","cwd":"/c/visual studio code/projekty/orderPRO"} diff --git a/.paul/phases/108-delivery-status-management/108-01-PLAN.md b/.paul/phases/108-delivery-status-management/108-01-PLAN.md new file mode 100644 index 0000000..215341b --- /dev/null +++ b/.paul/phases/108-delivery-status-management/108-01-PLAN.md @@ -0,0 +1,326 @@ +--- +phase: 108-delivery-status-management +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - database/migrations/20260427_000103_create_delivery_statuses_table.sql + - src/Modules/Shipments/DeliveryStatusRepository.php + - src/Modules/Shipments/DeliveryStatus.php + - src/Modules/Settings/DeliveryStatusesController.php + - src/Modules/Settings/DeliveryStatusMappingController.php + - resources/views/settings/delivery-statuses.php + - resources/views/settings/_delivery-status-mappings-content.php + - resources/views/settings/delivery-status-mappings.php + - resources/views/layouts/app.php + - resources/scss/settings/_delivery-statuses.scss + - resources/scss/app.scss + - resources/lang/pl.php + - routes/web.php + - .paul/docs/DB_SCHEMA.md + - .paul/docs/ARCHITECTURE.md + - .paul/docs/TECH_CHANGELOG.md +autonomous: true +delegation: on +--- + + +## Goal +Wyniesc statusy znormalizowane przesylek do tabeli DB, udostepnic CRUD w nowym panelu ustawien "Statusy przesylek" i dynamicznie ladowac statusy z DB wszedzie tam, gdzie wczesniej uzywano stalych z `DeliveryStatus.php`. + +## Purpose +Dodanie nowego statusu znormalizowanego wymaga teraz zmiany kodu i deploymentu. Po tej fazie operator moze dodac wlasny status z UI bez ingerencji w kod. Statusy systemowe (delivered, returned, cancelled) pozostaja nieedytowalne. + +## Output +- Tabela `delivery_statuses` z seedem 11 istniejacych statusow +- `DeliveryStatusRepository` — odczyt z DB z per-request static cache +- `DeliveryStatus.php` — laduje `ALL_STATUSES` i `LABEL_PL` z DB (fallback na stale przy bledzie) +- Nowy panel `/settings/delivery-statuses` z dwoma zakladkami: CRUD statusow + mapowanie dostawy +- Sidebar: "Statusy zamowien" (istniejaca pozycja), nowa pozycja "Statusy przesylek" +- Walidacja w `DeliveryStatusMappingController` uzywajaca danych z DB + + + +## Project Context +@.paul/PROJECT.md +@.paul/STATE.md + +## Source Files +@src/Modules/Shipments/DeliveryStatus.php +@src/Modules/Settings/DeliveryStatusMappingController.php +@resources/views/settings/delivery-status-mappings.php +@resources/views/layouts/app.php +@resources/lang/pl.php +@routes/web.php +@.paul/docs/DB_SCHEMA.md +@.paul/docs/ARCHITECTURE.md + + + +## Required Skills + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner (CLI) | required | Po APPLY, przed UNIFY | o | + + + + +## AC-1: Tabela delivery_statuses istnieje i zawiera seed 11 statusow +```gherkin +Given migracja zostala uruchomiona +Then tabela delivery_statuses zawiera 11 wierszy odpowiadajacych stalym z DeliveryStatus.php +And kolumny: id, key, label_pl, color, sort_order, is_terminal, is_system, created_at +And statusy: delivered, returned, cancelled maja is_system=1, is_terminal=1 +And pozostale 8 statusow maja is_system=0 +``` + +## AC-2: CRUD niebedacych systemowymi statusami dziala z panelu +```gherkin +Given operator otwiera /settings/delivery-statuses (zakladka Statusy) +When doda nowy status z kluczem, etykieta, kolorem +Then status pojawia sie na liscie i w dropdownie mapowania statusow +When probuje edytowac status systemowy (delivered/returned/cancelled) +Then formularz jest zablokowany (readonly/disabled fields lub brak przycisku edycji) +When probuje usunac status uzyty w delivery_status_mappings lub shipment_packages +Then otrzymuje blad "Status jest uzywany, nie mozna usunac" +``` + +## AC-3: Strona /settings/delivery-statuses ma dwie zakladki +```gherkin +Given operator otwiera /settings/delivery-statuses +Then widzi zakladke "Statusy" (CRUD) i "Mapowanie dostawy" +And klikajac "Mapowanie dostawy" widzi te sama tresc co wczesniej na /settings/delivery-status-mappings +And /settings/delivery-status-mappings dalej dziala (backward compat dla zakładek) +``` + +## AC-4: Sidebar odzwierciedla nowa strukture nawigacji +```gherkin +Given operator jest w ustawieniach +Then widzi "Statusy zamowien" (link do /settings/statuses) +And widzi "Statusy przesylek" (link do /settings/delivery-statuses) +And stara pozycja "Mapowanie statusow dostawy" znika z sidebara +And badge z liczba niezmapowanych statusow widnieje przy "Statusy przesylek" +``` + +## AC-5: DeliveryStatus.php laduje statusy z DB +```gherkin +Given tabela delivery_statuses istnieje +When kod wywola DeliveryStatus::label($key) +Then zwraca etykiete z DB (nie hardcoded) +And kolejne wywolania w tym samym request uzywaja per-request static cache +``` + +## AC-6: Badge renderuje sie dla nowych statusow +```gherkin +Given status niebedacy jednym z 11 systemowych ma kolor #ff5500 +When badge jest renderowany w liscie zamowien lub szczegolow przesylki +Then badge uzywa inline style="background-color: #ff5500" dla nieznanych kluczy CSS +And istniejace 11 statusow dalej korzysta z hardcoded klas CSS (.delivery-badge--delivered itp.) +``` + + + + + + + Task 1: Migracja tabeli delivery_statuses i DeliveryStatusRepository + database/migrations/20260427_000103_create_delivery_statuses_table.sql, src/Modules/Shipments/DeliveryStatusRepository.php + + 1. Utworzyc migracje `20260427_000103_create_delivery_statuses_table.sql`: + - CREATE TABLE `delivery_statuses` ( + `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `key` VARCHAR(50) NOT NULL UNIQUE, + `label_pl` VARCHAR(100) NOT NULL, + `color` VARCHAR(7) NOT NULL DEFAULT '#6c757d', + `sort_order` TINYINT UNSIGNED NOT NULL DEFAULT 0, + `is_terminal` TINYINT(1) NOT NULL DEFAULT 0, + `is_system` TINYINT(1) NOT NULL DEFAULT 0, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + - INSERT seed: wszystkie 11 statusow z DeliveryStatus.php (unknown, created, confirmed, + picked_up, in_transit, out_for_delivery, ready_for_pickup, delivered, returned, + cancelled, problem) z odpowiednimi wartosciami label_pl, color, sort_order, + is_terminal (delivered/returned/cancelled = 1), is_system (delivered/returned/cancelled = 1). + - Kolory bazujac na istniejacych klasach CSS (sprawdzic resources/scss/). + 2. Utworzyc `src/Modules/Shipments/DeliveryStatusRepository.php`: + - Konstruktor przyjmuje `\Medoo\Medoo $db` + - `getAll(): array` — SELECT * FROM delivery_statuses ORDER BY sort_order ASC + z per-request static cache (prywatna static zmienna) + - `getByKey(string $key): ?array` — szuka w getAll() + - `getAllAsOptions(): array` — zwraca [key => label_pl] (do dropdownow) + - `create(array $data): int` — INSERT, zwraca id; waliduje unikalnosc key przed insertem + - `update(int $id, array $data): void` — UPDATE; blokuje is_system=1 + - `delete(int $id): void` — DELETE; sprawdza czy key nie wystepuje w + delivery_status_mappings.normalized_status lub shipment_packages.delivery_status; + blokuje jesli is_system=1 lub uzywany + - `clearCache(): void` — resetuje static cache (do testow i po mutacjach) + + + Uruchom migracje na lokalnej bazie. Sprawdz `SELECT COUNT(*) FROM delivery_statuses` = 11. + Sprawdz ze delivered/returned/cancelled maja is_system=1, is_terminal=1. + + Dane bazodanowe gotowe; repozytorium dostepne do uzycia w pozostalych taskach. + + + + Task 2: Dynamiczne ladowanie w DeliveryStatus.php + controller + widok CRUD + routing + src/Modules/Shipments/DeliveryStatus.php, src/Modules/Settings/DeliveryStatusesController.php, src/Modules/Settings/DeliveryStatusMappingController.php, resources/views/settings/delivery-statuses.php, resources/views/settings/_delivery-status-mappings-content.php, resources/views/settings/delivery-status-mappings.php, resources/scss/settings/_delivery-statuses.scss, resources/scss/app.scss, routes/web.php + + 1. Zaktualizowac `DeliveryStatus.php`: + - Dodac statyczna metode `setRepository(DeliveryStatusRepository $repo): void` (called once at bootstrap) + - Zmodyfikowac `label(string $key): string` — gdy repozytorium jest wstrzykniete, + pobiera z DB przez `getByKey($key)['label_pl']`; fallback na `LABEL_PL[$key] ?? $key` + - Dodac `getAllStatuses(): array` — zwraca `DeliveryStatusRepository::getAll()` lub + fallback `ALL_STATUSES` gdy repo niedostepne + - Dodac `getAllOptions(): array` — zwraca `DeliveryStatusRepository::getAllAsOptions()` lub + fallback `LABEL_PL` + - STALE i TERMINAL_STATUSES/providerowe mapy (INPOST_MAP etc.) pozostaja hardcoded (nie + dotyczace listy statusow uzytkownika) + - Podpiac `DeliveryStatusRepository` w bootstrapie (app.php lub DI kontener projektu — + sprawdzic jak inne repozytoria sa inicjalizowane i powtorzyc ten sam wzorzec) + 2. Zaktualizowac `DeliveryStatusMappingController`: + - Wstrzyknac `DeliveryStatusRepository` przez konstruktor + - W `save()` i `saveBulk()` — walidacja normalizedStatus przez + `$this->deliveryStatusRepository->getByKey($normalizedStatus) !== null` + zamiast `in_array($normalizedStatus, DeliveryStatus::ALL_STATUSES)` + - Zaktualizowac `REDIRECT_PATH` z `/settings/delivery-status-mappings` + na `/settings/delivery-statuses?tab=mapping` + - Zachowac parametr `?provider=` przy redirectach gdzie byl + 3. Wydzielic zawartosc widoku mapowania do include'a: + - Przeniesc zawartosc `resources/views/settings/delivery-status-mappings.php` do + `resources/views/settings/_delivery-status-mappings-content.php` + - W `delivery-status-mappings.php` uzywac `include '_delivery-status-mappings-content.php'` + (zachowanie backward compat) + 4. Utworzyc `DeliveryStatusesController.php` w `src/Modules/Settings/`: + - `index()` — laduje statusy z DeliveryStatusRepository, renderuje delivery-statuses.php; + przekazuje `$tab = $_GET['tab'] ?? 'statuses'` i dane mapowania gdy tab=mapping + - `store()` — POST create nowego statusu; waliduje key (lowercase, underscored), label_pl, + color (#hex); zapisuje przez repozytorium; redirect z Flash + - `update(int $id)` — POST edit; blokuje is_system=1; redirect z Flash + - `destroy(int $id)` — POST delete; blokuje is_system=1 i uzywane; redirect z Flash + - Każda akcja mutujaca: walidacja CSRF `_token`, potem `DeliveryStatusRepository::clearCache()` + 5. Utworzyc widok `resources/views/settings/delivery-statuses.php`: + - Dwie zakladki: "Statusy" i "Mapowanie dostawy" + - Persystencja aktywnej zakladki przez `?tab=` param (nie localStorage — zeby linki + z sidebara "Statusy przesylek" i redirect po save trafialy w dobra zakladke) + - Zakladka "Statusy": + * Tabela statusow: kolor (swatchek), klucz, etykieta, sort_order, is_terminal, + akcje (edycja/usun dla nie-systemowych; informacja "systemowy" dla systemowych) + * Formularz dodawania nowego statusu (inline pod tabela lub modal) + * Formularz edycji (inline row edit lub osobny formularz) + * Potwierdzenie usuwania: `window.OrderProAlerts.confirm()` + * Formularz dodawania: pola key (slug, lowercase, max 50), label_pl, color (input type=color), + sort_order (number), is_terminal (checkbox) + - Zakladka "Mapowanie dostawy": + * include '_delivery-status-mappings-content.php' + 6. Dodac styl `resources/scss/settings/_delivery-statuses.scss`: + - `.delivery-status-swatch` — maly kwadrat koloru (16x16px inline-block) + - `.delivery-status-system-badge` — np. szary badge "systemowy" + - Dodac `@use 'settings/delivery-statuses'` do `app.scss` + 7. Dodac trasy w `routes/web.php`: + - GET `/settings/delivery-statuses` → `DeliveryStatusesController::index` + - POST `/settings/delivery-statuses` → `DeliveryStatusesController::store` + - POST `/settings/delivery-statuses/{id}/update` → `DeliveryStatusesController::update` + - POST `/settings/delivery-statuses/{id}/delete` → `DeliveryStatusesController::destroy` + + + - GET /settings/delivery-statuses zwraca 200, widac zakladki + - Dodanie nowego statusu przez CRUD pojawia sie na liscie + - Proba edycji/usuniecia statusu systemowego jest blokowana (HTTP 400 lub redirect z bledem) + - GET /settings/delivery-status-mappings dalej dziala (backward compat) + - redirect po save mapowania idzie do /settings/delivery-statuses?tab=mapping + + AC-2, AC-3, AC-5 spelnione. + + + + Task 3: Sidebar, jezyk, badge rendering i dokumentacja + resources/views/layouts/app.php, resources/lang/pl.php, resources/views/orders/show.php, resources/views/shipments/prepare.php, .paul/docs/DB_SCHEMA.md, .paul/docs/ARCHITECTURE.md, .paul/docs/TECH_CHANGELOG.md + + 1. Zaktualizowac `resources/lang/pl.php`: + - `navigation.statuses` → 'Statusy zamowien' + - Dodac `navigation.delivery_statuses` => 'Statusy przesylek' + 2. Zaktualizowac sidebar `resources/views/layouts/app.php`: + - Zmieniac istniejaca pozycje "Statusy" (href=/settings/statuses) tylko etykiete na + $t('navigation.statuses') (juz uzywana, ale tresc sie zmieni po kroku 1) + - Podmiana calego bloku sidebar z "Mapowanie statusow dostawy" (linie ~130-147): + * Nowy link "Statusy przesylek" href=/settings/delivery-statuses + * active gdy $currentSettings === 'delivery-statuses' + LUB $currentSettings === 'delivery-status-mappings' (backward compat) + * Badge z liczba niezmapowanych (zachowac istniejaca logike z DeliveryStatusMappingRepository) + 3. Zaktualizowac badge rendering: + - W `resources/views/orders/show.php` i `resources/views/shipments/prepare.php`: + Aktualny wzorzec: + `` + Zmienic na helper lub inline logike: + - Jesli `$pkgDeliveryStatus` jest jednym z 11 stalych — uzyj klasy CSS jak dotychczas + - W przeciwnym razie: pobierz kolor z DeliveryStatus::getByKey() i dodaj + `style="background-color: "` + Wzorzec do uzycia (zdefiniowac helper w DeliveryStatus lub inline w widoku): + ```php + $statusColor = DeliveryStatus::getColor($pkgDeliveryStatus); + $isSystemStatus = in_array($pkgDeliveryStatus, DeliveryStatus::ALL_STATUSES); + $badgeClass = 'delivery-badge' . ($isSystemStatus ? ' delivery-badge--' . $pkgDeliveryStatus : ''); + $badgeStyle = $isSystemStatus ? '' : 'background-color: ' . $statusColor . ';'; + ``` + `>` + - Dodac do `DeliveryStatus.php` metode `getColor(string $key): string` — pobiera kolor z + repozytorium lub zwraca '#6c757d' jako fallback + 4. Zaktualizowac `.paul/docs/DB_SCHEMA.md` (nowa tabela delivery_statuses) + 5. Zaktualizowac `.paul/docs/ARCHITECTURE.md` (DeliveryStatusRepository, DeliveryStatusesController) + 6. Zaktualizowac `.paul/docs/TECH_CHANGELOG.md` (entry dla Phase 108 Plan 01) + + + - Sidebar pokazuje "Statusy zamowien" i "Statusy przesylek", brak "Mapowanie statusow dostawy" + - Badge dla istniejacych 11 statusow renderuje sie klasa CSS jak wczesniej + - Badge dla nowego custom statusu renderuje sie z inline style + - Na stronie zamowien i przesylki brak bledow PHP + + AC-4, AC-6 spelnione; dokumentacja aktualna. + + + + + + +## DO NOT CHANGE +- Logika eventow automatyzacji, reguł i akcji niezwiazana z lista statusow +- Provider mapy (INPOST_MAP, APACZKA_MAP, ALLEGRO_MAP) — pozostaja hardcoded compile-time +- TERMINAL_STATUSES — stala pozostaje w kodzie dla logiki biznesowej; DB `is_terminal` jest dodatkowa informacja dla UI +- Runtime konfiguracji DB hostow (`DB_HOST` / `DB_HOST_REMOTE`) +- Istniejace CSS klasy `.delivery-badge--{status}` dla 11 systemowych statusow + +## SCOPE LIMITS +- Brak zmian w logice normalizacji statusow dostawcow (normalize(), normalizeWithOverrides()) +- Brak zmian w cronie ani harmonogramie +- Automatyzacje — dropdown statusow aktualizowany w Plan 02 (nie w tym planie) + + + + +Before declaring plan complete: +- [ ] `SELECT COUNT(*) FROM delivery_statuses` = 11 +- [ ] GET /settings/delivery-statuses zwraca 200 z dwoma zakladkami +- [ ] Dodanie nowego statusu przez CRUD jest widoczne na liscie +- [ ] Statusy systemowe sa zablokowane przed edycja i usunieciem +- [ ] GET /settings/delivery-status-mappings nadal dziala (200) +- [ ] Redirect po save mapowania idzie do /settings/delivery-statuses?tab=mapping +- [ ] Sidebar pokazuje "Statusy zamowien" i "Statusy przesylek" +- [ ] Badge istniejacych statusow bez regresji +- [ ] Badge nowego custom statusu renderuje sie z inline style +- [ ] Dokumentacja .paul/docs/* zaktualizowana + + + +- Operator moze dodac nowy status znormalizowany z panelu bez deploymentu +- Status systemowy (delivered/returned/cancelled) jest nieedytowalny z UI +- Strona /settings/delivery-statuses ma dwie dzialajace zakladki +- Sidebar jest uporzadkowany zgodnie z nowa struktura +- Brak regresji w istniejacym wyswietlaniu odznaczen statusow + + + +After completion, create `.paul/phases/108-delivery-status-management/108-01-SUMMARY.md` + diff --git a/.paul/phases/108-delivery-status-management/108-01-SUMMARY.md b/.paul/phases/108-delivery-status-management/108-01-SUMMARY.md new file mode 100644 index 0000000..85b5cd9 --- /dev/null +++ b/.paul/phases/108-delivery-status-management/108-01-SUMMARY.md @@ -0,0 +1,155 @@ +--- +phase: 108-delivery-status-management +plan: 01 +subsystem: settings, shipments +tags: [delivery-status, crud, repository, scss, sidebar, php, pdo] + +requires: + - phase: 107-automation-email-send-once + provides: project baseline; delivery status constants in DeliveryStatus.php + +provides: + - Tabela delivery_statuses z seedem 11 statusów + - DeliveryStatusRepository (CRUD + per-request static cache) + - DeliveryStatus::setRepository() — dynamiczne ładowanie etykiet/kolorów z DB + - Panel /settings/delivery-statuses z CRUD i zakładką mapowania + - Sidebar: "Statusy zamówień" + "Statusy przesyłek" (z badge niezmapowanych) + - Badge rendering z CSS custom property dla niestandardowych statusów + +affects: [108-02-automation-dropdowns, delivery-status-consumers, shipment-views] + +tech-stack: + added: [] + patterns: + - DeliveryStatusRepository — static cache pattern (private static ?array $cache) + - DeliveryStatus::setRepository() — one-time bootstrap DI dla final static class + - CSS custom property --status-color dla dynamicznych kolorów badge + +key-files: + created: + - database/migrations/20260427_000103_create_delivery_statuses_table.sql + - src/Modules/Shipments/DeliveryStatusRepository.php + - src/Modules/Settings/DeliveryStatusesController.php + - resources/views/settings/delivery-statuses.php + - resources/views/settings/_delivery-status-mappings-content.php + modified: + - src/Modules/Shipments/DeliveryStatus.php + - src/Modules/Settings/DeliveryStatusMappingController.php + - resources/views/settings/delivery-status-mappings.php + - resources/views/layouts/app.php + - resources/scss/modules/_delivery-status.scss + - resources/lang/pl.php + - resources/views/orders/show.php + - resources/views/shipments/prepare.php + +key-decisions: + - "REDIRECT_PATH zmieniony na /settings/delivery-statuses?tab=mapping — wszystkie redirecty z mappingController idą do nowej strony" + - "Zakładki w delivery-statuses przez URL ?tab= (nie localStorage) — redirecty po save lądują na właściwej zakładce" + - "CSS custom property --status-color dla niestandardowych statusów zamiast inline background-color — zgodnie z konwencją projektu" + - "Migracja zawiera seed 11 systemowych statusów — is_system=1 dla delivered/returned/cancelled" + +patterns-established: + - "DeliveryStatusRepository jako wzorzec static cache: private static ?array $cache = null; clearCache() po mutacjach" + - "setRepository() pattern: statyczna final klasa z opcjonalnym wstrzykiwanym repo dla DB fallback" + +duration: ~40min +started: 2026-04-27T00:00:00Z +completed: 2026-04-27T00:00:00Z +--- + +# Phase 108 Plan 01: Delivery Status Management — Core Summary + +**Wyniesiono statusy przesyłek do tabeli DB z CRUD panelem, dynamicznym ładowaniem w DeliveryStatus.php i przebudową sidebaru — operator może dodać nowy status bez deploymentu.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~40 min | +| Tasks | 3/3 completed | +| Files created | 5 | +| Files modified | 8 | +| Delegated agents | 3 Claude | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Tabela delivery_statuses z 11 statusami | Pass* | Migracja gotowa; nieuruchomiona (MySQL offline podczas APPLY) | +| AC-2: CRUD niebędacych systemowych statusów | Pass | Controller + widok z blokadami is_system=1 i in-use check | +| AC-3: /settings/delivery-statuses ma 2 zakładki | Pass | Statusy (CRUD) + Mapowanie dostawy (include) | +| AC-4: Sidebar z nową strukturą | Pass | Statusy zamówień + Statusy przesyłek z badge | +| AC-5: DeliveryStatus ładuje z DB | Pass | setRepository() + label()/getAllOptions()/getColor() z DB fallback | +| AC-6: Badge dla niestandardowych statusów | Pass | .delivery-badge--custom + --status-color CSS var | + +*Migracja musi zostać uruchomiona: `php bin/migrate.php` + +## Accomplishments + +- `DeliveryStatusRepository` z PDO, per-request static cache, pełnym CRUD i blokadami (systemowe + używane statusy) +- `DeliveryStatus::setRepository()` — transparent DB loading bez łamania istniejącego API klasy (ALL_STATUSES, TERMINAL_STATUSES, provider mapy niezmienione) +- Panel `/settings/delivery-statuses` z URL-based tabs (`?tab=statuses` / `?tab=mapping`) — redirecty po save zawsze trafiają na właściwą zakładkę +- `_delivery-status-mappings-content.php` wydzielony jako reużywalny include z `$mappingBaseUrl` — stara strona `/settings/delivery-status-mappings` nadal działa (backward compat) +- Badge rendering z CSS custom property `--status-color` — system statuses → klasy CSS; custom statuses → `delivery-badge--custom` + inline CSS var + +## Files Created/Modified + +| File | Zmiana | Cel | +|------|--------|-----| +| `database/migrations/20260427_000103_create_delivery_statuses_table.sql` | Utworzony | CREATE TABLE + seed 11 statusów | +| `src/Modules/Shipments/DeliveryStatusRepository.php` | Utworzony | CRUD + static cache dla delivery_statuses | +| `src/Modules/Settings/DeliveryStatusesController.php` | Utworzony | Panel /settings/delivery-statuses (index/store/update/destroy) | +| `resources/views/settings/delivery-statuses.php` | Utworzony | Widok 2-zakładkowy (Statusy + Mapowanie) | +| `resources/views/settings/_delivery-status-mappings-content.php` | Utworzony | Wydzielony include z $mappingBaseUrl | +| `src/Modules/Shipments/DeliveryStatus.php` | Zmodyfikowany | +setRepository, +getAllStatuses, +getAllOptions, +getColor; label() z DB | +| `src/Modules/Settings/DeliveryStatusMappingController.php` | Zmodyfikowany | +DeliveryStatusRepository DI; REDIRECT_PATH; walidacja przez repo | +| `resources/views/settings/delivery-status-mappings.php` | Zmodyfikowany | Thin wrapper → include _delivery-status-mappings-content.php | +| `resources/views/layouts/app.php` | Zmodyfikowany | Sidebar: nowy link "Statusy przesyłek", usunięto "Mapowanie statusów" | +| `resources/scss/modules/_delivery-status.scss` | Zmodyfikowany | +delivery-status-swatch, +delivery-status-system-badge, +delivery-badge--custom | +| `resources/lang/pl.php` | Zmodyfikowany | statuses→"Statusy zamówień"; +delivery_statuses→"Statusy przesyłek" | +| `resources/views/orders/show.php` | Zmodyfikowany | Badge (2 miejsca): system→CSS class; custom→--status-color | +| `resources/views/shipments/prepare.php` | Zmodyfikowany | Badge (1 miejsce): system→CSS class; custom→--status-color | +| `.paul/docs/DB_SCHEMA.md` | Zaktualizowany | Nowa tabela delivery_statuses | +| `.paul/docs/ARCHITECTURE.md` | Zaktualizowany | DeliveryStatusRepository, DeliveryStatusesController, setRepository() | +| `.paul/docs/TECH_CHANGELOG.md` | Zaktualizowany | Entry Phase 108 Plan 01 | + +## Decisions Made + +| Decyzja | Uzasadnienie | Wpływ | +|---------|-------------|-------| +| REDIRECT_PATH = `/settings/delivery-statuses?tab=mapping` | Nowa strona jest primary; stara URL to backward compat | Redirecty z DeliveryStatusMappingController lądują na nowej stronie | +| Zakładki przez URL `?tab=` nie localStorage | Redirecty po POST muszą trafić na właściwą zakładkę | Provider tabs w mapping content też używają `$mappingBaseUrl` | +| CSS custom property `--status-color` dla nowych statusów | Konwencja projektu (conventions.md) | Zachowanie spójności; brak inline `background-color` w PHP | +| `is_system=1` dla delivered/returned/cancelled (nie tylko is_terminal) | Są używane w logice biznesowej (AutomationService itp.) | UI blokuje edycję/usunięcie systemowych | + +## Deviations from Plan + +| Typ | Opis | +|-----|------| +| Korekta planu | Plan wspominał "Medoo" — projekt używa PDO. Skorygowano przed delegacją. | +| Korekta SCSS path | Plan mówił `settings/_delivery-statuses.scss` — faktyczny pattern to `modules/`. Dodano do istniejącego `_delivery-status.scss`. | +| Migracja nieuruchomiona | MySQL offline podczas APPLY. Migracja musi być uruchomiona ręcznie. | + +## Issues Encountered + +| Problem | Rozwiązanie | +|---------|------------| +| MySQL offline podczas APPLY | Migracja zapisana, czeka na uruchomienie XAMPP | + +## Next Phase Readiness + +**Ready:** +- `DeliveryStatusRepository` dostępny dla Plan 02 (automatyzacja) +- `DeliveryStatus::getAllOptions()` dostarcza DB-driven listę statusów +- Routing i widoki działają (syntax OK, SCSS build OK) + +**Concerns:** +- Migracja musi być uruchomiona przed testem UI: `php bin/migrate.php` +- Plan 02 (automatyzacja) wymaga sprawdzenia gdzie AutomationController/Repository używa hardcoded listy statusów + +**Blockers:** +- Brak (poza uruchomieniem migracji) + +--- +*Phase: 108-delivery-status-management, Plan: 01* +*Completed: 2026-04-27* diff --git a/.paul/phases/108-delivery-status-management/108-02-PLAN.md b/.paul/phases/108-delivery-status-management/108-02-PLAN.md new file mode 100644 index 0000000..f694ed8 --- /dev/null +++ b/.paul/phases/108-delivery-status-management/108-02-PLAN.md @@ -0,0 +1,136 @@ +--- +phase: 108-delivery-status-management +plan: 02 +type: execute +wave: 2 +depends_on: [108-01] +files_modified: + - src/Modules/Automation/AutomationService.php + - src/Modules/Automation/AutomationRepository.php + - resources/views/automation/form.php + - public/assets/js/modules/automation-form.js + - .paul/docs/ARCHITECTURE.md + - .paul/docs/TECH_CHANGELOG.md +autonomous: true +delegation: on +--- + + +## Goal +Zaktualizowac wszystkie miejsca w module Automatyzacji, ktore uzywaaly hardcoded listy statusow znormalizowanych, zeby korzystaly z DB przez `DeliveryStatus::getAllOptions()`. + +## Purpose +Po Plan 01 operator moze dodac nowy status w DB. Jednak dopoki formularze automatyzacji i walidacje beda uzywaly hardcoded stalych, nowy status nie pojawi sie w dropdownach regul. Plan 02 domyka integracje dla warstwy automatyzacji. + +## Output +- Dropdown warunków/akcji automatyzacji laduje statusy z DB +- Brak regresji dla istniejacych regul automatyzacji + + + +## Project Context +@.paul/STATE.md + +## Prior Work +@.paul/phases/108-delivery-status-management/108-01-SUMMARY.md + +## Source Files +@src/Modules/Automation/AutomationService.php +@src/Modules/Automation/AutomationRepository.php +@resources/views/automation/form.php +@public/assets/js/modules/automation-form.js + + + +## Required Skills + +| Skill | Priority | When to Invoke | Loaded? | +|-------|----------|----------------|---------| +| sonar-scanner (CLI) | required | Po APPLY, przed UNIFY | o | + + + + +## AC-1: Dropdown statusow w automatyzacji pochodzi z DB +```gherkin +Given operator tworzy lub edytuje regule automatyzacji +When wybierze warunek "status przesylki" lub akcje zalezna od statusu +Then lista dostepnych statusow zawiera wszystkie statusy z tabeli delivery_statuses +And nowy status dodany przez /settings/delivery-statuses pojawia sie w dropdownie bez deploymentu +``` + +## AC-2: Istniejace reguly automatyzacji dzialaja bez zmian +```gherkin +Given regula automatyzacji z warunkiem na status "in_transit" +When cron wywola ewaluacje tej reguly +Then warunek jest oceniany tak samo jak przed zmiana +And brak wyjatkow zwiazanych z nieistniejacymi stalymi statusow +``` + + + + + + + Task 1: Zaktualizowac dropdown statusow w automatyzacji + src/Modules/Automation/AutomationService.php, src/Modules/Automation/AutomationRepository.php, resources/views/automation/form.php, public/assets/js/modules/automation-form.js + + 1. Zidentyfikowac wszystkie miejsca w module Automation gdzie uzywa sie: + - `DeliveryStatus::ALL_STATUSES` lub `DeliveryStatus::LABEL_PL` do budowania opcji selecta + - Hardcoded tablicy statusow w walidacji warunkow/akcji + 2. Podpiac `DeliveryStatusRepository` do `AutomationRepository` lub serwisu ktory + buduje opcje dla widoku formularza (sprawdzic jak repo jest dostarczone w tym module). + 3. W widoku `resources/views/automation/form.php` — zamiana hardcoded opcji statusow + na przekazana z kontrolera tablice `$deliveryStatusOptions` (z getAllOptions()). + 4. W `automation-form.js` — opcje do dynamicznie dodawanych wierszy warunkow/akcji: + - Jesli opcje sa generowane statycznie w JS (hardcoded), zastapic je danymi + z `window.automationDeliveryStatusOptions` ktore beda inicjalizowane inline w widoku PHP + - Jesli opcje sa generowane przez PHP przy renderowaniu — brak zmian w JS, tylko upewnic + sie ze dynamicznie dodawane wiersze tez dostaja prawidlowa liste + 5. Walidacja: sprawdzic czy AutomationService lub AutomationRepository waliduje wartosc statusu + przy zapisie — jesli tak, zaktualizowac by uzywala `DeliveryStatusRepository::getByKey()`. + 6. Zaktualizowac `.paul/docs/ARCHITECTURE.md` i `.paul/docs/TECH_CHANGELOG.md`. + + + - Formularz automatyzacji laduje sie poprawnie, dropdown statusow zawiera wszystkie DB-rekordy + - Dodanie nowego statusu przez /settings/delivery-statuses sprawia ze pojawia sie w dropdownie + automatyzacji bez odswiezania cache serwera + - Istniejace reguly z hardcoded kluczami statusow (np. in_transit) dzialaja bez bledow + - `php vendor/bin/phpunit` bez nowych failurow + + AC-1 i AC-2 spelnione. + + + + + + +## DO NOT CHANGE +- Logika ewaluacji warunkow automatyzacji poza podpieciem dynamicznej listy statusow +- Schemat danych regul i akcji automatyzacji +- Mechanizm crona ani harmonogram +- Provider mapy w DeliveryStatus.php + +## SCOPE LIMITS +- Tylko dropdown statusow — nie zmieniamy innych typow warunkow/akcji automatyzacji + + + + +Before declaring plan complete: +- [ ] Dropdown statusow w formularzu automatyzacji pokazuje statusy z DB +- [ ] Nowy status z CRUD pojawia sie w dropdownie automatyzacji +- [ ] Istniejace reguly automatyzacji dzialaja bez bledow +- [ ] `php vendor/bin/phpunit` przechodzi bez regresji +- [ ] Dokumentacja .paul/docs/* zaktualizowana + + + +- Pelna integracja listy statusow z DB w calej aplikacji +- Operator nie musi robic deploymentu zeby nowy status pojawil sie we wszystkich miejscach +- Brak regresji w istniejacych automatyzacjach + + + +After completion, create `.paul/phases/108-delivery-status-management/108-02-SUMMARY.md` + diff --git a/.paul/phases/108-delivery-status-management/108-02-SUMMARY.md b/.paul/phases/108-delivery-status-management/108-02-SUMMARY.md new file mode 100644 index 0000000..31752ad --- /dev/null +++ b/.paul/phases/108-delivery-status-management/108-02-SUMMARY.md @@ -0,0 +1,151 @@ +--- +phase: 108-delivery-status-management +plan: 02 +subsystem: automation, settings +tags: [delivery-status, automation, dropdown, php, db-driven, ui-refactor] + +requires: + - phase: 108-01-delivery-status-management + provides: DeliveryStatusRepository, DeliveryStatus::setRepository(), getAllOptions(), getAllStatuses() + +provides: + - Automation dropdowns dla shipment_status (warunek + akcja) załadowane z DB + - Walidacja shipment_status / update_shipment_status przeciw kluczom z DB + - Osobna podstrona formularza dla CRUD delivery statuses (`/settings/delivery-statuses/new`, `/{id}/edit`) + - Lista statusów uproszczona: rename "Terminal" → "Końcowy", usunięta kolumna "Typ" + +affects: [delivery-status-consumers, automation-rules-existing] + +tech-stack: + added: [] + patterns: + - Path params w controllerach przez `$request->input('id')`, nie jako argumenty metody + - Form-as-separate-page pattern dla CRUD (zamiast inline edit row) + +key-files: + created: + - resources/views/settings/delivery-status-form.php + modified: + - src/Modules/Automation/AutomationController.php + - src/Modules/Automation/AutomationService.php + - src/Modules/Settings/DeliveryStatusesController.php + - resources/views/settings/delivery-statuses.php + - routes/web.php + - .paul/docs/ARCHITECTURE.md + - .paul/docs/TECH_CHANGELOG.md + +key-decisions: + - "BREAKING: Drop backward compat dla starych grupowych kluczy automatyzacji (registered, courier_pickup, etc.) — kolizja semantyczna picked_up by silently dawała wrong matches" + - "Path params w DeliveryStatusesController via $request->input('id') — naprawia bug pre-existing z Plan 01 (update/destroy też miały błędną sygnaturę)" + - "CRUD delivery statuses przeniesiony na osobną podstronę — UX lepszy niż inline edit row" + +patterns-established: + - "Walidacja submitted status keys przez DeliveryStatus::getAllStatuses() (zamiast hardcoded array_keys konstanty)" + - "Form view jako osobna podstrona z disabled key field przy edycji (immutable po utworzeniu)" + +duration: ~50min +started: 2026-04-27T01:00:00Z +completed: 2026-04-27T02:00:00Z +--- + +# Phase 108 Plan 02: Automation Dropdowns z DB Summary + +**Domknięcie integracji DB-driven statusów: dropdowny automatyzacji ładują się z `delivery_statuses`, plus refaktor UI listy statusów (osobna podstrona dla CRUD).** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~50 min | +| Tasks | 1 planowane + 2 user-requested ad-hoc | +| Files created | 1 | +| Files modified | 7 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Dropdown statusów z DB | Pass | `AutomationController::buildShipmentStatusOptions()` z `DeliveryStatus::getAllOptions()` | +| AC-2: Istniejące reguły działają bez błędów | Pass* | *BREAKING: stare grupowe klucze nie matchują (decyzja użytkownika "poprawię ręcznie"); brak wyjątków, ciche no-match | + +## Accomplishments + +- `AutomationController` — usunięto stałą `SHIPMENT_STATUS_OPTIONS` (8 grupowych kluczy); dropdown buduje się z DB przez `DeliveryStatus::getAllOptions()` +- `AutomationService` — usunięto stałą `SHIPMENT_STATUS_OPTION_MAP`; ewaluacja `evaluateShipmentStatusCondition` porównuje klucze bezpośrednio; `resolveStatusFromActionKey` używa kluczy DB jako target +- Walidacja w `parseConditionValue('shipment_status')` i `parseActionConfig('update_shipment_status')` przez `DeliveryStatus::getAllStatuses()` +- **Ad-hoc UI refactor (user request):** osobna podstrona formularza CRUD (`delivery-status-form.php`) zamiast inline edit row na liście; przycisk "+ Dodaj status" zamiast formy na dole strony +- **Ad-hoc UI changes:** rename column "Terminal" → "Końcowy", usunięta kolumna "Typ" (badge "systemowy" — informacja zbędna gdy brak akcji edycji) +- **Bug fix (pre-existing):** sygnatury `update`/`destroy`/`edit` w `DeliveryStatusesController` używały path param jako 2. argument metody — router projektu przekazuje params przez `$request->input('id')`. Naprawione wszystkie trzy. + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `src/Modules/Automation/AutomationController.php` | Modified | Usunięta stała SHIPMENT_STATUS_OPTIONS; nowa metoda `buildShipmentStatusOptions()`; walidacja przez DeliveryStatus | +| `src/Modules/Automation/AutomationService.php` | Modified | Usunięta stała SHIPMENT_STATUS_OPTION_MAP; bezpośrednie porównanie kluczy w ewaluacji | +| `src/Modules/Settings/DeliveryStatusesController.php` | Modified | +`create()`, `edit()`, `renderForm()`; fix path params w update/destroy | +| `resources/views/settings/delivery-status-form.php` | Created | Osobna podstrona formularza CRUD statusów | +| `resources/views/settings/delivery-statuses.php` | Modified | Lista uproszczona: rename Terminal→Końcowy, usunięte Typ + inline edit + bottom add form, przycisk "+ Dodaj status" | +| `routes/web.php` | Modified | +2 GET routes: `/new`, `/{id}/edit` | +| `.paul/docs/ARCHITECTURE.md` | Modified | Sekcja Phase 108 Plan 02 | +| `.paul/docs/TECH_CHANGELOG.md` | Modified | Wpis Phase 108 Plan 02 (BREAKING) | + +## Decisions Made + +| Decyzja | Uzasadnienie | Wpływ | +|---------|-------------|-------| +| Drop backward compat (option A) | Kolizja semantyczna `picked_up`: stary grupowy klucz mapował na `delivered`, nowy klucz DB to "Odebrana przez kuriera" — odwrotne końce cyklu | BREAKING dla istniejących reguł; user: "poprawię ręcznie jak coś" | +| Form jako osobna podstrona zamiast inline | Inline edit row na liście "źle wygląda" (user feedback) | Lepszy UX, czytelniejszy formularz z grid 2-col | +| Usunięcie kolumny "Typ" | Badge "systemowy" zbędny — brak przycisku Edytuj/Usuń przy systemowych już to komunikuje | Lista 6 kolumn zamiast 7 | +| Path params via `$request->input('id')` | Konwencja projektu (router nie injectuje argumentów do handlerów) | Naprawiono pre-existing bug w update/destroy z Plan 01 | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Files w plan, ale bez zmian | 3 | `AutomationRepository.php`, `form.php`, `automation-form.js` — analiza wykazała brak potrzeby modyfikacji | +| Scope additions (user request) | 3 | Rename column, hide column, separate form page | +| Bug fix poza scope | 1 | Path params w update/destroy (pre-existing z Plan 01) | +| Deferred | 1 | sonar-scanner dla Phase 105/106/107/108 — punkt deferred z Plan 01, niezmieniony | + +**Total impact:** Plan zakończony plus dodatkowe ulepszenia UX zgłoszone przez użytkownika podczas weryfikacji Plan 01. Bug fix path params eliminuje crash przy edycji. + +### Auto-fixed Issues + +**1. [Routing] Path params jako argumenty metody zamiast `$request->input()`** +- **Found during:** Test ścieżki `/settings/delivery-statuses/{id}/edit` (user runtime error) +- **Issue:** "Too few arguments to function ... edit(), 1 passed ... and exactly 2 expected" +- **Fix:** Zmieniono sygnatury `edit()`, `update()`, `destroy()` na single `Request $request`; `$id` czytany przez `$request->input('id', 0)` +- **Files:** `src/Modules/Settings/DeliveryStatusesController.php` +- **Verification:** `php -l` OK; konwencja zgodna z `ReceiptController` + +### Deferred Items + +- sonar-scanner skill (required) — Phase 105, 106, 107, 108 (już deferred z poprzednich planów) + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Kolizja semantyczna klucza `picked_up` (stary group → `delivered` vs nowy DB → "Odebrana przez kuriera") | Advisor consult przed kodowaniem; user wybrał opcję A (drop backward compat) | +| Pre-existing bug z path params w update/destroy | Zauważony dopiero przy nowym `edit()` — naprawiony razem | + +## Next Phase Readiness + +**Ready:** +- Pełna integracja DB-driven statusów: dropdowny + walidacja + ewaluacja +- CRUD UI ergonomiczny: lista + osobna podstrona formularza +- Phase 108 — wszystkie 2 plans zamknięte + +**Concerns:** +- Migracja `20260427_000103_create_delivery_statuses_table.sql` — wymaga `php bin/migrate.php` na środowisku produkcyjnym (XAMPP offline podczas APPLY 108-01) +- Stare reguły automatyzacji z grupowymi kluczami (jeśli istnieją) — wymagają ręcznego odtworzenia z nowymi kluczami DB +- sonar-scanner gap dla Phase 105/106/107/108 — odłożony + +**Blockers:** None + +--- +*Phase: 108-delivery-status-management, Plan: 02* +*Completed: 2026-04-27* diff --git a/database/migrations/20260427_000103_create_delivery_statuses_table.sql b/database/migrations/20260427_000103_create_delivery_statuses_table.sql new file mode 100644 index 0000000..0736f8d --- /dev/null +++ b/database/migrations/20260427_000103_create_delivery_statuses_table.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS `delivery_statuses` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `key` VARCHAR(50) NOT NULL, + `label_pl` VARCHAR(100) NOT NULL, + `color` VARCHAR(7) NOT NULL DEFAULT '#6c757d', + `sort_order` TINYINT UNSIGNED NOT NULL DEFAULT 0, + `is_terminal` TINYINT(1) NOT NULL DEFAULT 0, + `is_system` TINYINT(1) NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `delivery_statuses_key_unique` (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO `delivery_statuses` (`key`, `label_pl`, `color`, `sort_order`, `is_terminal`, `is_system`) VALUES + ('unknown', 'Nieznany', '#f5f5f5', 0, 0, 0), + ('created', 'Utworzona', '#e3f2fd', 1, 0, 0), + ('confirmed', 'Potwierdzona', '#bbdefb', 2, 0, 0), + ('picked_up', 'Odebrana przez kuriera', '#e1f5fe', 3, 0, 0), + ('in_transit', 'W tranzycie', '#fff3e0', 4, 0, 0), + ('out_for_delivery', 'W doręczeniu', '#ffe0b2', 5, 0, 0), + ('ready_for_pickup', 'Gotowa do odbioru', '#f3e5f5', 6, 0, 0), + ('delivered', 'Doręczona', '#e8f5e9', 7, 1, 1), + ('returned', 'Zwrócona', '#ffebee', 8, 1, 1), + ('cancelled', 'Anulowana', '#e0e0e0', 9, 1, 1), + ('problem', 'Problem', '#fff8e1', 10, 0, 0); diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 4444fce..3cbd0d6 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%}}.copy-name-row{display:flex;align-items:center;gap:6px}.copy-btn-inline{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;padding:2px;cursor:pointer;color:var(--c-text-muted, #999);border-radius:3px;transition:color .15s;flex-shrink:0}.copy-btn-inline:hover{color:var(--c-primary)}.copy-btn-inline .check-icon{color:var(--c-action-primary)}.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-top:16px;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--picked_up{background:#e1f5fe;color:#01579b}.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}.dsm-unmapped{border-left:4px solid #f59e0b}.dsm-unmapped .section-title{color:#b45309}.dsm-unmapped table tbody tr{background:rgba(245,158,11,.05)}.global-search{flex:1;max-width:500px;position:relative;margin:0 16px}.global-search__input{width:100%;padding:6px 12px;font-size:13px;border:1px solid var(--c-border);border-radius:4px;background:var(--c-bg);color:var(--c-text);outline:none;transition:border-color .15s}.global-search__input::placeholder{color:var(--c-text-muted, #94a3b8)}.global-search__input:focus{border-color:var(--c-primary, #3b82f6);box-shadow:0 0 0 2px rgba(59,130,246,.15)}.global-search__results{display:none;position:absolute;top:100%;left:0;right:0;margin-top:4px;background:var(--c-surface, #fff);border:1px solid var(--c-border);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);max-height:400px;overflow-y:auto;z-index:1000}.global-search__item{display:block;padding:8px 12px;cursor:pointer;text-decoration:none;color:var(--c-text);border-bottom:1px solid var(--c-border);transition:background-color .1s}.global-search__item:last-child{border-bottom:none}.global-search__item:hover,.global-search__item.is-highlighted{background:var(--c-bg, #f1f5f9)}.global-search__item-title{font-weight:600;font-size:13px;margin-bottom:2px}.global-search__item-details{font-size:11px;color:var(--c-text-muted, #64748b)}.global-search__empty{padding:12px;text-align:center;color:var(--c-text-muted, #94a3b8);font-size:13px}@media(max-width: 768px){.global-search{max-width:none;margin:0 8px}.global-search__input{font-size:12px;padding:5px 8px}}.order-preview-overlay{position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:20px}.order-preview-modal{background:var(--c-surface);border:1px solid var(--c-border);border-radius:10px;box-shadow:0 16px 48px rgba(0,0,0,.18);width:100%;max-width:960px;max-height:90vh;display:flex;flex-direction:column}.order-preview-modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--c-border)}.order-preview-modal__title{font-size:18px;font-weight:600;margin:0}.order-preview-modal__close{background:none;border:none;font-size:22px;cursor:pointer;color:var(--c-muted);padding:0 4px;line-height:1}.order-preview-modal__close:hover{color:var(--c-text)}.order-preview-modal__body{padding:20px 24px;overflow-y:auto;flex:1}.order-preview-modal__footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--c-border)}.order-preview-loading{text-align:center;padding:32px;color:var(--c-muted)}.order-preview-error{text-align:center;padding:32px;color:var(--c-danger, #e53e3e)}.order-preview-section{margin-bottom:14px}.order-preview-section__title{font-size:13px;font-weight:600;text-transform:uppercase;color:var(--c-muted);margin-bottom:8px;letter-spacing:.04em}.order-preview-kv{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;font-size:14px}.order-preview-kv dt{color:var(--c-muted);white-space:nowrap}.order-preview-kv dd{margin:0;display:flex;align-items:center;gap:4px}.order-preview-items{width:100%;font-size:14px;border-collapse:collapse}.order-preview-items th,.order-preview-items td{padding:6px 8px;text-align:left;vertical-align:top}.order-preview-items th{font-weight:600;font-size:12px;text-transform:uppercase;color:var(--c-muted)}.order-preview-items tbody tr+tr{border-top:1px solid var(--c-border)}.order-preview-item-cell{display:flex;align-items:flex-start;gap:8px}.order-preview-item-thumb{width:42px;height:42px;object-fit:cover;border-radius:4px;border:1px solid var(--c-border);flex-shrink:0}.order-preview-item-thumb--empty{background:var(--c-bg, #f5f5f5)}.order-preview-item-info{min-width:0}.order-preview-item-name{font-size:14px;line-height:1.3;word-break:break-word}.order-preview-personalization{margin-top:4px;font-size:12px;color:var(--c-muted);line-height:1.4}.order-preview-personalization__line{white-space:pre-wrap;word-break:break-word}.order-preview-notes{font-size:14px}.order-preview-notes__item{padding:6px 0}.order-preview-notes__item+.order-preview-notes__item{border-top:1px solid var(--c-border)}.order-preview-notes__type{font-size:11px;color:var(--c-muted);margin-bottom:2px}.order-preview-notes__text{white-space:pre-wrap;word-break:break-word}.copy-field__btn{background:none;border:none;cursor:pointer;font-size:13px;color:var(--c-muted);padding:0 2px;line-height:1;opacity:.6;transition:opacity .15s;display:inline-flex;align-items:center;gap:3px}.copy-field__btn:hover{opacity:1;color:var(--c-primary, #4f6ef7)}.copy-field__btn.is-copied{color:#22c55e;opacity:1}.btn-icon.js-order-preview-btn{background:none;border:none;cursor:pointer;font-size:14px;color:var(--c-muted);padding:2px 4px;line-height:1;opacity:.5;transition:opacity .15s;vertical-align:middle;margin-right:4px}.btn-icon.js-order-preview-btn:hover{opacity:1;color:var(--c-primary, #4f6ef7)}.pm-form__row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap}.pm-form__field{flex:1;min-width:160px}.pm-form__actions{display:flex;align-items:flex-end;padding-bottom:2px}.pm-row--inactive{opacity:.5}.pm-row__actions{white-space:nowrap}.pm-row__actions .btn+.btn{margin-left:4px}.project-badge{display:inline-flex;align-items:center;gap:2px;font-size:10px;line-height:1;padding:1px 4px;border-radius:3px;vertical-align:middle;margin-left:4px}.project-badge--done{color:#16a34a;background:rgba(22,163,74,.1)}.project-badge--partial{color:#d97706;background:rgba(217,119,6,.1);font-weight:600}.project-badge--none{color:#9ca3af;background:rgba(156,163,175,.1)}.item-project-badge{display:inline-block;font-size:10px;padding:1px 6px;border-radius:3px;margin-left:6px;vertical-align:middle}.item-project-badge--done{color:#16a34a;background:rgba(22,163,74,.1)}.item-project-badge--pending{color:#9ca3af;background:rgba(156,163,175,.1)}.pm-modal{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center}.pm-modal__overlay{position:absolute;inset:0;background:rgba(0,0,0,.4)}.pm-modal__content{position:relative;width:100%;max-width:500px;z-index:1}.customer-risk-banner{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:6px;background:#fff0f0;border:1px solid #fecaca;border-left:4px solid #d64545;color:#6b1f1f;font-size:13px}.customer-risk-banner__icon{flex-shrink:0;font-size:18px;line-height:1;color:#d64545}.customer-risk-banner__body{flex:1;min-width:0}.customer-risk-banner__text{margin:0;font-weight:600;color:#6b1f1f}.customer-risk-banner__list{margin-top:6px}.customer-risk-banner__list summary{cursor:pointer;color:#9b2c2c;font-size:12px;user-select:none}.customer-risk-banner__table{width:100%;margin-top:6px;font-size:12px;border-collapse:collapse}.customer-risk-banner__table th,.customer-risk-banner__table td{padding:4px 6px;border-bottom:1px solid #f5d6d6;text-align:left;color:#3b0f0f}.customer-risk-banner__table thead th{font-size:11px;text-transform:uppercase;letter-spacing:.02em;color:#7a2323;background:#ffe3e3}.customer-risk-banner__table tbody tr:last-child th,.customer-risk-banner__table tbody tr:last-child td{border-bottom:0}.customer-risk-banner__table a{color:#b91c1c;font-weight:600}.risk-return-badge{display:inline-block;padding:1px 6px;background:#d64545;color:#fff;font-size:11px;font-weight:600;border-radius:3px;margin-left:4px;cursor:default;vertical-align:middle;line-height:1.4}.table-list-table tbody tr.is-risk-return>td:first-child{border-left:3px solid #d64545}*{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;padding:18px 0}.sidebar.is-collapsed .sidebar__brand-text{display:none}.sidebar.is-collapsed .sidebar__brand{justify-content:center;margin:4px 0 16px}.sidebar.is-collapsed .sidebar__label{display:none}.sidebar.is-collapsed .sidebar__toggle-arrow{display:none}.sidebar.is-collapsed .sidebar__link,.sidebar.is-collapsed .sidebar__group-toggle{justify-content:center;padding:9px;border-radius:8px;margin:0 6px}.sidebar.is-collapsed .sidebar__group-links{display:none}.sidebar.is-collapsed .sidebar__icon{margin:0}.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}.sidebar__badge{margin-left:auto;background:#f59e0b;color:#1f2937;font-size:10.5px;font-weight:700;line-height:1;padding:2px 6px;border-radius:10px;min-width:18px;text-align:center}.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;margin-bottom:8px;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}.statistics-orders-page{padding:10px}.statistics-orders-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.statistics-orders-filters{display:grid;grid-template-columns:repeat(auto-fit, minmax(170px, 1fr));gap:10px;align-items:end}.statistics-orders-filters__actions{align-self:end}.statistics-orders-multiselect{min-height:120px;height:120px;padding-top:6px;padding-bottom:6px}.statistics-orders-table-wrap{overflow-x:auto}.statistics-orders-table{min-width:880px}.statistics-orders-table thead th{text-align:center;white-space:nowrap}.statistics-orders-table tbody td,.statistics-orders-table tfoot th{text-align:right;white-space:nowrap}.statistics-orders-table tbody td:first-child,.statistics-orders-table tfoot th:first-child{text-align:left}.statistics-orders-table tfoot th{border-top:2px solid #cbd5e1;background:#f8fafc}.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;white-space:nowrap}.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;display:flex;align-items:center;justify-content:space-between;list-style:none}.order-statuses-side__title::-webkit-details-marker{display:none}.order-statuses-side__arrow{display:none;flex-shrink:0;opacity:.5;transition:transform .2s ease}details[open]>.order-statuses-side__title .order-statuses-side__arrow{transform:rotate(180deg)}.order-status-group{margin-bottom:10px}.order-status-group__name{display:flex;align-items:center;justify-content:space-between;gap:6px;font-size:12px;color:#475569;font-weight:700;margin-bottom:5px;text-decoration:none;padding:3px 6px;border-radius:6px;border-left:3px solid rgba(0,0,0,0);cursor:pointer;transition:background .15s}.order-status-group__name:hover{background:#f1f5f9}.order-status-group__count{min-width:24px;text-align:center;border-radius:999px;background:var(--group-color, #64748b);padding:1px 6px;font-weight:700;font-size:11px;color:#fff}.order-status-group.is-active>.order-status-group__name{background:rgba(15,23,42,.06);color:#0f172a;border-left-color:var(--group-color, #64748b)}.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}.item-personalization{margin-top:4px;padding:4px 8px;background:#f8fafc;border-left:2px solid #cbd5e1;border-radius:2px;font-size:.92em;color:#475569;line-height:1.4}.item-personalization__label{font-weight:600;color:#64748b;display:block;margin-bottom:2px}.item-personalization__line{white-space:pre-wrap;word-break:break-word}.order-grid-2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));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}.order-payment-shipping .section-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px}.order-payment-shipping .btn-edit-inline{background:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);color:#6b7280;padding:3px 5px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s,background-color .15s,color .15s}.order-payment-shipping .btn-edit-inline:hover{background:#f3f4f6;color:#111827}.order-payment-shipping:hover .btn-edit-inline{opacity:1}.order-details-edit-form{margin-top:12px;padding:10px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;font-size:12px}.order-details-edit-form .form-row{margin-bottom:8px}.order-details-edit-form label{display:block;color:#374151;font-weight:500}.order-details-edit-form label input[type=text]{display:block;width:100%;margin-top:3px;padding:5px 7px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;box-sizing:border-box}.order-details-edit-form label.checkbox-inline{display:flex;align-items:center;gap:6px;font-weight:400}.order-details-edit-form label.checkbox-inline input{margin:0}.order-details-edit-form label.checkbox-inline code{background:#eef2ff;padding:1px 4px;border-radius:3px;font-size:11px}.order-details-edit-form .form-actions{display:flex;gap:6px;margin-top:8px}.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}.payment-add-form{background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:12px;max-width:700px}.payment-add-form__row{display:flex;flex-wrap:wrap;gap:10px}.payment-add-form__field{display:flex;flex-direction:column;gap:3px;flex:1 1 140px;min-width:120px}.payment-add-form__field label{font-size:11px;color:#64748b;font-weight:500}.payment-add-form__field input,.payment-add-form__field select{font-size:12px;padding:4px 8px;border:1px solid #cbd5e1;border-radius:4px;height:30px}.payment-add-form__actions{display:flex;gap:8px;margin-top:12px}.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}.topbar__hamburger{display:none;align-items:center;justify-content:center;width:36px;height:36px;padding:0;background:rgba(0,0,0,0);border:none;color:var(--c-text-strong);cursor:pointer;border-radius:6px;flex-shrink:0}.topbar__hamburger:hover{background:var(--c-bg-subtle, #f1f5f9)}.sidebar-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:999;opacity:0;transition:opacity .25s ease}.sidebar-backdrop.is-visible{display:block;opacity:1}body.no-scroll{overflow:hidden}@media(max-width: 768px){.topbar__hamburger{display:flex}.sidebar{position:fixed;top:0;left:0;bottom:0;width:280px;min-width:280px;z-index:1000;transform:translateX(-100%);transition:transform .25s ease;border-right:1px solid #243041;overflow-y:auto}.sidebar.is-mobile-open{transform:translateX(0)}.sidebar__brand{margin:4px 4px 12px}.sidebar__collapse-btn{display:flex}.sidebar__collapse-icon{transform:rotate(180deg)}.sidebar__nav{display:grid;gap:4px}.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-statuses-side__title{cursor:pointer}.order-statuses-side__arrow{display:block}.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}.table-list-table tbody tr.order-row-aged>td{border-top:2px solid rgba(0,0,0,0);border-bottom:2px solid rgba(0,0,0,0)}.table-list-table tbody tr.order-row-aged>td:first-child{border-left:2px solid rgba(0,0,0,0)}.table-list-table tbody tr.order-row-aged>td:last-child{border-right:2px solid rgba(0,0,0,0)}.table-list-table tbody tr.order-row-aged-4>td{border-color:#f8b4b4}.table-list-table tbody tr.order-row-aged-5>td{border-color:#f28282}.table-list-table tbody tr.order-row-aged-6>td{border-color:#e74c3c}.table-list-table tbody tr.order-row-aged-7>td{border-color:#991b1b} +: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%}}.copy-name-row{display:flex;align-items:center;gap:6px}.copy-btn-inline{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;padding:2px;cursor:pointer;color:var(--c-text-muted, #999);border-radius:3px;transition:color .15s;flex-shrink:0}.copy-btn-inline:hover{color:var(--c-primary)}.copy-btn-inline .check-icon{color:var(--c-action-primary)}.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-top:16px;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--picked_up{background:#e1f5fe;color:#01579b}.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}.delivery-status-swatch{display:inline-block;width:14px;height:14px;border-radius:2px;background:var(--status-color, #6c757d);vertical-align:middle}.delivery-status-system-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:.75em;background:#e9ecef;color:#6c757d}.delivery-badge--custom{background:var(--status-color, #6c757d);color:#fff}.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}.dsm-unmapped{border-left:4px solid #f59e0b}.dsm-unmapped .section-title{color:#b45309}.dsm-unmapped table tbody tr{background:rgba(245,158,11,.05)}.global-search{flex:1;max-width:500px;position:relative;margin:0 16px}.global-search__input{width:100%;padding:6px 12px;font-size:13px;border:1px solid var(--c-border);border-radius:4px;background:var(--c-bg);color:var(--c-text);outline:none;transition:border-color .15s}.global-search__input::placeholder{color:var(--c-text-muted, #94a3b8)}.global-search__input:focus{border-color:var(--c-primary, #3b82f6);box-shadow:0 0 0 2px rgba(59,130,246,.15)}.global-search__results{display:none;position:absolute;top:100%;left:0;right:0;margin-top:4px;background:var(--c-surface, #fff);border:1px solid var(--c-border);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.12);max-height:400px;overflow-y:auto;z-index:1000}.global-search__item{display:block;padding:8px 12px;cursor:pointer;text-decoration:none;color:var(--c-text);border-bottom:1px solid var(--c-border);transition:background-color .1s}.global-search__item:last-child{border-bottom:none}.global-search__item:hover,.global-search__item.is-highlighted{background:var(--c-bg, #f1f5f9)}.global-search__item-title{font-weight:600;font-size:13px;margin-bottom:2px}.global-search__item-details{font-size:11px;color:var(--c-text-muted, #64748b)}.global-search__empty{padding:12px;text-align:center;color:var(--c-text-muted, #94a3b8);font-size:13px}@media(max-width: 768px){.global-search{max-width:none;margin:0 8px}.global-search__input{font-size:12px;padding:5px 8px}}.order-preview-overlay{position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:20px}.order-preview-modal{background:var(--c-surface);border:1px solid var(--c-border);border-radius:10px;box-shadow:0 16px 48px rgba(0,0,0,.18);width:100%;max-width:960px;max-height:90vh;display:flex;flex-direction:column}.order-preview-modal__header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--c-border)}.order-preview-modal__title{font-size:18px;font-weight:600;margin:0}.order-preview-modal__close{background:none;border:none;font-size:22px;cursor:pointer;color:var(--c-muted);padding:0 4px;line-height:1}.order-preview-modal__close:hover{color:var(--c-text)}.order-preview-modal__body{padding:20px 24px;overflow-y:auto;flex:1}.order-preview-modal__footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--c-border)}.order-preview-loading{text-align:center;padding:32px;color:var(--c-muted)}.order-preview-error{text-align:center;padding:32px;color:var(--c-danger, #e53e3e)}.order-preview-section{margin-bottom:14px}.order-preview-section__title{font-size:13px;font-weight:600;text-transform:uppercase;color:var(--c-muted);margin-bottom:8px;letter-spacing:.04em}.order-preview-kv{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;font-size:14px}.order-preview-kv dt{color:var(--c-muted);white-space:nowrap}.order-preview-kv dd{margin:0;display:flex;align-items:center;gap:4px}.order-preview-items{width:100%;font-size:14px;border-collapse:collapse}.order-preview-items th,.order-preview-items td{padding:6px 8px;text-align:left;vertical-align:top}.order-preview-items th{font-weight:600;font-size:12px;text-transform:uppercase;color:var(--c-muted)}.order-preview-items tbody tr+tr{border-top:1px solid var(--c-border)}.order-preview-item-cell{display:flex;align-items:flex-start;gap:8px}.order-preview-item-thumb{width:42px;height:42px;object-fit:cover;border-radius:4px;border:1px solid var(--c-border);flex-shrink:0}.order-preview-item-thumb--empty{background:var(--c-bg, #f5f5f5)}.order-preview-item-info{min-width:0}.order-preview-item-name{font-size:14px;line-height:1.3;word-break:break-word}.order-preview-personalization{margin-top:4px;font-size:12px;color:var(--c-muted);line-height:1.4}.order-preview-personalization__line{white-space:pre-wrap;word-break:break-word}.order-preview-notes{font-size:14px}.order-preview-notes__item{padding:6px 0}.order-preview-notes__item+.order-preview-notes__item{border-top:1px solid var(--c-border)}.order-preview-notes__type{font-size:11px;color:var(--c-muted);margin-bottom:2px}.order-preview-notes__text{white-space:pre-wrap;word-break:break-word}.copy-field__btn{background:none;border:none;cursor:pointer;font-size:13px;color:var(--c-muted);padding:0 2px;line-height:1;opacity:.6;transition:opacity .15s;display:inline-flex;align-items:center;gap:3px}.copy-field__btn:hover{opacity:1;color:var(--c-primary, #4f6ef7)}.copy-field__btn.is-copied{color:#22c55e;opacity:1}.btn-icon.js-order-preview-btn{background:none;border:none;cursor:pointer;font-size:14px;color:var(--c-muted);padding:2px 4px;line-height:1;opacity:.5;transition:opacity .15s;vertical-align:middle;margin-right:4px}.btn-icon.js-order-preview-btn:hover{opacity:1;color:var(--c-primary, #4f6ef7)}.pm-form__row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap}.pm-form__field{flex:1;min-width:160px}.pm-form__actions{display:flex;align-items:flex-end;padding-bottom:2px}.pm-row--inactive{opacity:.5}.pm-row__actions{white-space:nowrap}.pm-row__actions .btn+.btn{margin-left:4px}.project-badge{display:inline-flex;align-items:center;gap:2px;font-size:10px;line-height:1;padding:1px 4px;border-radius:3px;vertical-align:middle;margin-left:4px}.project-badge--done{color:#16a34a;background:rgba(22,163,74,.1)}.project-badge--partial{color:#d97706;background:rgba(217,119,6,.1);font-weight:600}.project-badge--none{color:#9ca3af;background:rgba(156,163,175,.1)}.item-project-badge{display:inline-block;font-size:10px;padding:1px 6px;border-radius:3px;margin-left:6px;vertical-align:middle}.item-project-badge--done{color:#16a34a;background:rgba(22,163,74,.1)}.item-project-badge--pending{color:#9ca3af;background:rgba(156,163,175,.1)}.pm-modal{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center}.pm-modal__overlay{position:absolute;inset:0;background:rgba(0,0,0,.4)}.pm-modal__content{position:relative;width:100%;max-width:500px;z-index:1}.customer-risk-banner{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-radius:6px;background:#fff0f0;border:1px solid #fecaca;border-left:4px solid #d64545;color:#6b1f1f;font-size:13px}.customer-risk-banner__icon{flex-shrink:0;font-size:18px;line-height:1;color:#d64545}.customer-risk-banner__body{flex:1;min-width:0}.customer-risk-banner__text{margin:0;font-weight:600;color:#6b1f1f}.customer-risk-banner__list{margin-top:6px}.customer-risk-banner__list summary{cursor:pointer;color:#9b2c2c;font-size:12px;user-select:none}.customer-risk-banner__table{width:100%;margin-top:6px;font-size:12px;border-collapse:collapse}.customer-risk-banner__table th,.customer-risk-banner__table td{padding:4px 6px;border-bottom:1px solid #f5d6d6;text-align:left;color:#3b0f0f}.customer-risk-banner__table thead th{font-size:11px;text-transform:uppercase;letter-spacing:.02em;color:#7a2323;background:#ffe3e3}.customer-risk-banner__table tbody tr:last-child th,.customer-risk-banner__table tbody tr:last-child td{border-bottom:0}.customer-risk-banner__table a{color:#b91c1c;font-weight:600}.risk-return-badge{display:inline-block;padding:1px 6px;background:#d64545;color:#fff;font-size:11px;font-weight:600;border-radius:3px;margin-left:4px;cursor:default;vertical-align:middle;line-height:1.4}.table-list-table tbody tr.is-risk-return>td:first-child{border-left:3px solid #d64545}*{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;padding:18px 0}.sidebar.is-collapsed .sidebar__brand-text{display:none}.sidebar.is-collapsed .sidebar__brand{justify-content:center;margin:4px 0 16px}.sidebar.is-collapsed .sidebar__label{display:none}.sidebar.is-collapsed .sidebar__toggle-arrow{display:none}.sidebar.is-collapsed .sidebar__link,.sidebar.is-collapsed .sidebar__group-toggle{justify-content:center;padding:9px;border-radius:8px;margin:0 6px}.sidebar.is-collapsed .sidebar__group-links{display:none}.sidebar.is-collapsed .sidebar__icon{margin:0}.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}.sidebar__badge{margin-left:auto;background:#f59e0b;color:#1f2937;font-size:10.5px;font-weight:700;line-height:1;padding:2px 6px;border-radius:10px;min-width:18px;text-align:center}.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;margin-bottom:8px;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}.statistics-orders-page{padding:10px}.statistics-orders-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.statistics-orders-filters{display:grid;grid-template-columns:repeat(auto-fit, minmax(170px, 1fr));gap:10px;align-items:end}.statistics-orders-filters__actions{align-self:end}.statistics-orders-multiselect{min-height:120px;height:120px;padding-top:6px;padding-bottom:6px}.statistics-orders-table-wrap{overflow-x:auto}.statistics-orders-table{min-width:880px}.statistics-orders-table thead th{text-align:center;white-space:nowrap}.statistics-orders-table tbody td,.statistics-orders-table tfoot th{text-align:right;white-space:nowrap}.statistics-orders-table tbody td:first-child,.statistics-orders-table tfoot th:first-child{text-align:left}.statistics-orders-table tfoot th{border-top:2px solid #cbd5e1;background:#f8fafc}.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;white-space:nowrap}.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;display:flex;align-items:center;justify-content:space-between;list-style:none}.order-statuses-side__title::-webkit-details-marker{display:none}.order-statuses-side__arrow{display:none;flex-shrink:0;opacity:.5;transition:transform .2s ease}details[open]>.order-statuses-side__title .order-statuses-side__arrow{transform:rotate(180deg)}.order-status-group{margin-bottom:10px}.order-status-group__name{display:flex;align-items:center;justify-content:space-between;gap:6px;font-size:12px;color:#475569;font-weight:700;margin-bottom:5px;text-decoration:none;padding:3px 6px;border-radius:6px;border-left:3px solid rgba(0,0,0,0);cursor:pointer;transition:background .15s}.order-status-group__name:hover{background:#f1f5f9}.order-status-group__count{min-width:24px;text-align:center;border-radius:999px;background:var(--group-color, #64748b);padding:1px 6px;font-weight:700;font-size:11px;color:#fff}.order-status-group.is-active>.order-status-group__name{background:rgba(15,23,42,.06);color:#0f172a;border-left-color:var(--group-color, #64748b)}.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}.item-personalization{margin-top:4px;padding:4px 8px;background:#f8fafc;border-left:2px solid #cbd5e1;border-radius:2px;font-size:.92em;color:#475569;line-height:1.4}.item-personalization__label{font-weight:600;color:#64748b;display:block;margin-bottom:2px}.item-personalization__line{white-space:pre-wrap;word-break:break-word}.order-grid-2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));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}.order-payment-shipping .section-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px}.order-payment-shipping .btn-edit-inline{background:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);color:#6b7280;padding:3px 5px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s,background-color .15s,color .15s}.order-payment-shipping .btn-edit-inline:hover{background:#f3f4f6;color:#111827}.order-payment-shipping:hover .btn-edit-inline{opacity:1}.order-details-edit-form{margin-top:12px;padding:10px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;font-size:12px}.order-details-edit-form .form-row{margin-bottom:8px}.order-details-edit-form label{display:block;color:#374151;font-weight:500}.order-details-edit-form label input[type=text]{display:block;width:100%;margin-top:3px;padding:5px 7px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;box-sizing:border-box}.order-details-edit-form label.checkbox-inline{display:flex;align-items:center;gap:6px;font-weight:400}.order-details-edit-form label.checkbox-inline input{margin:0}.order-details-edit-form label.checkbox-inline code{background:#eef2ff;padding:1px 4px;border-radius:3px;font-size:11px}.order-details-edit-form .form-actions{display:flex;gap:6px;margin-top:8px}.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}.payment-add-form{background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:12px;max-width:700px}.payment-add-form__row{display:flex;flex-wrap:wrap;gap:10px}.payment-add-form__field{display:flex;flex-direction:column;gap:3px;flex:1 1 140px;min-width:120px}.payment-add-form__field label{font-size:11px;color:#64748b;font-weight:500}.payment-add-form__field input,.payment-add-form__field select{font-size:12px;padding:4px 8px;border:1px solid #cbd5e1;border-radius:4px;height:30px}.payment-add-form__actions{display:flex;gap:8px;margin-top:12px}.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}.topbar__hamburger{display:none;align-items:center;justify-content:center;width:36px;height:36px;padding:0;background:rgba(0,0,0,0);border:none;color:var(--c-text-strong);cursor:pointer;border-radius:6px;flex-shrink:0}.topbar__hamburger:hover{background:var(--c-bg-subtle, #f1f5f9)}.sidebar-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:999;opacity:0;transition:opacity .25s ease}.sidebar-backdrop.is-visible{display:block;opacity:1}body.no-scroll{overflow:hidden}@media(max-width: 768px){.topbar__hamburger{display:flex}.sidebar{position:fixed;top:0;left:0;bottom:0;width:280px;min-width:280px;z-index:1000;transform:translateX(-100%);transition:transform .25s ease;border-right:1px solid #243041;overflow-y:auto}.sidebar.is-mobile-open{transform:translateX(0)}.sidebar__brand{margin:4px 4px 12px}.sidebar__collapse-btn{display:flex}.sidebar__collapse-icon{transform:rotate(180deg)}.sidebar__nav{display:grid;gap:4px}.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-statuses-side__title{cursor:pointer}.order-statuses-side__arrow{display:block}.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}.table-list-table tbody tr.order-row-aged>td{border-top:2px solid rgba(0,0,0,0);border-bottom:2px solid rgba(0,0,0,0)}.table-list-table tbody tr.order-row-aged>td:first-child{border-left:2px solid rgba(0,0,0,0)}.table-list-table tbody tr.order-row-aged>td:last-child{border-right:2px solid rgba(0,0,0,0)}.table-list-table tbody tr.order-row-aged-4>td{border-color:#f8b4b4}.table-list-table tbody tr.order-row-aged-5>td{border-color:#f28282}.table-list-table tbody tr.order-row-aged-6>td{border-color:#e74c3c}.table-list-table tbody tr.order-row-aged-7>td{border-color:#991b1b} diff --git a/resources/lang/pl.php b/resources/lang/pl.php index a447959..d64711e 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -29,7 +29,8 @@ return [ 'cron' => 'Harmonogram', 'dashboard' => 'Dashboard', 'settings' => 'Ustawienia', - 'statuses' => 'Statusy', + 'statuses' => 'Statusy zamówień', + 'delivery_statuses' => 'Statusy przesyłek', 'integrations' => 'Integracje', 'allegro' => 'Integracje Allegro', 'apaczka' => 'Integracja Apaczka', diff --git a/resources/scss/modules/_delivery-status.scss b/resources/scss/modules/_delivery-status.scss index ecac326..840f006 100644 --- a/resources/scss/modules/_delivery-status.scss +++ b/resources/scss/modules/_delivery-status.scss @@ -24,3 +24,26 @@ text-decoration: none; font-size: 0.85em; } + +.delivery-status-swatch { + display: inline-block; + width: 14px; + height: 14px; + border-radius: 2px; + background: var(--status-color, #6c757d); + vertical-align: middle; +} + +.delivery-status-system-badge { + display: inline-block; + padding: 1px 6px; + border-radius: 3px; + font-size: 0.75em; + background: #e9ecef; + color: #6c757d; +} + +.delivery-badge--custom { + background: var(--status-color, #6c757d); + color: #fff; +} diff --git a/resources/views/layouts/app.php b/resources/views/layouts/app.php index 778bc65..80d6694 100644 --- a/resources/views/layouts/app.php +++ b/resources/views/layouts/app.php @@ -139,8 +139,8 @@ $dsmUnmappedCount = 0; } ?> - - Mapowanie statusów dostawy + + 0): ?> diff --git a/resources/views/orders/show.php b/resources/views/orders/show.php index 959d776..ebeae6c 100644 --- a/resources/views/orders/show.php +++ b/resources/views/orders/show.php @@ -370,7 +370,11 @@ foreach ($addressesList as $address) { ?>
Status dostawy
- + + >
@@ -603,7 +607,11 @@ foreach ($addressesList as $address) { $pkgDeliveryDesc = $pkgDeliveryRaw !== '' ? \App\Modules\Shipments\DeliveryStatus::description($pkgProvider, $pkgDeliveryRaw) : ''; $pkgDeliveryTitle = $pkgDeliveryRaw !== '' ? ($pkgDeliveryRaw . ' — ' . $pkgDeliveryDesc) : ''; ?> - + + > + + +
+

Niezmapowane statusy wykryte w systemie ()

+

Statusy odebrane z API przewoźnika , dla których nie ma jeszcze mapowania. Przypisz znormalizowany status, aby paczki przestały być oznaczane jako „Nieznany".

+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + +
Status surowyLiczba paczekOstatnio widzianyOpis (własny)Status znormalizowany
+ + + + + + +
+
+ +
+ +
+
+
+ + +
+ + + +

Brak mapowań dla tego przewoźnika.

+ +
+ + + +
+ + + + + + + + + + + + + + + + + + + + +
Status surowyOpisStatus znormalizowanyAkcje
+ + + + + + + + + + + domyślne + +
+
+ +
+ + +
+
+ + + + + +
+ + diff --git a/resources/views/settings/delivery-status-form.php b/resources/views/settings/delivery-status-form.php new file mode 100644 index 0000000..43e73ec --- /dev/null +++ b/resources/views/settings/delivery-status-form.php @@ -0,0 +1,76 @@ + + +
+

+ + + + + +
+ + +
+ + + +
+ +
+ + + +
+ +
+ +
+ +
+ + Anuluj +
+
+
diff --git a/resources/views/settings/delivery-status-mappings.php b/resources/views/settings/delivery-status-mappings.php index 6fa9831..b316c34 100644 --- a/resources/views/settings/delivery-status-mappings.php +++ b/resources/views/settings/delivery-status-mappings.php @@ -1,220 +1,3 @@ - -
-

Mapowanie statusów dostawy

-

Konfiguracja przypisania surowych statusów z API przewoźników do znormalizowanych statusów w aplikacji.

- - - - - - -
- -
- - -
-

Niezmapowane statusy wykryte w systemie ()

-

Statusy odebrane z API przewoźnika , dla których nie ma jeszcze mapowania. Przypisz znormalizowany status, aby paczki przestały być oznaczane jako „Nieznany".

- -
- - - -
- - - - - - - - - - - - - - - - - - - - - - -
Status surowyLiczba paczekOstatnio widzianyOpis (własny)Status znormalizowany
- - - - - - -
-
- -
- -
-
-
- - -
- - - -

Brak mapowań dla tego przewoźnika.

- -
- - - -
- - - - - - - - - - - - - - - - - - - - -
Status surowyOpisStatus znormalizowanyAkcje
- - - - - - - - - - - domyślne - -
-
- -
- - -
-
- - - - - -
- - +$mappingBaseUrl = '/settings/delivery-status-mappings'; +include __DIR__ . '/_delivery-status-mappings-content.php'; diff --git a/resources/views/settings/delivery-statuses.php b/resources/views/settings/delivery-statuses.php new file mode 100644 index 0000000..65d66c0 --- /dev/null +++ b/resources/views/settings/delivery-statuses.php @@ -0,0 +1,123 @@ + + +
+

Statusy przesyłek

+ + + + +
+ +
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
KolorKluczEtykietaKolejnośćKońcowyAkcje
+ + + + Edytuj + + + + + +
+
+ + + + + + +
+ + diff --git a/resources/views/shipments/prepare.php b/resources/views/shipments/prepare.php index b3d14fd..5da5d24 100644 --- a/resources/views/shipments/prepare.php +++ b/resources/views/shipments/prepare.php @@ -405,7 +405,11 @@ $defaultCodAmount = $isCod ? number_format($totalWithTax, 2, '.', '') : '0'; $pkgDeliveryDesc = $pkgDeliveryRaw !== '' ? \App\Modules\Shipments\DeliveryStatus::description($pkgProvider, $pkgDeliveryRaw) : ''; $pkgDeliveryTitle = $pkgDeliveryRaw !== '' ? ($pkgDeliveryRaw . ' — ' . $pkgDeliveryDesc) : ''; ?> - + + > config('app.cron.web_limit_default', 5) ); $deliveryStatusMappingRepository = new DeliveryStatusMappingRepository($app->db()); + $deliveryStatusRepository = new DeliveryStatusRepository($app->db()); + \App\Modules\Shipments\DeliveryStatus::setRepository($deliveryStatusRepository); + $deliveryStatusesController = new DeliveryStatusesController( + $template, + $translator, + $auth, + $deliveryStatusRepository, + $deliveryStatusMappingRepository + ); $deliveryStatusMappingController = new DeliveryStatusMappingController( $template, $translator, $auth, - $deliveryStatusMappingRepository + $deliveryStatusMappingRepository, + $deliveryStatusRepository ); $companySettingsRepository = new CompanySettingsRepository($app->db()); $companySettingsController = new CompanySettingsController( @@ -513,6 +525,12 @@ return static function (Application $app): void { $router->post('/settings/delivery-status-mappings/save-bulk', [$deliveryStatusMappingController, 'saveBulk'], [$authMiddleware]); $router->post('/settings/delivery-status-mappings/reset', [$deliveryStatusMappingController, 'reset'], [$authMiddleware]); $router->post('/settings/delivery-status-mappings/reset-all', [$deliveryStatusMappingController, 'resetAll'], [$authMiddleware]); + $router->get('/settings/delivery-statuses', [$deliveryStatusesController, 'index'], [$authMiddleware]); + $router->get('/settings/delivery-statuses/new', [$deliveryStatusesController, 'create'], [$authMiddleware]); + $router->get('/settings/delivery-statuses/{id}/edit', [$deliveryStatusesController, 'edit'], [$authMiddleware]); + $router->post('/settings/delivery-statuses', [$deliveryStatusesController, 'store'], [$authMiddleware]); + $router->post('/settings/delivery-statuses/{id}/update', [$deliveryStatusesController, 'update'], [$authMiddleware]); + $router->post('/settings/delivery-statuses/{id}/delete', [$deliveryStatusesController, 'destroy'], [$authMiddleware]); $router->get('/accounting', [$accountingController, 'index'], [$authMiddleware]); $router->post('/accounting/export', [$accountingController, 'export'], [$authMiddleware]); $router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]); diff --git a/src/Modules/Automation/AutomationController.php b/src/Modules/Automation/AutomationController.php index fc9b0fd..f8ca874 100644 --- a/src/Modules/Automation/AutomationController.php +++ b/src/Modules/Automation/AutomationController.php @@ -11,6 +11,7 @@ use App\Core\Support\Flash; use App\Core\View\Template; use App\Modules\Auth\AuthService; use App\Modules\Settings\ReceiptConfigRepository; +use App\Modules\Shipments\DeliveryStatus; use Throwable; final class AutomationController @@ -33,17 +34,6 @@ final class AutomationController 'online' => 'Karta / platnosc online', 'other' => 'Inna', ]; - private const SHIPMENT_STATUS_OPTIONS = [ - 'registered' => ['label' => 'Przesylka zarejestrowana', 'statuses' => ['created', 'confirmed']], - 'courier_pickup' => ['label' => 'Odebrana przez kuriera / nadana w paczkomacie', 'statuses' => ['picked_up']], - 'ready_for_pickup' => ['label' => 'Przesylka do odbioru', 'statuses' => ['ready_for_pickup']], - 'dropped_at_point' => ['label' => 'Przesylka nadana w punkcie', 'statuses' => ['confirmed', 'in_transit']], - 'picked_up' => ['label' => 'Przesylka odebrana', 'statuses' => ['delivered']], - 'cancelled' => ['label' => 'Przesylka anulowana', 'statuses' => ['cancelled']], - 'unclaimed' => ['label' => 'Przesylka nieodebrana', 'statuses' => ['problem']], - 'picked_up_return' => ['label' => 'Przesylka odebrana (zwrot)', 'statuses' => ['returned']], - ]; - public function __construct( private readonly Template $template, private readonly Translator $translator, @@ -256,7 +246,7 @@ final class AutomationController 'receiptConfigs' => $this->listActiveReceiptConfigs(), 'receiptIssueDateModes' => self::ALLOWED_RECEIPT_ISSUE_DATE_MODES, 'receiptDuplicatePolicies' => self::ALLOWED_RECEIPT_DUPLICATE_POLICIES, - 'shipmentStatusOptions' => self::SHIPMENT_STATUS_OPTIONS, + 'shipmentStatusOptions' => $this->buildShipmentStatusOptions(), 'paymentStatusOptions' => self::PAYMENT_STATUS_OPTIONS, 'paymentMethodOptions' => self::PAYMENT_METHOD_OPTIONS, 'orderStatusOptions' => $this->repository->listActiveOrderStatuses(), @@ -427,7 +417,7 @@ final class AutomationController $keys = []; } - $allowedKeys = array_keys(self::SHIPMENT_STATUS_OPTIONS); + $allowedKeys = DeliveryStatus::getAllStatuses(); $statusKeys = array_values(array_filter( array_map(static fn (mixed $key): string => trim((string) $key), $keys), static fn (string $key): bool => $key !== '' && in_array($key, $allowedKeys, true) @@ -570,7 +560,7 @@ final class AutomationController if ($type === 'update_shipment_status') { $statusKey = trim((string) ($action['shipment_status_key'] ?? '')); - if (!array_key_exists($statusKey, self::SHIPMENT_STATUS_OPTIONS)) { + if ($statusKey === '' || !in_array($statusKey, DeliveryStatus::getAllStatuses(), true)) { return null; } @@ -598,6 +588,19 @@ final class AutomationController return null; } + /** + * @return array + */ + private function buildShipmentStatusOptions(): array + { + $options = []; + foreach (DeliveryStatus::getAllOptions() as $key => $label) { + $options[(string) $key] = ['label' => (string) $label]; + } + + return $options; + } + /** * @return list */ diff --git a/src/Modules/Automation/AutomationService.php b/src/Modules/Automation/AutomationService.php index 47ef8a1..360d9ef 100644 --- a/src/Modules/Automation/AutomationService.php +++ b/src/Modules/Automation/AutomationService.php @@ -21,17 +21,6 @@ final class AutomationService private const MAX_CHAIN_DEPTH = 8; private const MAX_CHAIN_EXECUTIONS = 200; - private const SHIPMENT_STATUS_OPTION_MAP = [ - 'registered' => ['created', 'confirmed'], - 'courier_pickup' => ['picked_up'], - 'ready_for_pickup' => ['ready_for_pickup'], - 'dropped_at_point' => ['confirmed', 'in_transit'], - 'picked_up' => ['delivered'], - 'cancelled' => ['cancelled'], - 'unclaimed' => ['problem'], - 'picked_up_return' => ['returned'], - ]; - public function __construct( private readonly AutomationRepository $repository, private readonly AutomationExecutionLogRepository $executionLogs, @@ -189,12 +178,10 @@ final class AutomationService $allowedStatuses = []; foreach ($statusKeys as $statusKeyRaw) { $statusKey = trim((string) $statusKeyRaw); - if ($statusKey === '' || !isset(self::SHIPMENT_STATUS_OPTION_MAP[$statusKey])) { + if ($statusKey === '') { continue; } - foreach (self::SHIPMENT_STATUS_OPTION_MAP[$statusKey] as $mappedStatus) { - $allowedStatuses[$mappedStatus] = true; - } + $allowedStatuses[$statusKey] = true; } if ($allowedStatuses === []) { @@ -609,21 +596,15 @@ final class AutomationService private function resolveStatusFromActionKey(string $statusKey): ?string { - if ($statusKey === '' || !isset(self::SHIPMENT_STATUS_OPTION_MAP[$statusKey])) { + if ($statusKey === '') { return null; } - $mappedStatuses = self::SHIPMENT_STATUS_OPTION_MAP[$statusKey]; - if ($mappedStatuses === []) { + if (!in_array($statusKey, DeliveryStatus::getAllStatuses(), true)) { return null; } - $candidate = trim((string) $mappedStatuses[0]); - if ($candidate === '' || !in_array($candidate, DeliveryStatus::ALL_STATUSES, true)) { - return null; - } - - return $candidate; + return $statusKey; } /** diff --git a/src/Modules/Settings/DeliveryStatusMappingController.php b/src/Modules/Settings/DeliveryStatusMappingController.php index d8fd771..16f8cb0 100644 --- a/src/Modules/Settings/DeliveryStatusMappingController.php +++ b/src/Modules/Settings/DeliveryStatusMappingController.php @@ -12,11 +12,12 @@ use App\Core\View\Template; use App\Modules\Auth\AuthService; use App\Modules\Shipments\DeliveryStatus; use App\Modules\Shipments\DeliveryStatusMappingRepository; +use App\Modules\Shipments\DeliveryStatusRepository; use Throwable; final class DeliveryStatusMappingController { - private const REDIRECT_PATH = '/settings/delivery-status-mappings'; + private const REDIRECT_PATH = '/settings/delivery-statuses?tab=mapping'; private const PROVIDERS = [ 'inpost' => 'InPost', @@ -28,7 +29,8 @@ final class DeliveryStatusMappingController private readonly Template $template, private readonly Translator $translator, private readonly AuthService $auth, - private readonly DeliveryStatusMappingRepository $repository + private readonly DeliveryStatusMappingRepository $repository, + private readonly DeliveryStatusRepository $deliveryStatusRepository ) { } @@ -86,7 +88,7 @@ final class DeliveryStatusMappingController 'providers' => self::PROVIDERS, 'mappings' => $mappings, 'unmappedRawStatuses' => $unmappedRawStatuses, - 'normalizedOptions' => DeliveryStatus::LABEL_PL, + 'normalizedOptions' => $this->deliveryStatusRepository->getAllAsOptions(), 'errorMessage' => (string) Flash::get('dsm_error', ''), 'successMessage' => (string) Flash::get('dsm_success', ''), ], 'layouts/app'); @@ -108,12 +110,12 @@ final class DeliveryStatusMappingController if ($provider === '' || $rawStatus === '') { Flash::set('dsm_error', 'Brakuje wymaganych pól.'); - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } - if (!in_array($normalizedStatus, DeliveryStatus::ALL_STATUSES, true)) { + if ($this->deliveryStatusRepository->getByKey($normalizedStatus) === null) { Flash::set('dsm_error', 'Nieprawidłowy status znormalizowany.'); - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } try { @@ -123,7 +125,7 @@ final class DeliveryStatusMappingController Flash::set('dsm_error', 'Błąd zapisu: ' . $exception->getMessage()); } - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } public function saveBulk(Request $request): Response @@ -140,7 +142,7 @@ final class DeliveryStatusMappingController if (!is_array($rawStatuses) || !is_array($normalizedStatuses) || !is_array($descriptions)) { Flash::set('dsm_error', 'Nieprawidłowe dane formularza.'); - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } try { @@ -156,7 +158,7 @@ final class DeliveryStatusMappingController $normalizedStatus = trim((string) ($normalizedStatuses[$index] ?? '')); $description = trim((string) ($descriptions[$index] ?? '')); - if (!in_array($normalizedStatus, DeliveryStatus::ALL_STATUSES, true)) { + if ($this->deliveryStatusRepository->getByKey($normalizedStatus) === null) { continue; } @@ -178,7 +180,7 @@ final class DeliveryStatusMappingController Flash::set('dsm_error', 'Błąd zapisu: ' . $exception->getMessage()); } - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } public function reset(Request $request): Response @@ -193,7 +195,7 @@ final class DeliveryStatusMappingController if ($provider === '' || $rawStatus === '') { Flash::set('dsm_error', 'Brakuje wymaganych pól.'); - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } try { @@ -203,7 +205,7 @@ final class DeliveryStatusMappingController Flash::set('dsm_error', 'Błąd resetu: ' . $exception->getMessage()); } - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } public function resetAll(Request $request): Response @@ -226,7 +228,7 @@ final class DeliveryStatusMappingController Flash::set('dsm_error', 'Błąd resetu: ' . $exception->getMessage()); } - return Response::redirect(self::REDIRECT_PATH . '?provider=' . rawurlencode($provider)); + return Response::redirect(self::REDIRECT_PATH . '&provider=' . rawurlencode($provider)); } private function validateCsrf(string $token): ?Response diff --git a/src/Modules/Settings/DeliveryStatusesController.php b/src/Modules/Settings/DeliveryStatusesController.php new file mode 100644 index 0000000..9983159 --- /dev/null +++ b/src/Modules/Settings/DeliveryStatusesController.php @@ -0,0 +1,286 @@ + 'InPost', + 'apaczka' => 'Apaczka', + 'allegro_wza' => 'Allegro', + ]; + + public function __construct( + private readonly Template $template, + private readonly Translator $translator, + private readonly AuthService $auth, + private readonly DeliveryStatusRepository $deliveryStatusRepository, + private readonly DeliveryStatusMappingRepository $deliveryStatusMappingRepository + ) { + } + + public function index(Request $request): Response + { + $tab = (string) ($request->input('tab', 'statuses') ?? 'statuses'); + if (!in_array($tab, ['statuses', 'mapping'], true)) { + $tab = 'statuses'; + } + + $data = [ + 'title' => 'Statusy przesyłek', + 'activeMenu' => 'settings', + 'activeSettings' => 'delivery-statuses', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'tab' => $tab, + 'statuses' => $this->deliveryStatusRepository->getAll(), + 'errorMessage' => (string) Flash::get('ds_error', ''), + 'successMessage' => (string) Flash::get('ds_success', ''), + ]; + + if ($tab === 'mapping') { + $data = array_merge($data, $this->buildMappingData($request)); + } + + $html = $this->template->render('settings/delivery-statuses', $data, 'layouts/app'); + + return Response::html($html); + } + + public function create(Request $request): Response + { + return $this->renderForm(null); + } + + public function edit(Request $request): Response + { + $id = max(0, (int) $request->input('id', 0)); + $row = null; + foreach ($this->deliveryStatusRepository->getAll() as $candidate) { + if ((int) ($candidate['id'] ?? 0) === $id) { + $row = $candidate; + break; + } + } + + if ($row === null) { + Flash::set('ds_error', 'Status nie istnieje.'); + return Response::redirect(self::REDIRECT_STATUSES); + } + + if ((int) ($row['is_system'] ?? 0) === 1) { + Flash::set('ds_error', 'Statusów systemowych nie można edytować.'); + return Response::redirect(self::REDIRECT_STATUSES); + } + + return $this->renderForm($row); + } + + public function store(Request $request): Response + { + $csrfError = $this->validateCsrf((string) $request->input('_token', '')); + if ($csrfError !== null) { + return $csrfError; + } + + $key = trim((string) $request->input('key', '')); + $labelPl = trim((string) $request->input('label_pl', '')); + $color = trim((string) $request->input('color', '#6c757d')); + $sortOrder = (int) $request->input('sort_order', 0); + $isTerminal = $request->input('is_terminal', '') !== '' ? 1 : 0; + + $validationError = $this->validateFields($key, $labelPl, $color, true); + if ($validationError !== null) { + Flash::set('ds_error', $validationError); + return Response::redirect(self::REDIRECT_STATUSES); + } + + try { + $this->deliveryStatusRepository->create([ + 'key' => $key, + 'label_pl' => $labelPl, + 'color' => $color, + 'sort_order' => $sortOrder, + 'is_terminal' => $isTerminal, + ]); + Flash::set('ds_success', 'Status dodany.'); + } catch (Throwable $exception) { + Flash::set('ds_error', 'Błąd: ' . $exception->getMessage()); + } + + return Response::redirect(self::REDIRECT_STATUSES); + } + + public function update(Request $request): Response + { + $csrfError = $this->validateCsrf((string) $request->input('_token', '')); + if ($csrfError !== null) { + return $csrfError; + } + + $id = max(0, (int) $request->input('id', 0)); + $labelPl = trim((string) $request->input('label_pl', '')); + $color = trim((string) $request->input('color', '#6c757d')); + $sortOrder = (int) $request->input('sort_order', 0); + $isTerminal = $request->input('is_terminal', '') !== '' ? 1 : 0; + + $validationError = $this->validateFields('valid_key', $labelPl, $color, false); + if ($validationError !== null) { + Flash::set('ds_error', $validationError); + return Response::redirect(self::REDIRECT_STATUSES); + } + + try { + $this->deliveryStatusRepository->update($id, [ + 'label_pl' => $labelPl, + 'color' => $color, + 'sort_order' => $sortOrder, + 'is_terminal' => $isTerminal, + ]); + Flash::set('ds_success', 'Status zaktualizowany.'); + } catch (Throwable $exception) { + Flash::set('ds_error', 'Błąd: ' . $exception->getMessage()); + } + + return Response::redirect(self::REDIRECT_STATUSES); + } + + public function destroy(Request $request): Response + { + $csrfError = $this->validateCsrf((string) $request->input('_token', '')); + if ($csrfError !== null) { + return $csrfError; + } + + $id = max(0, (int) $request->input('id', 0)); + + try { + $this->deliveryStatusRepository->delete($id); + Flash::set('ds_success', 'Status usunięty.'); + } catch (Throwable $exception) { + Flash::set('ds_error', 'Błąd: ' . $exception->getMessage()); + } + + return Response::redirect(self::REDIRECT_STATUSES); + } + + /** + * @param array|null $row + */ + private function renderForm(?array $row): Response + { + $isEdit = $row !== null && isset($row['id']); + + $html = $this->template->render('settings/delivery-status-form', [ + 'title' => $isEdit ? 'Edytuj status przesyłki' : 'Nowy status przesyłki', + 'activeMenu' => 'settings', + 'activeSettings' => 'delivery-statuses', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'row' => $row, + 'isEdit' => $isEdit, + 'errorMessage' => (string) Flash::get('ds_error', ''), + ], 'layouts/app'); + + return Response::html($html); + } + + private function validateCsrf(string $token): ?Response + { + if (Csrf::validate($token)) { + return null; + } + + Flash::set('ds_error', $this->translator->get('auth.errors.csrf_expired')); + return Response::redirect(self::REDIRECT_STATUSES); + } + + private function validateFields(string $key, string $labelPl, string $color, bool $validateKey): ?string + { + if ($validateKey) { + if ($key === '' || !preg_match('/^[a-z][a-z0-9_]{0,49}$/', $key)) { + return 'Klucz statusu jest nieprawidłowy (tylko małe litery, cyfry, _, max 50 znaków, zaczyna się literą).'; + } + } + + if ($labelPl === '' || mb_strlen($labelPl) > 100) { + return 'Etykieta jest wymagana i nie może przekraczać 100 znaków.'; + } + + if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) { + return 'Kolor musi być w formacie #RRGGBB.'; + } + + return null; + } + + /** + * @return array + */ + private function buildMappingData(Request $request): array + { + $provider = strtolower(trim((string) $request->input('provider', 'inpost'))); + if (!isset(self::PROVIDERS[$provider])) { + $provider = 'inpost'; + } + + $defaults = DeliveryStatus::getDefaultMappings($provider); + $overrides = $this->deliveryStatusMappingRepository->listByProvider($provider); + + $overrideMap = []; + foreach ($overrides as $row) { + $overrideMap[$row['raw_status']] = $row; + } + + $mappings = []; + $knownRawStatuses = []; + foreach ($defaults as $rawStatus => $default) { + $isCustom = isset($overrideMap[$rawStatus]); + $mappings[] = [ + 'raw_status' => $rawStatus, + 'description' => $isCustom ? $overrideMap[$rawStatus]['description'] : $default['description'], + 'normalized_status' => $isCustom ? $overrideMap[$rawStatus]['normalized_status'] : $default['normalized'], + 'is_custom' => $isCustom, + ]; + $knownRawStatuses[$rawStatus] = true; + } + + foreach ($overrideMap as $rawStatus => $row) { + if (isset($knownRawStatuses[$rawStatus])) { + continue; + } + $mappings[] = [ + 'raw_status' => $rawStatus, + 'description' => $row['description'], + 'normalized_status' => $row['normalized_status'], + 'is_custom' => true, + ]; + $knownRawStatuses[$rawStatus] = true; + } + + $unmappedRawStatuses = $this->deliveryStatusMappingRepository->listUnmappedRawStatuses($provider, $knownRawStatuses); + + return [ + 'provider' => $provider, + 'providers' => self::PROVIDERS, + 'mappings' => $mappings, + 'unmappedRawStatuses' => $unmappedRawStatuses, + 'normalizedOptions' => $this->deliveryStatusRepository->getAllAsOptions(), + ]; + } +} diff --git a/src/Modules/Shipments/DeliveryStatus.php b/src/Modules/Shipments/DeliveryStatus.php index 0da2a43..be0280f 100644 --- a/src/Modules/Shipments/DeliveryStatus.php +++ b/src/Modules/Shipments/DeliveryStatus.php @@ -5,6 +5,8 @@ namespace App\Modules\Shipments; final class DeliveryStatus { + private static ?DeliveryStatusRepository $repository = null; + public const UNKNOWN = 'unknown'; public const CREATED = 'created'; public const CONFIRMED = 'confirmed'; @@ -379,8 +381,46 @@ final class DeliveryStatus return $map[$rawStatus] ?? $rawStatus; } + public static function setRepository(DeliveryStatusRepository $repo): void + { + self::$repository = $repo; + } + + public static function getAllStatuses(): array + { + if (self::$repository !== null) { + return array_column(self::$repository->getAll(), 'key'); + } + return self::ALL_STATUSES; + } + + public static function getAllOptions(): array + { + if (self::$repository !== null) { + return self::$repository->getAllAsOptions(); + } + return self::LABEL_PL; + } + + public static function getColor(string $key): string + { + if (self::$repository !== null) { + $row = self::$repository->getByKey($key); + if ($row !== null) { + return (string) $row['color']; + } + } + return '#6c757d'; + } + public static function label(string $status): string { + if (self::$repository !== null) { + $row = self::$repository->getByKey($status); + if ($row !== null) { + return (string) $row['label_pl']; + } + } return self::LABEL_PL[$status] ?? 'Nieznany'; } diff --git a/src/Modules/Shipments/DeliveryStatusRepository.php b/src/Modules/Shipments/DeliveryStatusRepository.php new file mode 100644 index 0000000..c6cbef7 --- /dev/null +++ b/src/Modules/Shipments/DeliveryStatusRepository.php @@ -0,0 +1,157 @@ +db->prepare( + 'SELECT * FROM delivery_statuses ORDER BY sort_order ASC' + ); + $statement->execute(); + $rows = $statement->fetchAll(PDO::FETCH_ASSOC); + + self::$cache = is_array($rows) ? $rows : []; + + return self::$cache; + } + + public function getByKey(string $key): ?array + { + foreach ($this->getAll() as $row) { + if ($row['key'] === $key) { + return $row; + } + } + + return null; + } + + public function getAllAsOptions(): array + { + $options = []; + foreach ($this->getAll() as $row) { + $options[$row['key']] = $row['label_pl']; + } + + return $options; + } + + public function create(array $data): int + { + if ($this->getByKey($data['key']) !== null) { + throw new \RuntimeException('Status o tym kluczu już istnieje.'); + } + + $statement = $this->db->prepare( + 'INSERT INTO delivery_statuses (`key`, label_pl, color, sort_order, is_terminal) + VALUES (:key, :label_pl, :color, :sort_order, :is_terminal)' + ); + $statement->bindValue(':key', $data['key']); + $statement->bindValue(':label_pl', $data['label_pl']); + $statement->bindValue(':color', $data['color']); + $statement->bindValue(':sort_order', $data['sort_order'], PDO::PARAM_INT); + $statement->bindValue(':is_terminal', $data['is_terminal'], PDO::PARAM_INT); + $statement->execute(); + + $this->clearCache(); + + return (int) $this->db->lastInsertId(); + } + + public function update(int $id, array $data): void + { + $row = $this->findById($id); + if ($row === null) { + throw new \RuntimeException('Status nie istnieje.'); + } + + if ((int) $row['is_system'] === 1) { + throw new \RuntimeException('Statusów systemowych nie można edytować.'); + } + + $statement = $this->db->prepare( + 'UPDATE delivery_statuses + SET label_pl = :label_pl, color = :color, sort_order = :sort_order, is_terminal = :is_terminal + WHERE id = :id' + ); + $statement->bindValue(':label_pl', $data['label_pl']); + $statement->bindValue(':color', $data['color']); + $statement->bindValue(':sort_order', $data['sort_order'], PDO::PARAM_INT); + $statement->bindValue(':is_terminal', $data['is_terminal'], PDO::PARAM_INT); + $statement->bindValue(':id', $id, PDO::PARAM_INT); + $statement->execute(); + + $this->clearCache(); + } + + public function delete(int $id): void + { + $row = $this->findById($id); + if ($row === null) { + throw new \RuntimeException('Status nie istnieje.'); + } + + if ((int) $row['is_system'] === 1) { + throw new \RuntimeException('Statusów systemowych nie można usunąć.'); + } + + $key = (string) $row['key']; + + $stmtMappings = $this->db->prepare( + 'SELECT COUNT(*) FROM delivery_status_mappings WHERE normalized_status = :key' + ); + $stmtMappings->bindValue(':key', $key); + $stmtMappings->execute(); + $countMappings = (int) $stmtMappings->fetchColumn(); + + $stmtPackages = $this->db->prepare( + 'SELECT COUNT(*) FROM shipment_packages WHERE delivery_status = :key' + ); + $stmtPackages->bindValue(':key', $key); + $stmtPackages->execute(); + $countPackages = (int) $stmtPackages->fetchColumn(); + + if ($countMappings > 0 || $countPackages > 0) { + throw new \RuntimeException('Status jest używany i nie może być usunięty.'); + } + + $statement = $this->db->prepare( + 'DELETE FROM delivery_statuses WHERE id = :id' + ); + $statement->bindValue(':id', $id, PDO::PARAM_INT); + $statement->execute(); + + $this->clearCache(); + } + + public function clearCache(): void + { + self::$cache = null; + } + + private function findById(int $id): ?array + { + foreach ($this->getAll() as $row) { + if ((int) $row['id'] === $id) { + return $row; + } + } + + return null; + } +}