diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 60ad5bb..f6c3117 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -7,6 +7,12 @@ "lmtime": 1772652932723, "modified": false }, + "_allegro_check.php": { + "type": "-", + "size": 1954, + "lmtime": 1772803697369, + "modified": false + }, "ARCHITECTURE.md": { "type": "-", "size": 659, @@ -733,6 +739,24 @@ } } }, + "_db_check2.php": { + "type": "-", + "size": 1656, + "lmtime": 1772803550728, + "modified": false + }, + "_db_check3.php": { + "type": "-", + "size": 1919, + "lmtime": 1772803572007, + "modified": false + }, + "_db_check.php": { + "type": "-", + "size": 2025, + "lmtime": 1772803459353, + "modified": false + }, "DB_SCHEMA.md": { "type": "-", "size": 363, @@ -748,9 +772,9 @@ }, "DB_SCHEMA.md": { "type": "-", - "size": 9585, + "size": 9908, "lmtime": 1772751841787, - "modified": false + "modified": true }, "ORDERS_SCHEMA_APILO_DRAFT.md": { "type": "-", @@ -766,8 +790,8 @@ }, "TECH_CHANGELOG.md": { "type": "-", - "size": 21813, - "lmtime": 1772751847189, + "size": 22477, + "lmtime": 1772803892625, "modified": false }, "todo.md": { @@ -2057,8 +2081,8 @@ "lang": { "pl.php": { "type": "-", - "size": 49807, - "lmtime": 1772751834546, + "size": 49998, + "lmtime": 1772791865236, "modified": false } }, @@ -2168,14 +2192,14 @@ }, "list.php": { "type": "-", - "size": 2344, + "size": 1552, "lmtime": 1772497374098, "modified": true }, "show.php": { "type": "-", - "size": 17387, - "lmtime": 1772747158454, + "size": 26016, + "lmtime": 1772791685988, "modified": false } }, @@ -2214,8 +2238,8 @@ "settings": { "allegro.php": { "type": "-", - "size": 31284, - "lmtime": 1772754778803, + "size": 32395, + "lmtime": 1772791281085, "modified": false }, "apaczka.php": { @@ -2290,8 +2314,8 @@ "shipments": { "prepare.php": { "type": "-", - "size": 26553, - "lmtime": 1772754107648, + "size": 28300, + "lmtime": 1772792497957, "modified": false } } @@ -2300,8 +2324,8 @@ "routes": { "web.php": { "type": "-", - "size": 10727, - "lmtime": 1772750372002, + "size": 10849, + "lmtime": 1772791657587, "modified": false } }, @@ -2349,8 +2373,8 @@ "Core": { "Application.php": { "type": "-", - "size": 10729, - "lmtime": 1772661737740, + "size": 11003, + "lmtime": 1772803779141, "modified": false }, "Database": { @@ -2566,9 +2590,9 @@ "Orders": { "OrderImportRepository.php": { "type": "-", - "size": 18405, + "size": 18666, "lmtime": 1772655751334, - "modified": false + "modified": true }, "OrderImportService.php": { "type": "-", @@ -2578,15 +2602,15 @@ }, "OrdersController.php": { "type": "-", - "size": 26496, - "lmtime": 1772752037320, + "size": 27379, + "lmtime": 1772791653816, "modified": false }, "OrdersRepository.php": { "type": "-", - "size": 30133, + "size": 30430, "lmtime": 1772752022929, - "modified": false + "modified": true }, "OrderStatusSyncService.php": { "type": "-", @@ -2686,8 +2710,8 @@ }, "AllegroIntegrationController.php": { "type": "-", - "size": 36725, - "lmtime": 1772751733083, + "size": 36988, + "lmtime": 1772794656848, "modified": false }, "AllegroIntegrationRepository.php": { @@ -2704,14 +2728,14 @@ }, "AllegroOrderImportService.php": { "type": "-", - "size": 32629, - "lmtime": 1772743953928, + "size": 32797, + "lmtime": 1772792227813, "modified": false }, "AllegroOrdersSyncService.php": { "type": "-", "size": 11544, - "lmtime": 1772660728121, + "lmtime": 1772803813533, "modified": false }, "AllegroOrderSyncStateRepository.php": { @@ -2734,8 +2758,8 @@ }, "AllegroStatusSyncService.php": { "type": "-", - "size": 1537, - "lmtime": 1772661706942, + "size": 3266, + "lmtime": 1772803803020, "modified": false }, "ApaczkaIntegrationController.php": { @@ -2834,14 +2858,14 @@ "Shipments": { "AllegroShipmentService.php": { "type": "-", - "size": 17565, - "lmtime": 1772753969008, + "size": 18000, + "lmtime": 1772792396143, "modified": false }, "ShipmentController.php": { "type": "-", - "size": 10999, - "lmtime": 1772753068348, + "size": 12247, + "lmtime": 1772792492765, "modified": false }, "ShipmentPackageRepository.php": { @@ -4735,6 +4759,12 @@ } } }, + "_test_status_sync.php": { + "type": "-", + "size": 2434, + "lmtime": 1772803861129, + "modified": false + }, "tmp_gs1_test.php": { "type": "-", "size": 3392, diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index 008ef55..971d7df 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -32,6 +32,7 @@ - `POST /settings/statuses/reorder` - `GET /settings/cron` - `POST /settings/cron` +- `GET /settings/integrations` - `GET /settings/integrations/allegro` - `POST /settings/integrations/allegro/save` - `POST /settings/integrations/allegro/oauth/start` @@ -45,6 +46,12 @@ - `POST /settings/integrations/apaczka/save` - `GET /settings/integrations/inpost` - `POST /settings/integrations/inpost/save` +- `GET /settings/integrations/shoppro` +- `POST /settings/integrations/shoppro/save` +- `POST /settings/integrations/shoppro/test` +- `POST /settings/integrations/shoppro/statuses/save` +- `POST /settings/integrations/shoppro/statuses/sync` +- `POST /settings/integrations/shoppro/delivery/save` - `GET /health` - `GET /` (redirect) @@ -69,6 +76,8 @@ - `App\Modules\Settings\AllegroOrderImportService` - `App\Modules\Settings\AllegroStatusMappingRepository` - `App\Modules\Settings\AllegroStatusDiscoveryService` +- `App\Modules\Settings\IntegrationsRepository` +- `App\Modules\Settings\IntegrationSecretCipher` - `App\Modules\Orders\OrderImportRepository` - `App\Modules\Settings\CronSettingsController` - `App\Modules\Cron\CronRepository` @@ -76,12 +85,24 @@ - `App\Modules\Cron\AllegroTokenRefreshHandler` - `App\Modules\Cron\AllegroOrdersImportHandler` - `App\Modules\Cron\AllegroStatusSyncHandler` +- `App\Modules\Cron\ShopproOrdersImportHandler` +- `App\Modules\Cron\ShopproStatusSyncHandler` +- `App\Modules\Cron\ShopproPaymentStatusSyncHandler` - `App\Modules\Users\UsersController` - `App\Modules\Users\UserRepository` - `App\Modules\Settings\ApaczkaIntegrationController` - `App\Modules\Settings\ApaczkaIntegrationRepository` - `App\Modules\Settings\InpostIntegrationController` - `App\Modules\Settings\InpostIntegrationRepository` +- `App\Modules\Settings\IntegrationsHubController` +- `App\Modules\Settings\ShopproIntegrationsController` +- `App\Modules\Settings\ShopproIntegrationsRepository` +- `App\Modules\Settings\ShopproStatusMappingRepository` +- `App\Modules\Settings\ShopproDeliveryMethodMappingRepository` +- `App\Modules\Settings\ShopproApiClient` +- `App\Modules\Settings\ShopproOrdersSyncService` +- `App\Modules\Settings\ShopproStatusSyncService` +- `App\Modules\Settings\ShopproPaymentStatusSyncService` - `App\Modules\Settings\AllegroOrdersSyncService` - `App\Modules\Settings\AllegroOrderSyncStateRepository` - `App\Modules\Settings\AllegroStatusSyncService` @@ -167,15 +188,14 @@ - Sidebar (`resources/views/layouts/app.php`) ma nowy podlink: - `Statusy` (`/settings/statuses`). - `Cron` (`/settings/cron`). - - `Integracje Allegro` (`/settings/integrations/allegro`). - - `Integracja Apaczka` (`/settings/integrations/apaczka`). - - `Integracja InPost` (`/settings/integrations/inpost`). + - `Integracje` (`/settings/integrations`) - wspolny hub konfiguracji providerow. ## Przeplyw Ustawienia > Cron - `GET /settings/cron`: - `CronSettingsController::index(Request): Response` - pobiera ustawienia `cron_run_on_web`, `cron_web_limit`, - - renderuje harmonogramy (`cron_schedules`) oraz kolejke/historie (`cron_jobs`). + - renderuje harmonogramy (`cron_schedules`) oraz kolejke/historie (`cron_jobs`), + - historia (`past_jobs`) ma stronicowanie po parametrze query `past_page` (25 rekordow na strone). - `POST /settings/cron`: - `CronSettingsController::save(Request): Response` - waliduje CSRF, @@ -196,6 +216,9 @@ - Dodatkowy handler: - `allegro_orders_import` -> `AllegroOrdersImportHandler::handle(...)` (automatyczny import zamowien Allegro). - `allegro_status_sync` -> `AllegroStatusSyncHandler::handle(...)` (synchronizacja statusow wg kierunku z ustawien integracji Allegro). + - `shoppro_orders_import` -> `ShopproOrdersImportHandler::handle(...)` (automatyczny import zamowien z aktywnych integracji `shopPRO` z wlaczonym pobieraniem). + - `shoppro_order_status_sync` -> `ShopproStatusSyncHandler::handle(...)` (synchronizacja statusow shopPRO wg kierunku ustawionego per instancja). + - `shoppro_payment_status_sync` -> `ShopproPaymentStatusSyncHandler::handle(...)` (odswiezanie statusu platnosci zamowien shopPRO na podstawie flagi `paid`). ## Przeplyw Ustawienia > Integracje > Allegro - `GET /settings/integrations/allegro`: @@ -248,10 +271,11 @@ - mapuje terminy wysylki z `delivery.time.dispatch` do `send_date_min` / `send_date_max`, - buduje diagnostyke importu miniatur (statystyki + przyczyny brakow), - mapuje status Allegro na status orderPRO na podstawie `allegro_order_status_mappings`, - - mapuje payload Allegro na neutralny model tabel zamowien, + - mapuje payload Allegro na neutralny model tabel zamowien (z `integration_id` aktywnej instancji Allegro), - zapisuje aggregate przez `OrderImportRepository::upsertOrderAggregate(...)`. - `AllegroOrdersSyncService`: - uruchamiany z crona (`allegro_orders_import`), + - korzysta z dynamicznego `integration_id` aktywnego srodowiska Allegro (zamiast stalej), - respektuje ustawienia integracji (`orders_fetch_enabled`, `orders_fetch_start_date`), - pobiera listy checkout forms (`GET /order/checkout-forms?sort=-updatedAt`) i importuje nowe/zmienione zamowienia, - utrzymuje kursor sync i status ostatniego wykonania w `integration_order_sync_state`. @@ -286,7 +310,7 @@ - `POST /settings/integrations/apaczka/save`: - `ApaczkaIntegrationController::save(Request): Response` - waliduje CSRF i klucz API, - - zapisuje zaszyfrowany klucz API przez `ApaczkaIntegrationRepository::saveSettings(...)`. + - zapisuje zaszyfrowany klucz API przez `ApaczkaIntegrationRepository::saveSettings(...)` do tabeli bazowej `integrations` (`type=apaczka`). ## Przeplyw Ustawienia > Integracja InPost - `GET /settings/integrations/inpost`: @@ -296,7 +320,9 @@ - `POST /settings/integrations/inpost/save`: - `InpostIntegrationController::save(Request): Response` - waliduje CSRF, - - zapisuje ustawienia (token API szyfrowany AES-256-CBC, parametry domyslne przesylek) przez `InpostIntegrationRepository::saveSettings(...)`. + - zapisuje ustawienia przez `InpostIntegrationRepository::saveSettings(...)`: + - token API w `integrations.api_key_encrypted` (`type=inpost`), + - parametry specyficzne przewoznika w `inpost_integration_settings`. ## Przeplyw Ustawienia > Baza danych - `GET /settings/database`: @@ -323,3 +349,66 @@ - nowe klasy i metody (sygnatury + odpowiedzialnosc), - zmiany przeplywu request -> controller -> repository, - kontrakty wejscia/wyjscia istotnych metod. +## Przeplyw Ustawienia > Integracje (hub) +- `GET /settings/integrations`: + - `IntegrationsHubController::index(Request): Response` + - buduje liste instancji providerow (Allegro sandbox/production, Apaczka, InPost, shopPRO), + - pokazuje tabele podsumowania i przycisk `Ustawienia` w kazdym wierszu, + - przycisk `Ustawienia` prowadzi do dedykowanego ekranu providera (`/settings/integrations/allegro|apaczka|inpost|shoppro`), + - renderuje `resources/views/settings/integrations.php`. + +## Przeplyw Ustawienia > Integracje > shopPRO +- `GET /settings/integrations/shoppro`: + - `ShopproIntegrationsController::index(Request): Response` + - pobiera liste instancji przez `ShopproIntegrationsRepository::listIntegrations()`, + - opcjonalnie laduje wskazana instancje (`?id=`) przez `findIntegration(...)`, + - renderuje `resources/views/settings/shoppro.php` z zakladkami: `Integracja`, `Statusy`, `Ustawienia`, `Formy dostawy`. +- `POST /settings/integrations/shoppro/save`: + - `ShopproIntegrationsController::save(Request): Response` + - waliduje CSRF, nazwe, URL (`http|https`), klucz API (wymagany przy nowej konfiguracji) oraz format daty `orders_fetch_start_date` (`Y-m-d`), + - zapisuje konfiguracje przez `ShopproIntegrationsRepository::saveIntegration(...)` do tabeli bazowej `integrations` (`type=shoppro`), + - zapisuje interwal joba `shoppro_orders_import` (minuty) do `cron_schedules.interval_seconds`, + - zapisuje kierunek synchronizacji statusow per instancja (`integrations.order_status_sync_direction`), + - zapisuje interwal joba `shoppro_order_status_sync` (minuty) do `cron_schedules.interval_seconds`, + - zapisuje interwal joba `shoppro_payment_status_sync` (minuty) do `cron_schedules.interval_seconds`, + - zapisuje liste statusow orderPRO (per instancja) dla kontroli platnosci (`integrations.payment_sync_status_codes_json`). +- `POST /settings/integrations/shoppro/test`: + - `ShopproIntegrationsController::test(Request): Response` + - waliduje CSRF i `integration_id`, + - wykonuje test API przez `ShopproIntegrationsRepository::testConnection(...)`, + - zapisuje wynik testu w `integrations.last_test_*` i `integration_test_logs`. +- `POST /settings/integrations/shoppro/statuses/sync`: + - `ShopproIntegrationsController::syncStatuses(Request): Response` + - pobiera slownik statusow z API (`dictionaries/statuses`) przez `ShopproIntegrationsRepository::fetchOrderStatuses(...)`, + - przekazuje odkryte statusy do widoku zakladki `Statusy` (flash/sesja). +- `POST /settings/integrations/shoppro/statuses/save`: + - `ShopproIntegrationsController::saveStatusMappings(Request): Response` + - waliduje CSRF, `integration_id` i kody statusow orderPRO, + - zapisuje mapowania per instancja shopPRO przez `ShopproStatusMappingRepository::replaceForIntegration(...)` do `order_status_mappings`. +- `POST /settings/integrations/shoppro/delivery/save`: + - `ShopproIntegrationsController::saveDeliveryMappings(Request): Response` + - waliduje CSRF i `integration_id`, + - zapisuje mapowania form dostawy przez `ShopproDeliveryMethodMappingRepository::saveMappings(...)` (per instancja). +- `ShopproOrdersSyncService`: + - uruchamiany z crona (`shoppro_orders_import`), + - pobiera liste zamowien i (opcjonalnie) szczegoly zamowienia z API shopPRO, + - mapuje kwoty z fallbackami (`summary`, `paid`, `transport_cost`) oraz ceny pozycji (`price_brutto`), + - uzupelnia `order_items.media_url` przez pobranie `products/get` po `product_id`, gdy zamowienie nie zawiera obrazu. + - mapuje punkty odbioru (`inpost_paczkomat` / `orlen_point`) do adresu `delivery` (`parcel_external_id`, `parcel_name`, ulica/kod/miasto), + - uzupelnia `delivery` o telefon/e-mail klienta i etykiete metody dostawy z kosztem (`transport_cost`). +- `ShopproStatusSyncService`: + - uruchamiany z crona (`shoppro_order_status_sync`), + - filtruje aktywne instancje `shopPRO` po kierunku synchronizacji statusow (`shoppro_to_orderpro`), + - dla wspieranego kierunku wykorzystuje `ShopproOrdersSyncService` do odswiezenia statusow/importu danych, + - dla kierunku `orderpro_to_shoppro` pomija instancje i zwraca wynik informacyjny (tryb przygotowany pod kolejny etap). +- `ShopproPaymentStatusSyncService`: + - uruchamiany z crona (`shoppro_payment_status_sync`), + - pobiera zamowienia shopPRO nieoznaczone jako oplacone (`orders.payment_status != 2`) i nie-finalne, + - dla kazdego zamowienia odpytuje API `orders/get|details` i odczytuje flage `paid`, + - aktualizuje `orders.payment_status`, `orders.total_paid` i `order_payments`, + - zapisuje log `payment` do `order_activity_log`, + - respektuje liste statusow z `integrations.payment_sync_status_codes_json` (gdy pusta: fallback na pomijanie statusow finalnych). +- Zakladka `Formy dostawy` (shopPRO): + - laduje formy dostawy wykryte w zamowieniach danej instancji (`orders.source=shoppro` + `orders.integration_id`), + - laduje uslugi dostawy z Allegro API (`delivery-services`) z fallbackiem na odswiezenie tokenu OAuth, + - zapisuje mapowanie: forma dostawy shopPRO -> usluga Allegro/InPost WZA. diff --git a/DOCS/DB_SCHEMA.md b/DOCS/DB_SCHEMA.md index 28071b5..4b29894 100644 --- a/DOCS/DB_SCHEMA.md +++ b/DOCS/DB_SCHEMA.md @@ -27,6 +27,24 @@ - 2026-03-06: Dodano kolumne `carrier` do tabeli `allegro_delivery_method_mappings` (default 'allegro') - umozliwia mapowanie na roznych przewoznikow (Allegro, InPost). - 2026-03-06: Wdrozono migracje `20260302_000019_add_internal_order_number_to_orders.sql` - kolumna `internal_order_number` VARCHAR(11) UNIQUE w tabeli `orders`, format `OPXXXXXXXXX` (np. `OP000000001`); backfill istniejacych rekordow; UI: lista i szczegoly zamowien wyswietlaja numer wewnetrzny jako glowny identyfikator. - 2026-03-04: Poprawiono prezentacje daty zamowienia na liscie (`fallback ordered_at -> source_created_at -> source_updated_at -> fetched_at`) - bez zmian schematu. +- 2026-03-08: Rozpoczeto ujednolicanie integracji - migracja `20260308_000037_unify_integrations_base_links.sql`: + - dodano `integration_id` do tabel `allegro_integration_settings`, `apaczka_integration_settings`, `inpost_integration_settings`, + - dodano FK/UNIQUE 1:1 z tabela `integrations`, + - dodano/uzupelniono rekordy bazowe providerow (`allegro`, `apaczka`, `inpost`) w `integrations`. +- 2026-03-08: Dodano UI i endpointy konfiguracji `shopPRO` (wieloinstancyjnie) w oparciu o istniejaca tabele `integrations` (`type=shoppro`) - bez zmian schematu. +- 2026-03-08: Dodano mapowanie statusow dla `shopPRO` (zakladka `Statusy`) z zapisem do istniejacej tabeli `order_status_mappings` per `integration_id` - bez zmian schematu. +- 2026-03-08: Dodano migracje naprawcza `20260308_000038_ensure_order_status_mappings_table.sql` (uzupelnia brakujaca tabele `order_status_mappings` w srodowiskach z niepelna historia migracji). +- 2026-03-08: Dodano migracje naprawcza `20260308_000039_ensure_integrations_fetch_columns.sql` (uzupelnia brakujace kolumny `orders_fetch_enabled` i `orders_fetch_start_date` w `integrations`). +- 2026-03-08: Dodano migracje `20260308_000040_ensure_shoppro_orders_import_schedule.sql` (seed/naprawa harmonogramu `cron_schedules` dla joba `shoppro_orders_import`) - bez zmian schematu tabel. +- 2026-03-08: Dodano migracje `20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql` (seed/naprawa harmonogramu `cron_schedules` dla joba `shoppro_order_status_sync` oraz uzupelnienie kolumny `integrations.order_status_sync_direction` w srodowiskach niezgodnych). +- 2026-03-08: Dodano migracje `20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql`: + - uzupelnienie kolumny `integrations.payment_sync_status_codes_json` (JSON) dla konfiguracji statusow objetych kontrola platnosci, + - seed/naprawa harmonogramu `cron_schedules` dla joba `shoppro_payment_status_sync` (domyslnie 600s, priorytet 105). +- 2026-03-08: Dodano migracje `20260308_000043_create_shoppro_delivery_method_mappings_table.sql`: + - nowa tabela `shoppro_delivery_method_mappings` (mapowanie form dostawy per `integration_id`), + - tabela przechowuje mapowanie formy shopPRO na usluge Allegro WZA/InPost (`allegro_delivery_method_id`, `allegro_credentials_id`, `allegro_carrier_id`, `allegro_service_name`, `carrier`). +- 2026-03-08: Poprawiono mapowanie importu zamowien shopPRO (kwoty i miniatury pozycji) - bez zmian schematu bazy. +- 2026-03-08: Poprawiono mapowanie danych wysylki shopPRO (paczkomat/punkt odbioru + kontakt klienta + koszt transportu) - bez zmian schematu bazy. ## Tabele @@ -94,10 +112,27 @@ - `last_error` (varchar 500), - `created_at`, `updated_at`. +### `order_status_mappings` +- Mapowanie statusow zamowien shopPRO na statusy orderPRO per instancja integracji. +- Kolumny: + - `id` (PK, int unsigned, AI), + - `integration_id` (int unsigned, FK -> `integrations.id`), + - `shoppro_status_code` (varchar 64), + - `shoppro_status_name` (varchar 128, nullable), + - `orderpro_status_code` (varchar 64), + - `created_at`, `updated_at`. +- Indeksy: + - `order_status_mappings_integration_shoppro_unique` (UNIQUE: `integration_id`, `shoppro_status_code`), + - `order_status_mappings_integration_idx` (`integration_id`), + - `order_status_mappings_orderpro_idx` (`orderpro_status_code`). +- Klucze obce: + - `order_status_mappings_integration_fk`: `integration_id` -> `integrations.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`). + ### `allegro_integration_settings` -- Konfiguracja pojedynczej integracji Allegro (`id = 1`) zarzadzanej z `Ustawienia > Integracje > Allegro`. +- Konfiguracja OAuth i sync dla integracji Allegro per srodowisko (`sandbox|production`) zarzadzana z `Ustawienia > Integracje > Allegro`. - Kolumny: - `id` (PK, tinyint unsigned), + - `integration_id` (int unsigned, UNIQUE, FK -> `integrations.id`), - `environment` (varchar 16, `sandbox|production`), - `client_id` (varchar 128), - `client_secret_encrypted` (text), @@ -113,7 +148,8 @@ - `created_at`, `updated_at`. - Indeksy: - `allegro_integration_settings_environment_idx` (`environment`), - - `allegro_integration_settings_token_expires_at_idx` (`token_expires_at`). + - `allegro_integration_settings_token_expires_at_idx` (`token_expires_at`), + - `allegro_integration_settings_integration_unique` (`integration_id`, UNIQUE). ### `allegro_order_status_mappings` - Mapowanie kodow statusow Allegro na kody statusow orderPRO. @@ -142,17 +178,54 @@ - `order_activity_log_order_created_idx` (`order_id`, `created_at`), - `order_activity_log_event_type_idx` (`event_type`). +### `integrations` +- Bazowa tabela wszystkich instancji integracji (model docelowy pod wielu providerow i wiele kont per provider). +- Kolumny: + - `id` (PK, int unsigned, AI), + - `type` (varchar 32, np. `allegro`, `apaczka`, `inpost`, `shoppro`), + - `name` (varchar 128, unikalne w obrebie `type`), + - `base_url` (varchar 255), + - `api_key_encrypted` (text, nullable), + - `timeout_seconds` (smallint unsigned), + - `is_active` (tinyint(1)), + - `orders_fetch_enabled` (tinyint(1)), + - `orders_fetch_start_date` (date, nullable), + - `order_status_sync_direction` (varchar 32), + - `payment_sync_status_codes_json` (json, nullable; lista kodow statusow orderPRO, dla ktorych cron ma sprawdzac oplacenie zamowien), + - pola diagnostyki testu (`last_test_status`, `last_test_http_code`, `last_test_message`, `last_test_at`), + - `created_at`, `updated_at`. + +### `shoppro_delivery_method_mappings` +- Mapowanie form dostawy shopPRO na uslugi dostawy Allegro WZA/InPost per instancja integracji. +- Kolumny: + - `id` (PK, int unsigned, AI), + - `integration_id` (int unsigned, FK -> `integrations.id`), + - `order_delivery_method` (varchar 200), + - `carrier` (varchar 50; np. `allegro`, `inpost`), + - `allegro_delivery_method_id` (varchar 128), + - `allegro_credentials_id` (varchar 128, nullable), + - `allegro_carrier_id` (varchar 128, nullable), + - `allegro_service_name` (varchar 255, nullable), + - `created_at`, `updated_at`. +- Indeksy: + - `shoppro_dm_mapping_unique` (UNIQUE: `integration_id`, `order_delivery_method`), + - `shoppro_dm_mapping_integration_idx` (`integration_id`). +- Klucze obce: + - `shoppro_dm_mapping_integration_fk`: `integration_id` -> `integrations.id` (`ON DELETE CASCADE`, `ON UPDATE CASCADE`). + ### `apaczka_integration_settings` -- Konfiguracja pojedynczej integracji Apaczka (`id = 1`) zarzadzanej z `Ustawienia > Integracja Apaczka`. +- Tabela kompatybilnosci dla integracji Apaczka (`id = 1`); sekret API jest utrzymywany bazowo w `integrations.api_key_encrypted`. - Kolumny: - `id` (PK, tinyint unsigned), + - `integration_id` (int unsigned, UNIQUE, FK -> `integrations.id`), - `api_key_encrypted` (text, nullable), - `created_at`, `updated_at`. ### `inpost_integration_settings` -- Konfiguracja pojedynczej integracji InPost ShipX (`id = 1`) zarzadzanej z `Ustawienia > Integracja InPost`. +- Tabela ustawien specyficznych InPost ShipX (`id = 1`); token API utrzymywany bazowo w `integrations.api_key_encrypted`. - Kolumny: - `id` (PK, tinyint unsigned), + - `integration_id` (int unsigned, UNIQUE, FK -> `integrations.id`), - `api_token_encrypted` (text, nullable), - `organization_id` (varchar 50, nullable), - `environment` (enum: sandbox, production), diff --git a/DOCS/TECH_CHANGELOG.md b/DOCS/TECH_CHANGELOG.md index 2b9e54b..0ec5092 100644 --- a/DOCS/TECH_CHANGELOG.md +++ b/DOCS/TECH_CHANGELOG.md @@ -1,5 +1,148 @@ # Tech Changelog +## 2026-03-08 +- Dodano zakladke `Formy dostawy` dla integracji `shopPRO` (analogicznie do Allegro): + - nowy endpoint `POST /settings/integrations/shoppro/delivery/save`, + - mapowanie per instancja: forma dostawy shopPRO -> usluga dostawy Allegro/InPost WZA, + - UI z wyborem przewoznika (`allegro`/`inpost`) i wyszukiwaniem uslug Allegro. +- Dodano `ShopproDeliveryMethodMappingRepository` i tabele mapowan per `integration_id`. +- Dodano migracje `20260308_000043_create_shoppro_delivery_method_mappings_table.sql`. +- Dodano synchronizacje platnosci shopPRO oparta o flage `paid`: + - nowy job cron `shoppro_payment_status_sync`, + - nowy handler `App\Modules\Cron\ShopproPaymentStatusSyncHandler`, + - nowy serwis `App\Modules\Settings\ShopproPaymentStatusSyncService`, + - runner podlaczony w `App\Core\Application::maybeRunCronOnWeb(...)` oraz `bin/cron.php`. +- Rozszerzono `Ustawienia > Integracje > shopPRO > Ustawienia`: + - dodano interwal sprawdzania platnosci (minuty) dla joba `shoppro_payment_status_sync`, + - dodano wybor statusow orderPRO, dla ktorych cron ma sprawdzac oplacenie zamowien, + - zapis konfiguracji listy statusow per instancja trafia do `integrations.payment_sync_status_codes_json`. +- Import zamowien shopPRO zapisuje status platnosci numerycznie (`orders.payment_status`) na podstawie flagi `paid`, co unifikuje filtry/statystyki platnosci z Allegro. +- Dodano migracje `20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql`: + - uzupelnienie kolumny `integrations.payment_sync_status_codes_json`, + - seed/naprawa harmonogramu `shoppro_payment_status_sync` (domyslnie 600s, priorytet 105). +- Fix danych wysylki dla zamowien shopPRO (np. `OP000000016`): + - `ShopproOrdersSyncService` mapuje `inpost_paczkomat`/`orlen_point` do adresu `delivery` (punkt, ulica, kod, miasto), + - zapisuje `parcel_external_id` i `parcel_name` dla punktu odbioru, + - `delivery` dziedziczy telefon i e-mail klienta, gdy API nie zwraca osobnych danych odbiorcy, + - etykieta metody dostawy (`external_carrier_id`) zawiera koszt transportu (`transport_cost`), np. `Paczkomaty InPost - przedpłata: 13.5 zł`. +- Fix importu shopPRO dla listy zamowien (`Kwoty` + miniatury): + - `ShopproOrdersSyncService` mapuje kwoty zamowienia z `summary` i `paid` (fallback), ceny pozycji z `price_brutto`, + - poprawiono laczenie payloadow `orders/list` i `orders/get|details` (zachowanie kluczowych pol z listy), + - dodano fallback miniatur pozycji przez API `products/get` po `product_id`, + - dodano dodatkowy fallback miniatur po `parent_product_id` (warianty), gdy obraz nie istnieje na produkcie potomnym. +- `OrdersRepository`: + - resolver miniatur pozycji uwzglednia kod kanalu zgodny ze zrodlem zamowienia (`o.source`) zamiast stalego `allegro`. +- Korekta layoutu sekcji `Ustawienia` dla integracji `shopPRO`: + - wyrownano pola w siatce (`integration-settings-group__grid`) przez `align-items: start`, + - wymuszono spojna wysokosc kontrolek (`.form-control`), w tym pola `date`, + - przebudowano CSS (`public/assets/css/app.css`) dla rownego przebiegu linii i pol w obu kolumnach. +- Fix UX przycisku `Nowa integracja` w `Ustawienia > Integracje > shopPRO`: + - przycisk otwiera teraz tryb wymuszonego tworzenia (`?new=1`), + - `ShopproIntegrationsController::index(...)` nie auto-wybiera wtedy pierwszej istniejacej integracji, + - formularz tworzenia otwiera sie zawsze jako pusty. +- Poprawiono prezentacje dostawy na szczegolach zamowienia (`orders/show`): + - `Platnosc i wysylka` sanitizuje nazwe przewoznika (usuwa tagi HTML typu `...`), + - `Dane wysylki` pokazuja `parcel_name` i `parcel_external_id` (np. punkt/paczkomat Allegro), + - gdy brak adresu `delivery`, sekcja `Dane wysylki` pokazuje fallback z metody dostawy (`external_carrier_id`). +- Poprawiono import shopPRO dla formy dostawy: + - `ShopproOrdersSyncService` sanitizuje `external_carrier_id` i `order_shipments.carrier_provider_id` (usuwanie HTML + dekodowanie encji), + - rozszerzono fallbacki mapowania przewoznika (`transport`, `transport_description`, `transport_id`). +- Fix mapowania formy dostawy z shopPRO: + - `ShopproOrdersSyncService` mapuje teraz `orders.external_carrier_id` z fallbackiem na pola `transport` i `transport_description`, + - `orders.external_carrier_account_id` mapowane z `transport_id`, + - `order_shipments.carrier_provider_id` rozszerzone o fallback `transport`/`transport_description`. +- Fix importu adresow shopPRO na realnym payloadzie zamowien (`client_name`, `client_surname`, `client_email`, `client_phone`): + - `ShopproOrdersSyncService::mapAddresses(...)` mapuje teraz pola klienta w formacie flat (bez zagniezdzenia), + - usunieto przypadek tworzenia pustego adresu `delivery` tylko na bazie fallbacku e-mail, + - po wymuszonym re-sync zamowienia #13 dane zamawiajacego zapisaly sie jako `Jacek Pyziak`, `pyziak84@gmail.com`, `530755774`. +- Poprawiono mapowanie danych adresowych w imporcie zamowien `shopPRO`: + - `ShopproOrdersSyncService::mapAddresses(...)` obsluguje rozszerzony zestaw aliasow pol klienta i dostawy (`buyer/customer/client`, `billing_address`, `shipping_address`, `delivery_address`, `receiver`, warianty `first_name/last_name`, `postcode` itd.), + - adres dostawy jest zapisywany takze wtedy, gdy brak pelnej nazwy, ale istnieja inne dane adresowe, + - rozszerzono mapowanie `orders.customer_login` i `orders.external_carrier_id` o dodatkowe fallbacki z payloadu shopPRO. +- Poprawiono UX zakladki `Ustawienia` integracji `shopPRO`: + - ustawienia sa pogrupowane w sekcje `Pobieranie zamowien` oraz `Synchronizacja statusow`, + - dodano naglowki sekcji i opisy kontekstu, aby pola nie zlewaly sie wizualnie, + - dodano dedykowane style SCSS (`integration-settings-group*`) i przebudowano `public/assets/css/app.css`. +- Dodano cron synchronizacji statusow `shopPRO`: + - nowy handler `App\Modules\Cron\ShopproStatusSyncHandler`, + - nowy serwis `App\Modules\Settings\ShopproStatusSyncService`, + - nowy job `shoppro_order_status_sync` podlaczony do runnera w `App\Core\Application::maybeRunCronOnWeb(...)` i `bin/cron.php`. +- Rozszerzono `Ustawienia > Integracje > shopPRO > Ustawienia`: + - dodano wybor kierunku synchronizacji statusow (`shopPRO -> orderPRO`, `orderPRO -> shopPRO`), + - dodano pole interwalu synchronizacji statusow (minuty), + - zapis aktualizuje `integrations.order_status_sync_direction` i harmonogram `cron_schedules` dla `shoppro_order_status_sync`. +- Dodano migracje `20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql`: + - seed/naprawa harmonogramu `shoppro_order_status_sync` (domyslnie 900s, priorytet 100), + - uzupelnienie kolumny `integrations.order_status_sync_direction` jesli brak. +- Rozszerzono `ShopproOrdersSyncService` o opcje uruchomienia filtrowanego po `integration_id` i z pominięciem flagi `orders_fetch_enabled` (wykorzystane przez cron synchronizacji statusow). +- Dodano cron importu zamowien z `shopPRO`: + - nowy handler `App\Modules\Cron\ShopproOrdersImportHandler`, + - nowy serwis `App\Modules\Settings\ShopproOrdersSyncService`, + - nowy klient API `App\Modules\Settings\ShopproApiClient`, + - job `shoppro_orders_import` jest podlaczony do wykonania zarowno w `App\Core\Application::maybeRunCronOnWeb(...)`, jak i w `bin/cron.php`. +- Rozszerzono `Ustawienia > Integracje > shopPRO > Ustawienia`: + - dodano pole interwalu pobierania zamowien (minuty), + - zapis aktualizuje harmonogram `cron_schedules` dla joba `shoppro_orders_import`. +- Dodano migracje `20260308_000040_ensure_shoppro_orders_import_schedule.sql`: + - seed/naprawa harmonogramu `shoppro_orders_import` (domyslnie 300s, priorytet 90). +- Zadanie #13 z `DOCS/todo.md`: dodano stronicowanie historii w `Ustawienia > Cron`: + - `CronSettingsController` pobiera `past_page` z query i przekazuje metadane paginacji do widoku, + - `CronRepository` rozszerzono o `countPastJobs()` oraz `listPastJobs(limit, offset)`, + - widok `settings/cron.php` renderuje kontrolki paginacji dla sekcji `Historia jobow (przeszle)`. +- Rozpoczeto ujednolicanie modelu integracji na baze `integrations`: + - dodano klasy wspolne `App\Modules\Settings\IntegrationsRepository` oraz `App\Modules\Settings\IntegrationSecretCipher`, + - ograniczono duplikacje szyfrowania sekretow integracji (wspolny cipher dla repozytoriow integracji). +- Migracja `20260308_000037_unify_integrations_base_links.sql`: + - dodaje `integration_id` do `allegro_integration_settings`, `apaczka_integration_settings`, `inpost_integration_settings`, + - podpina relacje 1:1 FK do `integrations`, + - seeduje bazowe rekordy providerow (`allegro`, `apaczka`, `inpost`) i backfilluje powiazania. +- `ApaczkaIntegrationRepository`: + - klucz API zapisuje/odczytuje z `integrations.api_key_encrypted` (`type=apaczka`). +- `InpostIntegrationRepository`: + - token API zapisuje/odczytuje z `integrations.api_key_encrypted` (`type=inpost`), + - ustawienia specyficzne przewoznika pozostaja w `inpost_integration_settings`. +- `AllegroIntegrationRepository`: + - zapewnia powiazanie aktywnego srodowiska OAuth z rekordem bazowym `integrations`, + - dodana metoda `getActiveIntegrationId()` pod spojnosc domeny zamowien/sync. +- `AllegroOrderImportService` i `AllegroOrdersSyncService`: + - przestaly uzywac stalej/`null` dla `integration_id`, + - korzystaja z aktywnego `integration_id` Allegro, co eliminuje sztywne zalozenie `integration_id=1`. +- Dodano wspolny ekran `Ustawienia > Integracje`: + - nowa route `GET /settings/integrations`, + - nowa klasa `App\Modules\Settings\IntegrationsHubController`, + - nawigacja boczna prowadzi do jednego huba integracji. +- Hub integracji zawiera tabele podsumowania oraz akcje per instancja: + - przycisk `Ustawienia` w kazdym wierszu prowadzi do dedykowanego ekranu zaawansowanego providera. +- Fix UI: rozciaganie przyciskow w formularzach (`.form-actions`) przy ukladzie grid: + - ustawiono `align-items: flex-start` oraz `align-self: flex-start` dla `.form-actions .btn`, + - eliminuje pionowe rozciaganie przyciskow do wysokosci sasiednich pol formularza. +- Dodano wieloinstancyjna konfiguracje integracji `shopPRO`: + - nowe endpointy: `GET /settings/integrations/shoppro`, `POST /settings/integrations/shoppro/save`, `POST /settings/integrations/shoppro/test`, + - nowa klasa `App\Modules\Settings\ShopproIntegrationsController`, + - nowy widok `resources/views/settings/shoppro.php` (lista instancji, formularz dodawania/edycji, test polaczenia), + - hub integracji (`/settings/integrations`) zawiera wiersz `shopPRO` z przejsciem do ekranu ustawien, + - dodano pomocnicze style `.table-row-actions` dla kompaktowych akcji w tabelach. +- Ekran `shopPRO` rozbudowano o zakladki analogiczne do Allegro: + - `Integracja`, `Statusy`, `Ustawienia`, `Formy dostawy`, + - `Ustawienia` zawiera pola `Pobieraj zamowienia` i `Data startu pobierania`, + - zapis/odczyt tych pol jest realizowany przez tabele bazowa `integrations` (`orders_fetch_enabled`, `orders_fetch_start_date`). +- Wdrozone mapowanie statusow shopPRO (zakladka `Statusy`): + - nowe endpointy: `POST /settings/integrations/shoppro/statuses/sync` oraz `POST /settings/integrations/shoppro/statuses/save`, + - dodana klasa `App\Modules\Settings\ShopproStatusMappingRepository`, + - synchronizacja statusow pobiera slownik `dictionaries/statuses` z API shopPRO, + - zapis mapowan trafia do `order_status_mappings` per `integration_id` (wieloinstancyjnie). +- Poprawiono parser statusow shopPRO: + - obsluguje odpowiedzi zagniezdzone w `data`, + - obsluguje rowniez format mapy `kod => nazwa` oraz dodatkowe aliasy pol (`status_code`, `status_name`, `symbol`, `slug`). +- Dodano migracje naprawcza `20260308_000038_ensure_order_status_mappings_table.sql`: + - tworzy `order_status_mappings` jesli tabela nie istnieje (scenariusz niepelnej historii migracji na srodowisku). +- Dodano migracje naprawcza `20260308_000039_ensure_integrations_fetch_columns.sql`: + - uzupelnia w `integrations` brakujace kolumny `orders_fetch_enabled` i `orders_fetch_start_date` + dla srodowisk, gdzie tabela `integrations` zostala odtworzona pozniej niz pierwotne migracje shopPRO. +- Poprawiono UX ekranu `shopPRO`: + - przy istniejacych instancjach automatycznie wybierana jest pierwsza integracja (bez koniecznosci wracania do zakladki `Integracja`), + - dodano przełącznik instancji nad zakladkami (`Wybrana integracja`) dostepny globalnie dla `Statusy/Ustawienia/Formy dostawy`. + ## 2026-03-06 - Fix: synchronizacja statusow Allegro nie aktualizowala zamowien. - Przyczyna: Allegro API nie zmienia `updatedAt` przy zmianie `fulfillment.status`. diff --git a/DOCS/todo.md b/DOCS/todo.md index d15a36f..e73e47b 100644 --- a/DOCS/todo.md +++ b/DOCS/todo.md @@ -1,15 +1,17 @@ -1. [x] Na liĹ›cie zamĂłwieĹ„ powiÄ™kszenie zdjÄ™cia produktu na hover nie na onclick, wtedy to nie moĹĽe być modal zamykany X -2. [x] Dodać rejestrację historii zamówień, i zmiana statusu rejestrowana w Historii zmian zamĂłwienia -3. [x] Pobranie zamĂłwienia rejestrowane w histori zmian zamĂłwienia -4. [x] Przy imporcie zamĂłwieĹ„ musi być pobierania forma wysyĹ‚ki. -5. [x] W szczegĂłlach zamĂłwienia dorobić opcjÄ™ zmiany statusu. -6. [x] W szczególach zamówienia 2 razy wyświetla się ID zamówienai z allegro, np: 008d3d60-1743-11f1-b15c-fdb4f87ccfc6 -7. [x] Przy imporcie z allegro liczba przesyłek jest 0. -8. [x] Kolumna LP w szczególach zamówienia jest zbyt szeroka. -9. [x] Na lisćie zamówień pole po którym jest domyślnie sortowana czyli data zamówienia jest puste. -10. [x] Na liście zamówień ukryć kolumnę ostatnia zmiana. -11. [x] W ustawieniach dodać zakładkę Integracja Apaczka. Dodać tam pierwsze ustawienie, czyli klucz API. -12. [] synchronizować ręczną zmianę statusu z allegro -13. [] W ustawieniach cron https://orderpro.projectpro.pl/settings/cron historia powinna mieć stronicowanie -14. [] border inputów, select, textarea, itd zrób troszkę ciemniejszy - +1. [x] Na liście zamówień powiększenie zdjęcia produktu na hover nie na onclick, wtedy to nie może być modal zamykany X +2. [x] Doda� rejestracj� historii zam�wie�, i zmiana statusu rejestrowana w Historii zmian zamówienia +3. [x] Pobranie zamówienia rejestrowane w histori zmian zamówienia +4. [x] Przy imporcie zamówień musi być pobierania forma wysyłki. +5. [x] W szczególach zamówienia dorobić opcję zmiany statusu. +6. [x] W szczeg�lach zam�wienia 2 razy wy�wietla si� ID zam�wienai z allegro, np: 008d3d60-1743-11f1-b15c-fdb4f87ccfc6 +7. [x] Przy imporcie z allegro liczba przesy�ek jest 0. +8. [x] Kolumna LP w szczeg�lach zam�wienia jest zbyt szeroka. +9. [x] Na lis�ie zam�wie� pole po kt�rym jest domy�lnie sortowana czyli data zam�wienia jest puste. +10. [x] Na li�cie zam�wie� ukry� kolumn� ostatnia zmiana. +11. [x] W ustawieniach doda� zak�adk� Integracja Apaczka. Doda� tam pierwsze ustawienie, czyli klucz API. +12. [] synchronizowa� r�czn� zmian� statusu z allegro +13. [x] W ustawieniach cron https://orderpro.projectpro.pl/settings/cron historia powinna mie� stronicowanie +14. [] border input�w, select, textarea, itd zr�b troszk� ciemniejszy +15. [] W tym miejscu odwróć kolejność: najpierw źródło potem ID,
+16. [] Na liście zamówień statusy powinno być pokolorowane zgodnie z ustawieniami. +17. [] Na liście zamówien jak jest źródło i id zamówienia to zamiast shopPRO musi pisać która integracja konkretnie. Oraz dodajemy napis ID: ...D diff --git a/bin/cron.php b/bin/cron.php index 22f5fd4..6397d74 100644 --- a/bin/cron.php +++ b/bin/cron.php @@ -7,6 +7,9 @@ use App\Modules\Cron\AllegroStatusSyncHandler; use App\Modules\Cron\AllegroTokenRefreshHandler; use App\Modules\Cron\CronRepository; use App\Modules\Cron\CronRunner; +use App\Modules\Cron\ShopproOrdersImportHandler; +use App\Modules\Cron\ShopproPaymentStatusSyncHandler; +use App\Modules\Cron\ShopproStatusSyncHandler; use App\Modules\Orders\OrderImportRepository; use App\Modules\Orders\OrdersRepository; use App\Modules\Settings\AllegroApiClient; @@ -17,6 +20,12 @@ use App\Modules\Settings\AllegroOrderSyncStateRepository; use App\Modules\Settings\AllegroOAuthClient; use App\Modules\Settings\AllegroStatusSyncService; use App\Modules\Settings\AllegroStatusMappingRepository; +use App\Modules\Settings\ShopproApiClient; +use App\Modules\Settings\ShopproIntegrationsRepository; +use App\Modules\Settings\ShopproOrdersSyncService; +use App\Modules\Settings\ShopproPaymentStatusSyncService; +use App\Modules\Settings\ShopproStatusSyncService; +use App\Modules\Settings\ShopproStatusMappingRepository; /** @var Application $app */ $app = require dirname(__DIR__) . '/bootstrap/app.php'; @@ -51,6 +60,33 @@ $ordersSyncService = new AllegroOrdersSyncService( $apiClient, $orderImportService ); +$shopproSyncService = new ShopproOrdersSyncService( + new ShopproIntegrationsRepository( + $app->db(), + (string) $app->config('app.integrations.secret', '') + ), + new AllegroOrderSyncStateRepository($app->db()), + new ShopproApiClient(), + new OrderImportRepository($app->db()), + new ShopproStatusMappingRepository($app->db()), + new OrdersRepository($app->db()) +); +$shopproStatusSyncService = new ShopproStatusSyncService( + new ShopproIntegrationsRepository( + $app->db(), + (string) $app->config('app.integrations.secret', '') + ), + $shopproSyncService +); +$shopproPaymentSyncService = new ShopproPaymentStatusSyncService( + new ShopproIntegrationsRepository( + $app->db(), + (string) $app->config('app.integrations.secret', '') + ), + new ShopproApiClient(), + new OrdersRepository($app->db()), + $app->db() +); $runner = new CronRunner( $cronRepository, @@ -66,9 +102,19 @@ $runner = new CronRunner( 'allegro_status_sync' => new AllegroStatusSyncHandler( new AllegroStatusSyncService( $cronRepository, - $ordersSyncService + $orderImportService, + $app->db() ) ), + 'shoppro_orders_import' => new ShopproOrdersImportHandler( + $shopproSyncService + ), + 'shoppro_order_status_sync' => new ShopproStatusSyncHandler( + $shopproStatusSyncService + ), + 'shoppro_payment_status_sync' => new ShopproPaymentStatusSyncHandler( + $shopproPaymentSyncService + ), ] ); diff --git a/database/migrations/20260308_000037_unify_integrations_base_links.sql b/database/migrations/20260308_000037_unify_integrations_base_links.sql new file mode 100644 index 0000000..004a134 --- /dev/null +++ b/database/migrations/20260308_000037_unify_integrations_base_links.sql @@ -0,0 +1,175 @@ +CREATE TABLE IF NOT EXISTS integrations ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + type VARCHAR(32) NOT NULL, + name VARCHAR(128) NOT NULL, + base_url VARCHAR(255) NOT NULL, + api_key_encrypted TEXT NULL, + timeout_seconds SMALLINT UNSIGNED NOT NULL DEFAULT 10, + is_active TINYINT(1) NOT NULL DEFAULT 1, + last_test_status VARCHAR(16) NULL, + last_test_http_code SMALLINT UNSIGNED NULL, + last_test_message VARCHAR(255) NULL, + last_test_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY integrations_type_name_unique (type, name), + KEY integrations_type_active_idx (type, is_active), + KEY integrations_last_test_at_idx (last_test_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS integration_test_logs ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + integration_id INT UNSIGNED NOT NULL, + status VARCHAR(16) NOT NULL, + http_code SMALLINT UNSIGNED NULL, + message VARCHAR(255) NOT NULL, + endpoint_url VARCHAR(255) NULL, + tested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY integration_test_logs_integration_idx (integration_id), + KEY integration_test_logs_tested_at_idx (tested_at), + CONSTRAINT integration_test_logs_integration_fk + FOREIGN KEY (integration_id) REFERENCES integrations(id) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +ALTER TABLE `allegro_integration_settings` + ADD COLUMN IF NOT EXISTS `integration_id` INT UNSIGNED NULL AFTER `id`; + +ALTER TABLE `apaczka_integration_settings` + ADD COLUMN IF NOT EXISTS `integration_id` INT UNSIGNED NULL AFTER `id`; + +ALTER TABLE `inpost_integration_settings` + ADD COLUMN IF NOT EXISTS `integration_id` INT UNSIGNED NULL AFTER `id`; + +INSERT INTO `integrations` (`type`, `name`, `base_url`, `timeout_seconds`, `is_active`, `created_at`, `updated_at`) +VALUES + ('allegro', 'Allegro Sandbox', 'https://api.allegro.pl.allegrosandbox.pl', 20, 1, NOW(), NOW()), + ('allegro', 'Allegro Production', 'https://api.allegro.pl', 20, 1, NOW(), NOW()), + ('apaczka', 'Apaczka', 'https://www.apaczka.pl', 20, 1, NOW(), NOW()), + ('inpost', 'InPost ShipX', 'https://api-shipx-pl.easypack24.net', 20, 1, NOW(), NOW()) +ON DUPLICATE KEY UPDATE + `updated_at` = VALUES(`updated_at`); + +UPDATE `allegro_integration_settings` `s` +INNER JOIN `integrations` `i` + ON `i`.`type` = 'allegro' + AND `i`.`name` = CASE + WHEN `s`.`environment` = 'production' THEN 'Allegro Production' + ELSE 'Allegro Sandbox' + END +SET `s`.`integration_id` = `i`.`id` +WHERE `s`.`integration_id` IS NULL OR `s`.`integration_id` = 0; + +UPDATE `apaczka_integration_settings` `s` +INNER JOIN `integrations` `i` + ON `i`.`type` = 'apaczka' + AND `i`.`name` = 'Apaczka' +SET `s`.`integration_id` = `i`.`id` +WHERE `s`.`id` = 1 + AND (`s`.`integration_id` IS NULL OR `s`.`integration_id` = 0); + +UPDATE `inpost_integration_settings` `s` +INNER JOIN `integrations` `i` + ON `i`.`type` = 'inpost' + AND `i`.`name` = 'InPost ShipX' +SET `s`.`integration_id` = `i`.`id` +WHERE `s`.`id` = 1 + AND (`s`.`integration_id` IS NULL OR `s`.`integration_id` = 0); + +UPDATE `integrations` `i` +INNER JOIN `apaczka_integration_settings` `s` + ON `s`.`integration_id` = `i`.`id` +SET `i`.`api_key_encrypted` = `s`.`api_key_encrypted`, + `i`.`updated_at` = NOW() +WHERE `i`.`type` = 'apaczka' + AND (`i`.`api_key_encrypted` IS NULL OR `i`.`api_key_encrypted` = '') + AND `s`.`api_key_encrypted` IS NOT NULL + AND `s`.`api_key_encrypted` <> ''; + +UPDATE `integrations` `i` +INNER JOIN `inpost_integration_settings` `s` + ON `s`.`integration_id` = `i`.`id` +SET `i`.`api_key_encrypted` = `s`.`api_token_encrypted`, + `i`.`updated_at` = NOW() +WHERE `i`.`type` = 'inpost' + AND (`i`.`api_key_encrypted` IS NULL OR `i`.`api_key_encrypted` = '') + AND `s`.`api_token_encrypted` IS NOT NULL + AND `s`.`api_token_encrypted` <> ''; + +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `allegro_integration_settings` ADD UNIQUE KEY `allegro_integration_settings_integration_unique` (`integration_id`)', + 'SELECT 1') + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'allegro_integration_settings' + AND index_name = 'allegro_integration_settings_integration_unique' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `apaczka_integration_settings` ADD UNIQUE KEY `apaczka_integration_settings_integration_unique` (`integration_id`)', + 'SELECT 1') + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'apaczka_integration_settings' + AND index_name = 'apaczka_integration_settings_integration_unique' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `inpost_integration_settings` ADD UNIQUE KEY `inpost_integration_settings_integration_unique` (`integration_id`)', + 'SELECT 1') + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'inpost_integration_settings' + AND index_name = 'inpost_integration_settings_integration_unique' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `allegro_integration_settings` ADD CONSTRAINT `allegro_integration_settings_integration_fk` FOREIGN KEY (`integration_id`) REFERENCES `integrations`(`id`) ON DELETE CASCADE ON UPDATE CASCADE', + 'SELECT 1') + FROM information_schema.table_constraints + WHERE constraint_schema = DATABASE() + AND table_name = 'allegro_integration_settings' + AND constraint_name = 'allegro_integration_settings_integration_fk' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `apaczka_integration_settings` ADD CONSTRAINT `apaczka_integration_settings_integration_fk` FOREIGN KEY (`integration_id`) REFERENCES `integrations`(`id`) ON DELETE CASCADE ON UPDATE CASCADE', + 'SELECT 1') + FROM information_schema.table_constraints + WHERE constraint_schema = DATABASE() + AND table_name = 'apaczka_integration_settings' + AND constraint_name = 'apaczka_integration_settings_integration_fk' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `inpost_integration_settings` ADD CONSTRAINT `inpost_integration_settings_integration_fk` FOREIGN KEY (`integration_id`) REFERENCES `integrations`(`id`) ON DELETE CASCADE ON UPDATE CASCADE', + 'SELECT 1') + FROM information_schema.table_constraints + WHERE constraint_schema = DATABASE() + AND table_name = 'inpost_integration_settings' + AND constraint_name = 'inpost_integration_settings_integration_fk' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/database/migrations/20260308_000038_ensure_order_status_mappings_table.sql b/database/migrations/20260308_000038_ensure_order_status_mappings_table.sql new file mode 100644 index 0000000..cef3299 --- /dev/null +++ b/database/migrations/20260308_000038_ensure_order_status_mappings_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS order_status_mappings ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + integration_id INT UNSIGNED NOT NULL, + shoppro_status_code VARCHAR(64) NOT NULL, + shoppro_status_name VARCHAR(128) NULL, + orderpro_status_code VARCHAR(64) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY order_status_mappings_integration_shoppro_unique (integration_id, shoppro_status_code), + KEY order_status_mappings_integration_idx (integration_id), + KEY order_status_mappings_orderpro_idx (orderpro_status_code), + CONSTRAINT order_status_mappings_integration_fk + FOREIGN KEY (integration_id) REFERENCES integrations(id) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/database/migrations/20260308_000039_ensure_integrations_fetch_columns.sql b/database/migrations/20260308_000039_ensure_integrations_fetch_columns.sql new file mode 100644 index 0000000..c6ad2e1 --- /dev/null +++ b/database/migrations/20260308_000039_ensure_integrations_fetch_columns.sql @@ -0,0 +1,25 @@ +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `integrations` ADD COLUMN `orders_fetch_enabled` TINYINT(1) NOT NULL DEFAULT 0 AFTER `is_active`', + 'SELECT 1') + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'integrations' + AND column_name = 'orders_fetch_enabled' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `integrations` ADD COLUMN `orders_fetch_start_date` DATE NULL AFTER `orders_fetch_enabled`', + 'SELECT 1') + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'integrations' + AND column_name = 'orders_fetch_start_date' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/database/migrations/20260308_000040_ensure_shoppro_orders_import_schedule.sql b/database/migrations/20260308_000040_ensure_shoppro_orders_import_schedule.sql new file mode 100644 index 0000000..65e9559 --- /dev/null +++ b/database/migrations/20260308_000040_ensure_shoppro_orders_import_schedule.sql @@ -0,0 +1,19 @@ +INSERT INTO cron_schedules ( + job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at +) VALUES ( + 'shoppro_orders_import', + 300, + 90, + 3, + NULL, + 1, + NULL, + NOW(), + NOW(), + NOW() +) +ON DUPLICATE KEY UPDATE + interval_seconds = IFNULL(interval_seconds, VALUES(interval_seconds)), + priority = IFNULL(priority, VALUES(priority)), + max_attempts = IFNULL(max_attempts, VALUES(max_attempts)), + updated_at = VALUES(updated_at); diff --git a/database/migrations/20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql b/database/migrations/20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql new file mode 100644 index 0000000..9c63116 --- /dev/null +++ b/database/migrations/20260308_000041_ensure_shoppro_status_sync_schedule_and_direction.sql @@ -0,0 +1,32 @@ +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `integrations` ADD COLUMN `order_status_sync_direction` VARCHAR(32) NOT NULL DEFAULT ''shoppro_to_orderpro'' AFTER `orders_fetch_start_date`', + 'SELECT 1') + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'integrations' + AND column_name = 'order_status_sync_direction' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +INSERT INTO cron_schedules ( + job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at +) VALUES ( + 'shoppro_order_status_sync', + 900, + 100, + 3, + NULL, + 1, + NULL, + NOW(), + NOW(), + NOW() +) +ON DUPLICATE KEY UPDATE + interval_seconds = IFNULL(interval_seconds, VALUES(interval_seconds)), + priority = IFNULL(priority, VALUES(priority)), + max_attempts = IFNULL(max_attempts, VALUES(max_attempts)), + updated_at = VALUES(updated_at); diff --git a/database/migrations/20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql b/database/migrations/20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql new file mode 100644 index 0000000..07768f0 --- /dev/null +++ b/database/migrations/20260308_000042_ensure_shoppro_payment_sync_schedule_and_columns.sql @@ -0,0 +1,32 @@ +SET @sql := ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `integrations` ADD COLUMN `payment_sync_status_codes_json` JSON NULL AFTER `order_status_sync_direction`', + 'SELECT 1') + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'integrations' + AND column_name = 'payment_sync_status_codes_json' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +INSERT INTO cron_schedules ( + job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at +) VALUES ( + 'shoppro_payment_status_sync', + 600, + 105, + 3, + NULL, + 1, + NULL, + NOW(), + NOW(), + NOW() +) +ON DUPLICATE KEY UPDATE + interval_seconds = IFNULL(interval_seconds, VALUES(interval_seconds)), + priority = IFNULL(priority, VALUES(priority)), + max_attempts = IFNULL(max_attempts, VALUES(max_attempts)), + updated_at = VALUES(updated_at); diff --git a/database/migrations/20260308_000043_create_shoppro_delivery_method_mappings_table.sql b/database/migrations/20260308_000043_create_shoppro_delivery_method_mappings_table.sql new file mode 100644 index 0000000..ba89a2c --- /dev/null +++ b/database/migrations/20260308_000043_create_shoppro_delivery_method_mappings_table.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS shoppro_delivery_method_mappings ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + integration_id INT UNSIGNED NOT NULL, + order_delivery_method VARCHAR(200) NOT NULL, + carrier VARCHAR(50) NOT NULL DEFAULT 'allegro', + allegro_delivery_method_id VARCHAR(128) NOT NULL, + allegro_credentials_id VARCHAR(128) NULL, + allegro_carrier_id VARCHAR(128) NULL, + allegro_service_name VARCHAR(255) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY shoppro_dm_mapping_unique (integration_id, order_delivery_method), + KEY shoppro_dm_mapping_integration_idx (integration_id), + CONSTRAINT shoppro_dm_mapping_integration_fk + FOREIGN KEY (integration_id) REFERENCES integrations(id) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/public/assets/css/app.css b/public/assets/css/app.css index b65572b..c3f67d5 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1,2291 +1 @@ -@charset "UTF-8"; -:root { - --c-primary: #6690f4; - --c-primary-dark: #3164db; - --c-bg: #f4f6f9; - --c-surface: #ffffff; - --c-text: #4e5e6a; - --c-text-strong: #2d3748; - --c-muted: #718096; - --c-border: #e2e8f0; - --c-danger: #cc0000; - --focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15); - --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 transparent; - border-radius: 8px; - font: inherit; - font-weight: 600; - text-decoration: none; - cursor: pointer; - transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.1s ease; -} - -.btn--primary { - color: #ffffff; - background: var(--c-primary); -} - -.btn--primary:hover { - background: var(--c-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: #ffffff; - 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:active { - transform: translateY(1px); -} - -.btn:focus-visible { - outline: none; - box-shadow: var(--focus-ring); - border-color: var(--c-primary); -} - -.form-control { - width: 100%; - min-height: 34px; - border: 1px solid var(--c-border); - border-radius: 8px; - padding: 5px 10px; - font: inherit; - color: var(--c-text-strong); - background: #ffffff; - transition: border-color 0.2s ease, box-shadow 0.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: #ffffff; -} - -.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 transparent; - 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; -} - -* { - 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 0.22s ease, min-width 0.22s ease; - border-right: 1px solid #243041; - background: #111a28; - padding: 18px 10px; - display: flex; - flex-direction: column; -} - -.sidebar.is-collapsed { - width: 52px; - min-width: 52px; -} - -.sidebar__brand { - display: flex; - align-items: center; - justify-content: space-between; - margin: 4px 4px 16px; - gap: 6px; - min-width: 0; -} - -.sidebar__brand-text { - color: #e9f0ff; - font-size: 24px; - font-weight: 300; - letter-spacing: -0.02em; - white-space: nowrap; - overflow: hidden; - flex: 1; - min-width: 0; -} -.sidebar__brand-text strong { - font-weight: 700; -} - -.sidebar__collapse-btn { - flex-shrink: 0; - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - background: transparent; - border: 1px solid #2a3a54; - border-radius: 6px; - color: #64748b; - cursor: pointer; - padding: 0; - transition: background 0.15s, color 0.15s; -} -.sidebar__collapse-btn:hover { - background: #1b2a3f; - color: #cbd5e1; -} - -.sidebar__collapse-icon { - display: block; - transition: transform 0.22s ease; - flex-shrink: 0; -} - -.sidebar.is-collapsed .sidebar__collapse-icon { - transform: rotate(180deg); -} - -.sidebar__nav { - display: grid; - gap: 4px; -} - -.sidebar__link { - border-radius: 8px; - padding: 10px 12px; - text-decoration: none; - color: #cbd5e1; - font-weight: 600; -} - -.sidebar__link:hover { - color: #f8fafc; - background: #1b2a3f; -} - -.sidebar__link.is-active { - color: #ffffff; - 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: #ffffff; - background: #2e4f93; -} - -.sidebar__icon { - flex-shrink: 0; - width: 18px; - height: 18px; - display: flex; - align-items: center; - justify-content: center; - opacity: 0.85; -} - -.sidebar__label { - flex: 1; - min-width: 0; - overflow: hidden; -} - -.sidebar__toggle-arrow { - flex-shrink: 0; - margin-left: auto; - opacity: 0.5; - transition: transform 0.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, 0.3); - transition: background 0.15s; -} -.sidebar__sublink:hover { - color: #e2e8f0; - background: #1b2a3f; -} -.sidebar__sublink:hover::before { - background: rgba(148, 163, 184, 0.65); -} -.sidebar__sublink.is-active { - color: #ffffff; - background: rgba(46, 79, 147, 0.55); -} -.sidebar__sublink.is-active::before { - background: #93c5fd; -} - -.app-main { - flex: 1; - min-width: 0; -} - -.topbar { - height: 50px; - border-bottom: 1px solid var(--c-border); - background: var(--c-surface); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 20px; - position: sticky; - top: 0; - z-index: 100; -} - -.brand { - font-size: 22px; - font-weight: 300; - letter-spacing: -0.02em; - color: var(--c-text-strong); -} - -.brand strong { - font-weight: 700; -} - -.container { - max-width: none; - width: calc(100% - 20px); - margin: 12px 10px; - padding: 0 4px 14px; -} - -.card { - background: var(--c-surface); - border-radius: 10px; - box-shadow: var(--shadow-card); - padding: 14px; -} - -.card h1 { - margin: 0 0 10px; - color: var(--c-text-strong); - font-size: 24px; - font-weight: 700; -} - -.muted { - color: var(--c-muted); -} - -.accent { - color: var(--c-primary); - font-weight: 600; -} - -.users-form { - display: grid; - gap: 14px; - max-width: 460px; -} - -.section-title { - margin: 0; - color: var(--c-text-strong); - font-size: 18px; - font-weight: 700; -} - -.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: end; - 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; -} - -.form-grid-3 { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.form-grid-4 { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; -} - -.form-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.statuses-form { - display: grid; - gap: 8px; - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.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, 0.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: 0.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, 0.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, 0.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-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)); -} - -.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, 0.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 0.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 0.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, 0.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: 0.03em; -} -.orders-page .table tbody td { - vertical-align: middle; - padding-top: 10px; - padding-bottom: 10px; - border-bottom-color: #edf2f8; -} -.orders-page .table tbody tr:hover td { - background: #f9fcff; -} - -.orders-list-page { - padding: 10px; - margin-bottom: 10px; -} - -.orders-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; -} - -.orders-stats { - display: inline-grid; - grid-template-columns: repeat(3, minmax(86px, auto)); - gap: 8px; -} - -.orders-stat { - border: 1px solid #d8e2f0; - background: #f8fbff; - border-radius: 8px; - padding: 6px 8px; - line-height: 1.15; -} -.orders-stat__label { - display: block; - color: #5f6f83; - font-size: 11px; - margin-bottom: 2px; -} -.orders-stat__value { - color: #12233a; - font-size: 16px; - font-weight: 700; -} - -.orders-ref { - display: grid; - gap: 2px; - min-width: 170px; -} -.orders-ref__main { - font-weight: 700; - color: #0f1f35; - font-size: 14px; -} -.orders-ref__meta { - display: inline-flex; - flex-wrap: wrap; - gap: 4px 10px; - color: #64748b; - font-size: 12px; -} - -.orders-buyer { - display: grid; - gap: 2px; -} -.orders-buyer__name { - color: #0f172a; - font-weight: 600; - font-size: 14px; -} -.orders-buyer__meta { - display: inline-flex; - flex-wrap: wrap; - gap: 4px 10px; - color: #64748b; - font-size: 12px; -} - -.orders-status-wrap { - display: inline-flex; - align-items: center; - gap: 5px; - flex-wrap: wrap; -} - -.order-tag { - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid #d8e1ef; - background: #f8fafc; - color: #334155; - border-radius: 999px; - padding: 2px 8px; - font-size: 12px; - font-weight: 700; - line-height: 1.1; -} -.order-tag.is-info { - border-color: #bfdbfe; - background: #eff6ff; - color: #1d4ed8; -} -.order-tag.is-success { - border-color: #bbf7d0; - background: #f0fdf4; - color: #166534; -} -.order-tag.is-danger { - border-color: #fecaca; - background: #fef2f2; - color: #b91c1c; -} -.order-tag.is-warn { - border-color: #fde68a; - background: #fffbeb; - color: #92400e; -} -.order-tag.is-cod { - border-color: #f9a8d4; - background: #fdf2f8; - color: #9d174d; -} -.order-tag.is-unpaid { - border-color: #fca5a5; - background: #fef2f2; - color: #b91c1c; -} - -.orders-mini { - font-size: 14px; - color: #223247; - line-height: 1.25; -} -.orders-mini__delivery { - font-size: 12px; - color: #64748b; - margin-bottom: 2px; - max-width: 160px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.orders-products { - display: grid; - gap: 4px; - min-width: 240px; -} -.orders-products__meta, .orders-products__more { - font-size: 12px; - color: #64748b; -} - -.orders-product { - display: grid; - grid-template-columns: 48px 1fr; - gap: 6px; - align-items: center; -} -.orders-product__thumb { - width: 48px; - height: 48px; - border-radius: 4px; - border: 1px solid #dbe3ef; - object-fit: cover; - background: #fff; -} -.orders-product__thumb--empty { - display: inline-block; - background: #eef2f7; - border-style: dashed; -} -.orders-product__txt { - min-width: 0; - display: grid; - gap: 1px; -} -.orders-product__name { - font-size: 14px; - color: #0f172a; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.orders-product__qty { - font-size: 12px; - color: #64748b; -} - -.orders-image-hover-wrap { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: zoom-in; -} - -.orders-image-hover-popup { - display: none; - position: fixed; - left: auto; - top: auto; - width: 350px; - max-height: 350px; - object-fit: contain; - border-radius: 8px; - background: #fff; - box-shadow: 0 8px 24px rgba(0, 0, 0, 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: 0.02em; - white-space: nowrap; -} -.table-list[data-table-list-id=orders] .table tbody td { - vertical-align: top; - font-size: 14px; - line-height: 1.25; -} - -.order-show-layout { - display: grid; - grid-template-columns: 220px minmax(0, 1fr); - gap: 12px; - align-items: start; -} - -.order-statuses-side { - position: sticky; - top: 60px; - padding: 10px; -} -.order-statuses-side__title { - font-size: 13px; - font-weight: 700; - color: #0f172a; - margin-bottom: 8px; -} - -.order-status-group { - margin-bottom: 10px; -} -.order-status-group__name { - font-size: 12px; - color: #475569; - font-weight: 700; - margin-bottom: 5px; -} - -.order-status-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 4px 6px; - border-radius: 6px; - color: #334155; - font-size: 12px; - text-decoration: none; -} -.order-status-row__count { - min-width: 24px; - text-align: center; - border-radius: 999px; - background: var(--status-color, #64748b); - padding: 1px 6px; - font-weight: 700; - font-size: 11px; - color: #ffffff; -} - -.order-status-row:hover { - background: #f1f5f9; -} - -.order-status-row.is-active { - background: rgba(15, 23, 42, 0.06); - color: #0f172a; - font-weight: 700; -} - -.order-show-main { - min-width: 0; -} - -.order-details-actions { - display: inline-flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 6px; -} - -.order-details-page { - padding: 12px; -} - -.order-details-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; -} - -.order-back-link { - color: #475569; - text-decoration: none; - font-weight: 600; -} - -.order-back-link:hover { - color: #1d4ed8; -} - -.order-details-sub { - display: inline-flex; - gap: 10px; - flex-wrap: wrap; - color: #64748b; - font-size: 12px; -} - -.order-details-pill { - border-radius: 999px; - padding: 5px 10px; - background: #eef6ff; - border: 1px solid #cfe2ff; - color: #1d4ed8; - font-size: 12px; - font-weight: 700; -} - -.order-status-change { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; -} - -.order-status-change__form { - display: flex; - align-items: center; - gap: 6px; -} - -.order-status-change__select { - min-width: 180px; -} - -.order-details-tabs { - display: flex; - gap: 6px; - flex-wrap: wrap; -} - -.order-details-tab { - border: 1px solid #d6deea; - border-radius: 8px; - padding: 5px 10px; - color: #475569; - font-size: 12px; - background: #f8fafc; - cursor: pointer; -} - -.order-details-tab.is-active { - border-color: #bfdbfe; - color: #1d4ed8; - background: #eff6ff; - font-weight: 700; -} - -.order-item-cell { - display: grid; - grid-template-columns: 44px 1fr; - gap: 8px; - align-items: center; - min-width: 260px; -} - -.order-item-thumb { - width: 44px; - height: 44px; - border-radius: 6px; - border: 1px solid #dbe3ef; - object-fit: cover; -} - -.order-item-thumb--empty { - display: inline-block; - background: #eef2f7; - border-style: dashed; -} - -.order-item-name { - font-weight: 600; - color: #0f172a; -} - -.order-grid-2 { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.order-grid-3 { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.order-kv { - margin: 0; - display: grid; - grid-template-columns: 150px 1fr; - gap: 6px 10px; - font-size: 12px; -} - -.payment-summary { - display: grid; - gap: 6px; - max-width: 420px; -} - -.payment-summary__row { - display: flex; - align-items: center; - gap: 10px; - font-size: 12px; -} - -.payment-summary__label { - width: 150px; - flex-shrink: 0; - color: #64748b; -} - -.payment-summary__value { - font-weight: 600; - color: #0f172a; -} - -.order-kv dt { - color: #64748b; -} - -.order-kv dd { - margin: 0; - color: #0f172a; - font-weight: 600; -} - -.order-address { - display: grid; - gap: 3px; - font-size: 12px; - color: #0f172a; -} - -.order-events { - display: grid; - gap: 8px; -} - -.order-event { - border: 1px solid #e2e8f0; - border-radius: 8px; - padding: 8px; - background: #fbfdff; -} - -.order-event__head { - color: #64748b; - font-size: 11px; -} - -.order-event__body { - margin-top: 4px; - color: #0f172a; - font-size: 12px; -} - -.order-tab-panel { - display: none; -} - -.order-tab-panel.is-active { - display: block; -} - -.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: transparent; - 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 transparent; - border-right: 4px solid transparent; - 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, 0.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 transparent; - margin-bottom: -2px; - border-radius: 4px 4px 0 0; - transition: color 0.15s, border-color 0.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; -} - -@media (max-width: 768px) { - .app-shell { - flex-direction: column; - } - .sidebar { - width: 100% !important; - min-width: 0 !important; - border-right: 0; - border-bottom: 1px solid #243041; - padding: 14px; - overflow-x: auto; - } - .sidebar__brand { - margin: 0 0 10px; - font-size: 22px; - } - .sidebar__collapse-btn { - display: none; - } - .sidebar__nav { - display: flex; - gap: 8px; - overflow-x: auto; - } - .sidebar__link { - white-space: nowrap; - } - .topbar { - padding: 0 14px; - } - .container { - margin-top: 16px; - width: calc(100% - 16px); - margin-left: 8px; - margin-right: 8px; - padding: 0 3px 12px; - } - .settings-grid { - grid-template-columns: 1fr; - } - .page-head { - flex-direction: column; - align-items: flex-start; - } - .orders-stats { - grid-template-columns: 1fr; - width: 100%; - } - .order-show-layout { - grid-template-columns: 1fr; - } - .order-statuses-side { - position: static; - top: auto; - } - .order-details-actions { - justify-content: flex-start; - } - .order-grid-2, - .order-grid-3 { - grid-template-columns: 1fr; - } - .order-kv { - grid-template-columns: 1fr; - gap: 2px; - } - .filters-grid, - .form-grid, - .form-grid-2, - .form-grid-3, - .form-grid-4, - .shipment-grid, - .statuses-form, - .statuses-inline-form, - .table-list-filters, - .product-links-search-form, - .product-links-inline-form { - grid-template-columns: 1fr; - } - .statuses-dnd-item__content { - display: block; - } - .statuses-inline-delete { - margin-top: 6px; - } - .filters-actions { - align-items: center; - } - .table-list__header, - .table-list__footer { - align-items: flex-start; - } - .product-links-head { - grid-template-columns: 1fr; - } - .card { - padding: 12px; - } - .modal--image-preview { - width: min(92vw, 100%); - } -} +:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #e2e8f0;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--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-primary)}.btn--primary:hover{background:var(--c-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:active{transform:translateY(1px)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-color:var(--c-primary)}.form-control{width:100%;min-height:34px;border:1px solid var(--c-border);border-radius:8px;padding:5px 10px;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}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;font-size:13px;color:var(--c-text);background:var(--c-bg)}a{color:var(--c-primary)}.app-shell{min-height:100vh;display:flex}.sidebar{width:260px;min-width:260px;flex-shrink:0;overflow:hidden;transition:width .22s ease,min-width .22s ease;border-right:1px solid #243041;background:#111a28;padding:18px 10px;display:flex;flex-direction:column}.sidebar.is-collapsed{width:52px;min-width:52px}.sidebar__brand{display:flex;align-items:center;justify-content:space-between;margin:4px 4px 16px;gap:6px;min-width:0}.sidebar__brand-text{color:#e9f0ff;font-size:24px;font-weight:300;letter-spacing:-0.02em;white-space:nowrap;overflow:hidden;flex:1;min-width:0}.sidebar__brand-text strong{font-weight:700}.sidebar__collapse-btn{flex-shrink:0;width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0);border:1px solid #2a3a54;border-radius:6px;color:#64748b;cursor:pointer;padding:0;transition:background .15s,color .15s}.sidebar__collapse-btn:hover{background:#1b2a3f;color:#cbd5e1}.sidebar__collapse-icon{display:block;transition:transform .22s ease;flex-shrink:0}.sidebar.is-collapsed .sidebar__collapse-icon{transform:rotate(180deg)}.sidebar__nav{display:grid;gap:4px}.sidebar__link{border-radius:8px;padding:10px 12px;text-decoration:none;color:#cbd5e1;font-weight:600}.sidebar__link:hover{color:#f8fafc;background:#1b2a3f}.sidebar__link.is-active{color:#fff;background:#2e4f93}.sidebar__group{display:grid;gap:2px}.sidebar__group-toggle{list-style:none;border-radius:8px;padding:9px 10px;color:#cbd5e1;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:9px;white-space:nowrap;user-select:none}.sidebar__group-toggle::-webkit-details-marker{display:none}.sidebar__group:hover .sidebar__group-toggle,.sidebar__group-toggle:hover{color:#f8fafc;background:#1b2a3f}.sidebar__group.is-active .sidebar__group-toggle{color:#fff;background:#2e4f93}.sidebar__icon{flex-shrink:0;width:18px;height:18px;display:flex;align-items:center;justify-content:center;opacity:.85}.sidebar__label{flex:1;min-width:0;overflow:hidden}.sidebar__toggle-arrow{flex-shrink:0;margin-left:auto;opacity:.5;transition:transform .18s ease}details[open]>.sidebar__group-toggle .sidebar__toggle-arrow{transform:rotate(180deg)}.sidebar__group-links{display:grid;gap:2px;padding-left:12px;overflow:hidden}.sidebar__sublink{border-radius:6px;padding:7px 10px 7px 8px;text-decoration:none;color:#94a3b8;font-size:12.5px;font-weight:500;display:flex;align-items:center;gap:8px;white-space:nowrap}.sidebar__sublink::before{content:"";flex-shrink:0;width:5px;height:5px;border-radius:50%;background:rgba(148,163,184,.3);transition:background .15s}.sidebar__sublink:hover{color:#e2e8f0;background:#1b2a3f}.sidebar__sublink:hover::before{background:rgba(148,163,184,.65)}.sidebar__sublink.is-active{color:#fff;background:rgba(46,79,147,.55)}.sidebar__sublink.is-active::before{background:#93c5fd}.app-main{flex:1;min-width:0}.topbar{height:50px;border-bottom:1px solid var(--c-border);background:var(--c-surface);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100}.brand{font-size:22px;font-weight:300;letter-spacing:-0.02em;color:var(--c-text-strong)}.brand strong{font-weight:700}.container{max-width:none;width:calc(100% - 20px);margin:12px 10px;padding:0 4px 14px}.card{background:var(--c-surface);border-radius:10px;box-shadow:var(--shadow-card);padding:14px}.card h1{margin:0 0 10px;color:var(--c-text-strong);font-size:24px;font-weight:700}.muted{color:var(--c-muted)}.accent{color:var(--c-primary);font-weight:600}.users-form{display:grid;gap:14px;max-width:460px}.section-title{margin:0;color:var(--c-text-strong);font-size:18px;font-weight:700}.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:end;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}.form-grid-3{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.form-grid-4{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:12px}.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))}.table-col-toggle-wrapper{position:relative}.table-col-toggle-dropdown{display:none;position:absolute;right:0;top:calc(100% + 6px);z-index:30;width:260px;max-height:360px;overflow:auto;border:1px solid var(--c-border);border-radius:10px;background:#fff;box-shadow:0 10px 25px rgba(15,23,42,.12)}.table-col-toggle-dropdown.is-open{display:block}.table-col-toggle-header{padding:10px 12px;border-bottom:1px solid var(--c-border);font-size:12px;font-weight:700;color:var(--c-muted)}.table-col-toggle-item{display:flex;align-items:center;gap:10px;padding:8px 12px;font-size:13px;color:var(--c-text-strong)}.table-col-toggle-item:hover{background:#f8fafc}.table-col-toggle-footer{border-top:1px solid var(--c-border);padding:8px 12px}.table-col-hidden{display:none}.table-col-switch{position:relative;display:inline-block;width:34px;min-width:34px;height:18px}.table-col-switch input{opacity:0;width:0;height:0;position:absolute}.table-col-switch-slider{position:absolute;top:0;left:0;right:0;bottom:0;background:#cbd5e1;border-radius:999px;transition:background-color .2s ease}.table-col-switch-slider::before{content:"";position:absolute;height:14px;width:14px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:transform .2s ease}.table-col-switch input:checked+.table-col-switch-slider{background:#16a34a}.table-col-switch input:checked+.table-col-switch-slider::before{transform:translateX(16px)}.table-sort-link{display:inline-flex;align-items:center;gap:6px;color:var(--c-text-strong);text-decoration:none}.table-sort-link:hover{color:var(--c-primary-dark)}.table-sort-icon.is-muted{color:#a0aec0}.table-list__footer{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.table-list-per-page-form{display:inline-flex;align-items:center;gap:8px}.table-list-per-page-form .form-control{min-width:90px}.table-select-col{width:44px;text-align:center}.table-select-toggle{display:inline-flex;align-items:center;justify-content:center}.table-select-toggle input[type=checkbox]{width:16px;height:16px}.orders-page .orders-head{background:linear-gradient(120deg, #f8fbff 0%, #eef5ff 100%);border:1px solid #dbe7fb}.orders-page .table-list{border:1px solid #dde5f2;border-radius:12px;box-shadow:0 6px 16px rgba(20,44,86,.08)}.orders-page .table-list__header{padding:10px 6px 2px}.orders-page .table-list-filters{padding:6px 6px 2px;border-top:1px solid #ebf0f7;border-bottom:1px solid #ebf0f7;background:#f9fbff}.orders-page .table-wrap{border-radius:10px;overflow:hidden;border:1px solid #e7edf6}.orders-page .table thead th{background:#f3f7fd;color:#30435f;font-size:12px;text-transform:uppercase;letter-spacing:.03em}.orders-page .table tbody td{vertical-align:middle;padding-top:10px;padding-bottom:10px;border-bottom-color:#edf2f8}.orders-page .table tbody tr:hover td{background:#f9fcff}.orders-list-page{padding:10px;margin-bottom:10px}.orders-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.orders-stats{display:inline-grid;grid-template-columns:repeat(3, minmax(86px, auto));gap:8px}.orders-stat{border:1px solid #d8e2f0;background:#f8fbff;border-radius:8px;padding:6px 8px;line-height:1.15}.orders-stat__label{display:block;color:#5f6f83;font-size:11px;margin-bottom:2px}.orders-stat__value{color:#12233a;font-size:16px;font-weight:700}.orders-ref{display:grid;gap:2px;min-width:170px}.orders-ref__main{font-weight:700;color:#0f1f35;font-size:14px}.orders-ref__meta{display:inline-flex;flex-wrap:wrap;gap:4px 10px;color:#64748b;font-size:12px}.orders-buyer{display:grid;gap:2px}.orders-buyer__name{color:#0f172a;font-weight:600;font-size:14px}.orders-buyer__meta{display:inline-flex;flex-wrap:wrap;gap:4px 10px;color:#64748b;font-size:12px}.orders-status-wrap{display:inline-flex;align-items:center;gap:5px;flex-wrap:wrap}.order-tag{display:inline-flex;align-items:center;justify-content:center;border:1px solid #d8e1ef;background:#f8fafc;color:#334155;border-radius:999px;padding:2px 8px;font-size:12px;font-weight:700;line-height:1.1}.order-tag.is-info{border-color:#bfdbfe;background:#eff6ff;color:#1d4ed8}.order-tag.is-success{border-color:#bbf7d0;background:#f0fdf4;color:#166534}.order-tag.is-danger{border-color:#fecaca;background:#fef2f2;color:#b91c1c}.order-tag.is-warn{border-color:#fde68a;background:#fffbeb;color:#92400e}.order-tag.is-cod{border-color:#f9a8d4;background:#fdf2f8;color:#9d174d}.order-tag.is-unpaid{border-color:#fca5a5;background:#fef2f2;color:#b91c1c}.orders-mini{font-size:14px;color:#223247;line-height:1.25}.orders-mini__delivery{font-size:12px;color:#64748b;margin-bottom:2px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.orders-products{display:grid;gap:4px;min-width:240px}.orders-products__meta,.orders-products__more{font-size:12px;color:#64748b}.orders-product{display:grid;grid-template-columns:48px 1fr;gap:6px;align-items:center}.orders-product__thumb{width:48px;height:48px;border-radius:4px;border:1px solid #dbe3ef;object-fit:cover;background:#fff}.orders-product__thumb--empty{display:inline-block;background:#eef2f7;border-style:dashed}.orders-product__txt{min-width:0;display:grid;gap:1px}.orders-product__name{font-size:14px;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.orders-product__qty{font-size:12px;color:#64748b}.orders-image-hover-wrap{position:relative;display:inline-flex;align-items:center;justify-content:center;cursor:zoom-in}.orders-image-hover-popup{display:none;position:fixed;left:auto;top:auto;width:350px;max-height:350px;object-fit:contain;border-radius:8px;background:#fff;box-shadow:0 8px 24px rgba(0,0,0,.18);border:1px solid #dfe3ea;z-index:100;pointer-events:none}.orders-image-hover-wrap:hover .orders-image-hover-popup{display:block}.activity-type-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;white-space:nowrap;background:#e2e8f0;color:#334155}.activity-type-badge--status_change{background:#dbeafe;color:#1e40af}.activity-type-badge--payment{background:#dcfce7;color:#166534}.activity-type-badge--invoice{background:#fef3c7;color:#92400e}.activity-type-badge--shipment{background:#e0e7ff;color:#3730a3}.activity-type-badge--message{background:#f3e8ff;color:#6b21a8}.activity-type-badge--document{background:#fce7f3;color:#9d174d}.activity-type-badge--import{background:#f1f5f9;color:#475569}.activity-type-badge--note{background:#ecfdf5;color:#065f46}.text-nowrap{white-space:nowrap}.orders-money{display:grid;gap:2px}.orders-money__main{color:#0f172a;font-weight:700;font-size:14px}.orders-money__meta{color:#64748b;font-size:12px}.table-list[data-table-list-id=orders]{gap:8px}.table-list[data-table-list-id=orders] .table-list__header{padding:2px 0 0}.table-list[data-table-list-id=orders] .table-list-filters{gap:8px;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr))}.table-list[data-table-list-id=orders] .table th,.table-list[data-table-list-id=orders] .table td{padding:6px 8px}.table-list[data-table-list-id=orders] .table thead th{font-size:12px;text-transform:uppercase;letter-spacing:.02em;white-space:nowrap}.table-list[data-table-list-id=orders] .table tbody td{vertical-align:top;font-size:14px;line-height:1.25}.order-show-layout{display:grid;grid-template-columns:220px minmax(0, 1fr);gap:12px;align-items:start}.order-statuses-side{position:sticky;top:60px;padding:10px}.order-statuses-side__title{font-size:13px;font-weight:700;color:#0f172a;margin-bottom:8px}.order-status-group{margin-bottom:10px}.order-status-group__name{font-size:12px;color:#475569;font-weight:700;margin-bottom:5px}.order-status-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 6px;border-radius:6px;color:#334155;font-size:12px;text-decoration:none}.order-status-row__count{min-width:24px;text-align:center;border-radius:999px;background:var(--status-color, #64748b);padding:1px 6px;font-weight:700;font-size:11px;color:#fff}.order-status-row:hover{background:#f1f5f9}.order-status-row.is-active{background:rgba(15,23,42,.06);color:#0f172a;font-weight:700}.order-show-main{min-width:0}.order-details-actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.order-details-page{padding:12px}.order-details-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}.order-back-link{color:#475569;text-decoration:none;font-weight:600}.order-back-link:hover{color:#1d4ed8}.order-details-sub{display:inline-flex;gap:10px;flex-wrap:wrap;color:#64748b;font-size:12px}.order-details-pill{border-radius:999px;padding:5px 10px;background:#eef6ff;border:1px solid #cfe2ff;color:#1d4ed8;font-size:12px;font-weight:700}.order-status-change{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.order-status-change__form{display:flex;align-items:center;gap:6px}.order-status-change__select{min-width:180px}.order-details-tabs{display:flex;gap:6px;flex-wrap:wrap}.order-details-tab{border:1px solid #d6deea;border-radius:8px;padding:5px 10px;color:#475569;font-size:12px;background:#f8fafc;cursor:pointer}.order-details-tab.is-active{border-color:#bfdbfe;color:#1d4ed8;background:#eff6ff;font-weight:700}.order-item-cell{display:grid;grid-template-columns:44px 1fr;gap:8px;align-items:center;min-width:260px}.order-item-thumb{width:44px;height:44px;border-radius:6px;border:1px solid #dbe3ef;object-fit:cover}.order-item-thumb--empty{display:inline-block;background:#eef2f7;border-style:dashed}.order-item-name{font-weight:600;color:#0f172a}.order-grid-2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:12px}.order-grid-3{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.order-kv{margin:0;display:grid;grid-template-columns:150px 1fr;gap:6px 10px;font-size:12px}.payment-summary{display:grid;gap:6px;max-width:420px}.payment-summary__row{display:flex;align-items:center;gap:10px;font-size:12px}.payment-summary__label{width:150px;flex-shrink:0;color:#64748b}.payment-summary__value{font-weight:600;color:#0f172a}.order-kv dt{color:#64748b}.order-kv dd{margin:0;color:#0f172a;font-weight:600}.order-address{display:grid;gap:3px;font-size:12px;color:#0f172a}.order-events{display:grid;gap:8px}.order-event{border:1px solid #e2e8f0;border-radius:8px;padding:8px;background:#fbfdff}.order-event__head{color:#64748b;font-size:11px}.order-event__body{margin-top:4px;color:#0f172a;font-size:12px}.order-tab-panel{display:none}.order-tab-panel.is-active{display:block}.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}.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 8px;border-left:3px solid var(--c-primary, #2563eb);background:#eef4ff;border-radius:6px}.integration-settings-group__title{margin:0;font-size:14px;font-weight:700;letter-spacing:.01em;color:#1e3a8a}.integration-settings-group__desc{margin:4px 0 0;color:#4b5563}.integration-settings-group__grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:10px 12px;align-items:start}.integration-settings-group__full{grid-column:1/-1}.integration-settings-group__grid .form-field{margin:0;align-self:start}.integration-settings-group__grid .form-control{min-height:34px;height:34px}.integration-settings-group__grid input[type=date].form-control{line-height:1.2}.integration-settings-checkboxes{border:0;padding:0;margin:0}.integration-settings-checkboxes .field-label{display:block;margin-bottom:2px}.integration-settings-checkboxes__list{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:6px 12px}.integration-settings-checkboxes__item{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#334155}@media(max-width: 768px){.app-shell{flex-direction:column}.sidebar{width:100% !important;min-width:0 !important;border-right:0;border-bottom:1px solid #243041;padding:14px;overflow-x:auto}.sidebar__brand{margin:0 0 10px;font-size:22px}.sidebar__collapse-btn{display:none}.sidebar__nav{display:flex;gap:8px;overflow-x:auto}.sidebar__link{white-space:nowrap}.topbar{padding:0 14px}.container{margin-top:16px;width:calc(100% - 16px);margin-left:8px;margin-right:8px;padding:0 3px 12px}.settings-grid{grid-template-columns:1fr}.page-head{flex-direction:column;align-items:flex-start}.orders-stats{grid-template-columns:1fr;width:100%}.order-show-layout{grid-template-columns:1fr}.order-statuses-side{position:static;top:auto}.order-details-actions{justify-content:flex-start}.order-grid-2,.order-grid-3{grid-template-columns:1fr}.order-kv{grid-template-columns:1fr;gap:2px}.filters-grid,.form-grid,.form-grid-2,.form-grid-3,.form-grid-4,.shipment-grid,.statuses-form,.statuses-inline-form,.table-list-filters,.product-links-search-form,.product-links-inline-form{grid-template-columns:1fr}.statuses-dnd-item__content{display:block}.statuses-inline-delete{margin-top:6px}.filters-actions{align-items:center}.table-list__header,.table-list__footer{align-items:flex-start}.product-links-head{grid-template-columns:1fr}.integration-settings-group__grid{grid-template-columns:1fr}.integration-settings-checkboxes__list{grid-template-columns:1fr}.card{padding:12px}.modal--image-preview{width:min(92vw,100%)}} diff --git a/public/assets/css/login.css b/public/assets/css/login.css index 243bf94..fb27907 100644 --- a/public/assets/css/login.css +++ b/public/assets/css/login.css @@ -1 +1 @@ -:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #e2e8f0;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--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-primary)}.btn--primary:hover{background:var(--c-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--block{width:100%}.btn:active{transform:translateY(1px)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-color:var(--c-primary)}.form-control{width:100%;min-height:34px;border:1px solid var(--c-border);border-radius:8px;padding:5px 10px;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)}.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{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}.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}:root{--shadow-card: 0 20px 50px rgba(22, 34, 58, 0.14)}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;color:var(--c-text);background:var(--c-bg);overflow-x:hidden}.bg-orb{position:fixed;width:460px;height:460px;border-radius:999px;filter:blur(28px);z-index:0;opacity:.45;pointer-events:none}.bg-orb-left{top:-200px;left:-180px;background:radial-gradient(circle, rgba(102, 144, 244, 0.48) 0%, rgba(102, 144, 244, 0) 70%)}.bg-orb-right{right:-200px;bottom:-220px;background:radial-gradient(circle, rgba(30, 42, 58, 0.36) 0%, rgba(30, 42, 58, 0) 70%)}.login-page{min-height:100vh;display:grid;place-items:center;padding:32px 20px;position:relative;z-index:1}.login-card{width:100%;max-width:430px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:12px;box-shadow:var(--shadow-card);padding:34px 30px 28px;animation:card-enter 420ms ease-out}.login-header{margin-bottom:24px}.login-badge{display:inline-block;margin:0 0 14px;padding:5px 12px;border-radius:999px;border:1px solid #d9e2ff;background:#eef2ff;color:#3f5faf;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}h1{margin:0;color:var(--c-text-strong);font-size:clamp(1.6rem,2.5vw,1.9rem);line-height:1.15;font-weight:700}.login-subtitle{margin:10px 0 0;font-size:15px;line-height:1.55;color:var(--c-muted)}.login-alert{margin-bottom:18px}.login-alert-placeholder{opacity:.56}.login-form{display:grid;gap:16px}.form-field{display:grid;gap:7px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}.login-form .form-control{min-height:46px;padding:0 14px;border-width:2px}.login-form .form-control::placeholder{color:#cbd5e0}.login-submit{margin-top:2px;font-size:15px;min-height:48px}@keyframes card-enter{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@media(max-width: 640px){.login-page{padding:18px 14px}.login-card{padding:24px 20px 20px}h1{font-size:1.55rem}} +:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #e2e8f0;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--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-primary)}.btn--primary:hover{background:var(--c-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:active{transform:translateY(1px)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-color:var(--c-primary)}.form-control{width:100%;min-height:34px;border:1px solid var(--c-border);border-radius:8px;padding:5px 10px;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}:root{--shadow-card: 0 20px 50px rgba(22, 34, 58, 0.14)}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;color:var(--c-text);background:var(--c-bg);overflow-x:hidden}.bg-orb{position:fixed;width:460px;height:460px;border-radius:999px;filter:blur(28px);z-index:0;opacity:.45;pointer-events:none}.bg-orb-left{top:-200px;left:-180px;background:radial-gradient(circle, rgba(102, 144, 244, 0.48) 0%, rgba(102, 144, 244, 0) 70%)}.bg-orb-right{right:-200px;bottom:-220px;background:radial-gradient(circle, rgba(30, 42, 58, 0.36) 0%, rgba(30, 42, 58, 0) 70%)}.login-page{min-height:100vh;display:grid;place-items:center;padding:32px 20px;position:relative;z-index:1}.login-card{width:100%;max-width:430px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:12px;box-shadow:var(--shadow-card);padding:34px 30px 28px;animation:card-enter 420ms ease-out}.login-header{margin-bottom:24px}.login-badge{display:inline-block;margin:0 0 14px;padding:5px 12px;border-radius:999px;border:1px solid #d9e2ff;background:#eef2ff;color:#3f5faf;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}h1{margin:0;color:var(--c-text-strong);font-size:clamp(1.6rem,2.5vw,1.9rem);line-height:1.15;font-weight:700}.login-subtitle{margin:10px 0 0;font-size:15px;line-height:1.55;color:var(--c-muted)}.login-alert{margin-bottom:18px}.login-alert-placeholder{opacity:.56}.login-form{display:grid;gap:16px}.form-field{display:grid;gap:7px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}.login-form .form-control{min-height:46px;padding:0 14px;border-width:2px}.login-form .form-control::placeholder{color:#cbd5e0}.login-submit{margin-top:2px;font-size:15px;min-height:48px}@keyframes card-enter{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@media(max-width: 640px){.login-page{padding:18px 14px}.login-card{padding:24px 20px 20px}h1{font-size:1.55rem}} diff --git a/resources/lang/pl.php b/resources/lang/pl.php index c1b31de..4ede57c 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -28,6 +28,7 @@ return [ 'dashboard' => 'Dashboard', 'settings' => 'Ustawienia', 'statuses' => 'Statusy', + 'integrations' => 'Integracje', 'allegro' => 'Integracje Allegro', 'apaczka' => 'Integracja Apaczka', 'inpost' => 'Integracja InPost', @@ -469,6 +470,46 @@ return [ 'title' => 'Ustawienia', 'description' => 'Konfiguracja i narzedzia administracyjne systemu.', 'submenu_label' => 'Sekcje ustawien', + 'integrations_hub' => [ + 'title' => 'Integracje', + 'description' => 'Wspolny panel konfiguracji wszystkich providerow.', + 'list_title' => 'Skonfigurowane integracje', + 'empty' => 'Brak dostepnych integracji.', + 'fields' => [ + 'provider' => 'Provider', + 'instance' => 'Instancja', + 'authorization' => 'Status polaczenia', + 'secret' => 'Sekret API', + 'active' => 'Aktywna', + 'last_test' => 'Ostatni test', + 'actions' => 'Akcje', + ], + 'providers' => [ + 'allegro' => 'Allegro', + 'allegro_sandbox' => 'Allegro Sandbox', + 'allegro_production' => 'Allegro Production', + 'apaczka' => 'Apaczka', + 'inpost' => 'InPost', + 'shoppro' => 'shopPRO', + 'shoppro_instances' => ':count instancji', + ], + 'status' => [ + 'connected' => 'Polaczono', + 'not_connected' => 'Brak polaczenia', + 'configured' => 'Skonfigurowana', + 'not_configured' => 'Brak konfiguracji', + 'saved' => 'Zapisany', + 'missing' => 'Brak', + ], + 'active' => [ + 'yes' => 'Tak', + 'no' => 'Nie', + ], + 'actions' => [ + 'configure' => 'Konfiguruj', + 'settings' => 'Ustawienia', + ], + ], 'database' => [ 'title' => 'Baza danych', 'state' => [ @@ -800,10 +841,80 @@ return [ ], 'integrations' => [ 'title' => 'Integracje shopPRO', + 'description' => 'W tym miejscu konfigurujesz wiele niezaleznych instancji shopPRO.', 'list_title' => 'Integracje shopPRO', 'create_title' => 'Dodaj integracje', 'edit_title' => 'Edytuj integracje', 'empty' => 'Brak skonfigurowanych integracji.', + 'tabs' => [ + 'label' => 'Zakladki integracji shopPRO', + 'integration' => 'Integracja', + 'statuses' => 'Statusy', + 'settings' => 'Ustawienia', + 'delivery' => 'Formy dostawy', + ], + 'selector' => [ + 'integration' => 'Wybrana integracja', + ], + 'statuses' => [ + 'title' => 'Statusy', + 'description' => 'Mapowanie statusow zamowien pomiedzy shopPRO i orderPRO.', + 'empty' => 'Brak statusow do mapowania. Uzyj przycisku pobrania statusow.', + 'select_integration_first' => 'Najpierw wybierz lub zapisz integracje w zakladce Integracja.', + 'actions' => [ + 'sync' => 'Pobierz statusy z shopPRO', + ], + 'flash' => [ + 'sync_ok' => 'Pobrano statusy shopPRO. Rozpoznane statusy: :count.', + 'sync_failed' => 'Nie udalo sie pobrac statusow shopPRO.', + 'saved' => 'Mapowanie statusow zostalo zapisane.', + 'save_failed' => 'Nie udalo sie zapisac mapowania statusow.', + 'invalid_payload' => 'Niepoprawne dane mapowania statusow.', + ], + ], + 'settings' => [ + 'title' => 'Ustawienia synchronizacji', + 'description' => 'Parametry automatycznego pobierania zamowien shopPRO.', + 'select_integration_first' => 'Najpierw wybierz lub zapisz integracje w zakladce Integracja.', + 'orders_group_title' => 'Pobieranie zamowien', + 'orders_group_description' => 'Ustawienia automatycznego importu zamowien z shopPRO.', + 'orders_import_interval_minutes' => 'Interwal pobierania zamowien (minuty)', + 'orders_import_interval_hint' => 'Zakres: 1-1440 minut. Dotyczy harmonogramu joba shoppro_orders_import.', + 'statuses_group_title' => 'Synchronizacja statusow', + 'statuses_group_description' => 'Ustawienia harmonogramu i kierunku synchronizacji statusow.', + 'status_sync_direction_hint' => 'Aktualnie aktywny jest kierunek shopPRO -> orderPRO. Ustawienie orderPRO -> shopPRO jest przygotowane pod kolejny etap.', + 'status_sync_interval_minutes' => 'Interwal synchronizacji statusow (minuty)', + 'status_sync_interval_hint' => 'Zakres: 1-1440 minut. Dotyczy harmonogramu joba shoppro_order_status_sync.', + 'payment_group_title' => 'Synchronizacja platnosci', + 'payment_group_description' => 'Sprawdza czy zamowienia shopPRO zostaly oplacone i aktualizuje status platnosci w orderPRO.', + 'payment_sync_interval_minutes' => 'Interwal sprawdzania platnosci (minuty)', + 'payment_sync_interval_hint' => 'Zakres: 1-1440 minut. Dotyczy harmonogramu joba shoppro_payment_status_sync.', + 'payment_sync_status_codes' => 'Statusy do sprawdzania platnosci', + 'payment_sync_status_codes_hint' => 'Jesli nic nie zaznaczysz, system pominie tylko statusy koncowe (np. wyslane/anulowane).', + ], + 'delivery' => [ + 'title' => 'Formy dostawy', + 'description' => 'Mapowanie form dostawy shopPRO do uslug nadawczych Allegro WZA/InPost.', + 'select_integration_first' => 'Najpierw wybierz lub zapisz integracje w zakladce Integracja.', + 'empty_orders' => 'Brak form dostawy shopPRO wykrytych w zamowieniach tej integracji.', + 'not_connected' => 'Brak aktywnego polaczenia Allegro. Podlacz konto Allegro, aby pobrac liste uslug dostawy.', + 'no_inpost_services' => 'Brak uslug InPost (sprawdz polaczenie z Allegro).', + 'fields' => [ + 'order_method' => 'Forma dostawy shopPRO', + 'carrier' => 'Przewoznik', + 'allegro_service' => 'Usluga dostawy Allegro', + 'no_mapping' => 'brak mapowania', + 'search_placeholder' => 'Szukaj uslugi Allegro...', + 'select_carrier_first' => 'Najpierw wybierz przewoznika.', + ], + 'actions' => [ + 'save' => 'Zapisz mapowania dostawy', + ], + 'flash' => [ + 'saved' => 'Mapowania form dostawy zostaly zapisane.', + 'save_failed' => 'Nie udalo sie zapisac mapowan form dostawy.', + ], + ], 'fields' => [ 'name' => 'Nazwa', 'base_url' => 'Base URL', @@ -947,6 +1058,9 @@ return [ 'save_failed' => 'Nie udalo sie zapisac ustawien crona.', 'load_failed' => 'Nie udalo sie pobrac danych crona.', ], + 'pagination' => [ + 'summary' => 'Strona :page/:total_pages, rekordy: :total', + ], ], 'gs1' => [ 'title' => 'GS1 / EAN', diff --git a/resources/scss/app.scss b/resources/scss/app.scss index 394e8e8..0001b7f 100644 --- a/resources/scss/app.scss +++ b/resources/scss/app.scss @@ -445,6 +445,11 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow { display: flex; gap: 8px; flex-wrap: wrap; + align-items: flex-start; +} + +.form-actions .btn { + align-self: flex-start; } .statuses-form { @@ -453,6 +458,10 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.statuses-form .form-actions { + grid-column: 1 / -1; +} + .statuses-color-input { min-height: 32px; padding: 2px; @@ -673,6 +682,17 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow { 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; @@ -1997,6 +2017,111 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow { } } +.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; +} + +.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 8px; + border-left: 3px solid var(--c-primary, #2563eb); + background: #eef4ff; + border-radius: 6px; +} + +.integration-settings-group__title { + margin: 0; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.01em; + color: #1e3a8a; +} + +.integration-settings-group__desc { + margin: 4px 0 0; + color: #4b5563; +} + +.integration-settings-group__grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px 12px; + align-items: start; +} + +.integration-settings-group__full { + grid-column: 1 / -1; +} + +.integration-settings-group__grid .form-field { + margin: 0; + align-self: start; +} + +.integration-settings-group__grid .form-control { + min-height: 34px; + height: 34px; +} + +.integration-settings-group__grid input[type='date'].form-control { + line-height: 1.2; +} + +.integration-settings-checkboxes { + border: 0; + padding: 0; + margin: 0; +} + +.integration-settings-checkboxes .field-label { + display: block; + margin-bottom: 2px; +} + +.integration-settings-checkboxes__list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px 12px; +} + +.integration-settings-checkboxes__item { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: #334155; +} + @media (max-width: 768px) { .app-shell { flex-direction: column; @@ -2114,6 +2239,14 @@ details[open] > .sidebar__group-toggle .sidebar__toggle-arrow { grid-template-columns: 1fr; } + .integration-settings-group__grid { + grid-template-columns: 1fr; + } + + .integration-settings-checkboxes__list { + grid-template-columns: 1fr; + } + .card { padding: 12px; } diff --git a/resources/views/layouts/app.php b/resources/views/layouts/app.php index 0b5c4e9..24d0ec2 100644 --- a/resources/views/layouts/app.php +++ b/resources/views/layouts/app.php @@ -73,14 +73,8 @@ = $e($t('navigation.cron')) ?> - - = $e($t('navigation.allegro')) ?> - - - = $e($t('navigation.apaczka')) ?> - - - = $e($t('navigation.inpost')) ?> + + = $e($t('navigation.integrations')) ?> = $e($t('navigation.company')) ?> diff --git a/resources/views/orders/show.php b/resources/views/orders/show.php index b4420ac..bd4e72a 100644 --- a/resources/views/orders/show.php +++ b/resources/views/orders/show.php @@ -15,6 +15,8 @@ $allStatusesList = is_array($allStatuses ?? null) ? $allStatuses : []; $currentStatusCodeValue = (string) ($currentStatusCode ?? ''); $flashSuccessMsg = (string) ($flashSuccess ?? ''); $flashErrorMsg = (string) ($flashError ?? ''); +$carrierRaw = (string) ($orderRow['external_carrier_id'] ?? ''); +$carrierDisplay = trim(html_entity_decode(strip_tags($carrierRaw), ENT_QUOTES | ENT_HTML5, 'UTF-8')); $addressByType = [ 'customer' => null, @@ -193,7 +195,7 @@ foreach ($addressesList as $address) {= $e($t('settings.integrations_hub.description')) ?>
+ + +| = $e($t('settings.integrations_hub.fields.provider')) ?> | += $e($t('settings.integrations_hub.fields.instance')) ?> | += $e($t('settings.integrations_hub.fields.authorization')) ?> | += $e($t('settings.integrations_hub.fields.secret')) ?> | += $e($t('settings.integrations_hub.fields.active')) ?> | += $e($t('settings.integrations_hub.fields.last_test')) ?> | += $e($t('settings.integrations_hub.fields.actions')) ?> | +
|---|---|---|---|---|---|---|
| = $e($t('settings.integrations_hub.empty')) ?> | +||||||
| = $e((string) ($item['provider'] ?? '')) ?> | += $e((string) ($item['instance'] ?? '')) ?> | += $e((string) ($item['authorization_status'] ?? '')) ?> | += $e((string) ($item['secret_status'] ?? '')) ?> | += $e(!empty($item['is_active']) ? $t('settings.integrations_hub.active.yes') : $t('settings.integrations_hub.active.no')) ?> | += $e((string) ($item['last_test_at'] ?? '')) ?> | ++ + = $e($t('settings.integrations_hub.actions.settings')) ?> + + | +
= $e($t('settings.integrations.description')) ?>
+ + +| ID | += $e($t('settings.integrations.fields.name')) ?> | += $e($t('settings.integrations.fields.base_url')) ?> | += $e($t('settings.integrations.fields.active')) ?> | += $e($t('settings.integrations.fields.last_test')) ?> | += $e($t('settings.integrations.fields.actions')) ?> | +
|---|---|---|---|---|---|
| = $e($t('settings.integrations.empty')) ?> | +|||||
| = $e((string) ($item['id'] ?? 0)) ?> | += $e((string) ($item['name'] ?? '')) ?> | += $e((string) ($item['base_url'] ?? '')) ?> | ++ + = $e($t('settings.integrations.active.yes')) ?> + + = $e($t('settings.integrations.active.no')) ?> + + | +
+ = $e($statusLabel) ?>
+
+
+ = $e((string) $item['last_test_at']) ?>
+ | HTTP = $e((string) ($item['last_test_http_code'])) ?>
+
+
+ |
+ + + | +
= $e($t('settings.integrations.statuses.description')) ?>
+ + += $e($t('settings.integrations.statuses.select_integration_first')) ?>
+ + + + + += $e($t('settings.integrations.settings.description')) ?>
+ + += $e($t('settings.integrations.settings.select_integration_first')) ?>
+ + + += $e($t('settings.integrations.delivery.description')) ?>
+ + += $e($t('settings.integrations.delivery.select_integration_first')) ?>
+ + += $e($t('settings.integrations.delivery.empty_orders')) ?>
+ + + + +