diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b55705c..5207557 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,24 @@ "Bash(python3:*)", "Bash(py:*)", "Bash(npm ls:*)", - "WebFetch(domain:github.com)" + "WebFetch(domain:github.com)", + "mcp__serena__read_file", + "mcp__serena__find_symbol", + "mcp__serena__activate_project", + "mcp__serena__check_onboarding_performed", + "mcp__serena__search_for_pattern", + "mcp__serena__replace_content", + "WebFetch(domain:mojegs1.pl)", + "WebFetch(domain:gs1pl.org)", + "WebFetch(domain:modules4presta.io)", + "Bash(curl -s -w \"\\\\n%{http_code}\" -u \"user_85c27342-bc97-4f42-8890-f6b27d3233c4:K3sawGA3X?L?e^bJ$ZqyhieFG\\)w#c8f+?V^z\" \"https://mojegs1.pl/api/v2/products?page[offset]=1&page[limit]=5\")", + "Bash(curl -s -w \"\\\\n%{http_code}\" --user 'user_85c27342-bc97-4f42-8890-f6b27d3233c4:K3sawGA3X?L?e^bJ$ZqyhieFG\\)w#c8f+?V^z' \"https://mojegs1.pl/api/v2/products?page%5Boffset%5D=1&page%5Blimit%5D=5\")", + "Bash(curl -s --user 'user_85c27342-bc97-4f42-8890-f6b27d3233c4:K3sawGA3X?L?e^bJ$ZqyhieFG\\)w#c8f+?V^z' \"https://mojegs1.pl/api/v2/products?page%5Boffset%5D=93&page%5Blimit%5D=5&sort=name\")", + "Bash(curl -s --user 'user_85c27342-bc97-4f42-8890-f6b27d3233c4:K3sawGA3X?L?e^bJ$ZqyhieFG\\)w#c8f+?V^z' \"https://mojegs1.pl/api/v2/products?page%5Boffset%5D=1&page%5Blimit%5D=100&sort=name\")", + "Bash(php -r ':*)", + "Bash(php tmp_gs1_test.php)", + "mcp__serena__write_memory", + "mcp__serena__prepare_for_new_conversation" ] } } diff --git a/.env.example b/.env.example index 4db2e15..005102a 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ APP_DEBUG=true APP_URL=http://localhost:8000 SESSION_NAME=orderpro_session INTEGRATIONS_SECRET=change-me-long-random-secret +CRON_RUN_ON_WEB=false +CRON_WEB_LIMIT=5 DB_CONNECTION=mysql DB_HOST=127.0.0.1 diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..14d86ad --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/memories/gs1-integration/handover.md b/.serena/memories/gs1-integration/handover.md new file mode 100644 index 0000000..7a3a564 --- /dev/null +++ b/.serena/memories/gs1-integration/handover.md @@ -0,0 +1,17 @@ +# Handover: Integracja MojeGS1 — przypisywanie EAN + +## Status: ZAIMPLEMENTOWANE (core flow) + +## Co zostalo zaimplementowane: +1. `src/Modules/GS1/MojeGS1Client.php` — klient HTTP Basic Auth (cURL), listProducts, getProduct, upsertProduct, findHighestGtin, generateNextEan, calculateEan13CheckDigit +2. `src/Modules/GS1/GS1Service.php` — assignEanToProduct: sprawdza brak EAN, pobiera credentials z app_settings, generuje next EAN, rejestruje w GS1 API, zapisuje EAN lokalnie +3. `src/Modules/Products/ProductRepository.php` — dodano updateEan(int $id, string $ean) +4. `src/Modules/Products/ProductsController.php` — dodano assignGs1Ean method, wstrzyknięto GS1Service +5. `routes/web.php` — POST /products/{id}/assign-ean, wiring GS1Service +6. `resources/views/products/show.php` — przycisk "Przypisz EAN z GS1" gdy EAN pusty +7. `resources/lang/pl.php` — klucze products.gs1.* + +## Co jeszcze do zrobienia (opcjonalne): +- Strona ustawień GS1 (formularz w settings do zarządzania credentials) +- Migracja app_settings z domyślnymi wartościami GS1 +- Możliwość bulk-assign EAN dla wielu produktów naraz \ No newline at end of file diff --git a/.serena/memories/gs1-integration/plan.md b/.serena/memories/gs1-integration/plan.md new file mode 100644 index 0000000..e0025ee --- /dev/null +++ b/.serena/memories/gs1-integration/plan.md @@ -0,0 +1,142 @@ +# Plan integracji MojeGS1 API - przypisywanie EAN z poziomu orderPRO + +## Cel +Przycisk "Przypisz EAN z GS1" na stronie szczegółów produktu (`/products/{id}`), który automatycznie generuje kolejny EAN z puli GS1, rejestruje produkt w MojeGS1 i zapisuje EAN lokalnie. + +## API MojeGS1 v2 +- **Base URL:** `https://mojegs1.pl/api/v2` +- **Auth:** HTTP Basic (login + hasło) +- **Spec:** `/api/v2/swagger/external-api/swagger.json` + +### Endpointy używane: +- `GET /api/v2/products?page[offset]=X&page[limit]=100&sort=name` — lista produktów (paginacja) +- `GET /api/v2/products/{Gtin}` — pojedynczy produkt +- `PUT /api/v2/products/{Gtin}` — utwórz/aktualizuj produkt + +### Wymagane pola PUT: +- `brandName` (2-200 znaków) — marka +- `gpcCode` (8-cyfrowy int) — kod klasyfikacji GPC +- `netContent` (number) + `netContentUnit` (string: szt, kg, g, l, ml, m, cm, mm, m2, m3) +- `status` — "ACT" / "WIT" / "HID" +- `targetMarket` — array, np. ["PL"] +- `descriptionLanguage` — "PL" +- `description` — opis (20-4000 znaków, opcjonalne ale zalecane) +- `commonName` — nazwa zwyczajowa (do 150 znaków) + +### Struktura request PUT: +```json +{ + "data": { + "type": "products", + "id": "{GTIN-13}", + "attributes": { + "brandName": "marianek.pl", + "commonName": "Nazwa produktu", + "gpcCode": 10008365, + "netContent": 1, + "netContentUnit": "szt", + "status": "ACT", + "targetMarket": ["PL"], + "descriptionLanguage": "PL" + } + } +} +``` + +## Stan konta GS1 +- 461 produktów zarejestrowanych +- **Główny prefiks:** `590532390` (9 cyfr, 451 produktów, ciągła sekwencja 000-450) +- Drugi prefiks: `590531617` (10 produktów, starsze) +- Najwyższy GTIN: `5905323904507` (item 450) +- Następny wolny: `5905323904514` (item 451) +- Max capacity: 1000 produktów (item 000-999) + +## Dane dostępowe API +- Login: `user_85c27342-bc97-4f42-8890-f6b27d3233c4` +- Hasło: `K3sawGA3X?L?e^bJ$ZqyhieFG)w#c8f+?V^z` +- **UWAGA:** dane przechowywane w tabeli `app_settings`, NIE w kodzie + +## Plan implementacji + +### Krok 1: Tabela app_settings — migracja +Plik: `database/migrations/20260224_000013_add_gs1_settings.sql` +Dodać rekordy do `app_settings`: +- `gs1_api_login` — login API +- `gs1_api_password` — hasło API (zaszyfrowane lub plain — zależy od obecnego wzorca) +- `gs1_prefix` — prefiks GS1 (domyślnie `590532390`) +- `gs1_default_brand` — domyślna marka (domyślnie `marianek.pl`) +- `gs1_default_gpc_code` — domyślny kod GPC (domyślnie `10008365`) + +Sprawdzić czy tabela `app_settings` już istnieje (migracja 000012). + +### Krok 2: Strona ustawień GS1 +Plik widoku: `resources/views/settings/gs1.php` +Pliki PHP: zmiana w `SettingsController.php` — dodać metody `gs1()` i `gs1Save()` +Route: `GET /settings/gs1` + `POST /settings/gs1` + +Formularz z polami: login, hasło, prefiks, domyślna marka, domyślny kod GPC. +Przycisk "Test połączenia" (opcjonalnie). + +### Krok 3: Klient MojeGS1 +Plik: `src/Modules/GS1/MojeGS1Client.php` + +Metody: +- `listProducts(int $page, int $limit): array` — lista produktów +- `getProduct(string $gtin): ?array` — pojedynczy produkt +- `upsertProduct(string $gtin, array $attributes): array` — PUT produkt +- `findHighestGtin(string $prefix): ?string` — paginuje i szuka max GTIN z prefiksu +- `generateNextEan(string $prefix): string` — oblicza kolejny EAN z cyfrą kontrolną + +Statyczna metoda: +- `calculateEan13CheckDigit(string $partial12): int` + +### Krok 4: Serwis GS1 +Plik: `src/Modules/GS1/GS1Service.php` + +Metoda: `assignEanToProduct(int $productId): array` +1. Pobierz produkt z orderPRO (sprawdź czy nie ma już EAN) +2. Pobierz ustawienia GS1 z app_settings +3. Znajdź najwyższy GTIN w GS1 API +4. Wygeneruj następny EAN +5. Zarejestruj w GS1 (PUT) z danymi produktu (nazwa, marka, gpcCode) +6. Zaktualizuj EAN w produkcie orderPRO +7. Zwróć nowy EAN + +### Krok 5: Controller endpoint +Plik: `src/Modules/Products/ProductsController.php` +Nowa metoda: `assignGs1Ean(Request $request): Response` +Route: `POST /products/{id}/assign-ean` +- Walidacja CSRF +- Wywołanie GS1Service::assignEanToProduct +- Flash success/error +- Redirect back do `/products/{id}` + +### Krok 6: Widok — przycisk na stronie produktu +Plik: `resources/views/products/show.php` +Dodać przycisk "Przypisz EAN z GS1" widoczny gdy `$product['ean']` jest pusty. +Formularz POST do `/products/{id}/assign-ean`. + +### Krok 7: Tłumaczenia +Plik: `resources/lang/pl.php` +Dodać klucze: +- `products.gs1.assign_ean` — "Przypisz EAN z GS1" +- `products.gs1.ean_assigned` — "EAN :ean został przypisany i zarejestrowany w GS1." +- `products.gs1.already_has_ean` — "Produkt ma już przypisany EAN." +- `products.gs1.error` — "Błąd podczas przypisywania EAN z GS1." +- `settings.gs1.*` — etykiety formularza ustawień + +### Krok 8: Routing +Plik: `routes/web.php` +Dodać: +- `POST /products/{id}/assign-ean` → ProductsController::assignGs1Ean +- `GET /settings/gs1` → SettingsController::gs1 +- `POST /settings/gs1` → SettingsController::gs1Save + +## Zależności +- `AppSettingsRepository` — już istnieje (migracja 000012), sprawdzić API +- `ProductRepository` — metoda update EAN (sprawdzić czy istnieje) +- Curl extension — wymagany (prawdopodobnie już jest) + +## Kolejność prac +1 → 3 → 4 → 5 → 6 → 7 → 8 → 2 (ustawienia na końcu, na początku hardcode credentials do testów) +Praktycznie: 3 → 4 → 5 → 6 → 7 → 8 (migracja i ustawienia mogą być równolegle) diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..8f85441 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,126 @@ +# the name by which the project can be referenced within Serena +project_name: "orderPRO" + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# java julia kotlin lua markdown +# matlab nix pascal perl php +# php_phpactor powershell python python_jedi r +# rego ruby ruby_solargraph rust scala +# swift terraform toml typescript typescript_vts +# vue yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- php + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# The language backend to use for this project. +# If not set, the global setting from serena_config.yml is used. +# Valid values: LSP, JetBrains +# Note: the backend is fixed at startup. If a project with a different backend +# is activated post-init, an error will be returned. +language_backend: + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore in this project. +# Same syntax as gitignore, so you can use * and **. +# Note: global ignored_paths from serena_config.yml are also applied additively. +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +# time budget (seconds) per tool call for the retrieval of additional symbol information +# such as docstrings or parameter information. +# This overrides the corresponding setting in the global configuration; see the documentation there. +# If null or missing, use the setting from the global configuration. +symbol_info_budget: diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 903a2e4..91f3b09 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -14,6 +14,12 @@ "lmtime": 1771460522111, "modified": false }, + "cron.php": { + "type": "-", + "size": 2656, + "lmtime": 1771954648781, + "modified": false + }, "migrate.php": { "type": "-", "size": 1357, @@ -30,17 +36,23 @@ } }, ".claude": {}, + "CLAUDE.md": { + "type": "-", + "size": 109, + "lmtime": 1771943170048, + "modified": false + }, "composer.json": { "type": "-", - "size": 368, - "lmtime": 1771691973809, + "size": 413, + "lmtime": 1771954556361, "modified": false }, "config": { "app.php": { "type": "-", - "size": 690, - "lmtime": 1771692227139, + "size": 972, + "lmtime": 1771955055783, "modified": false }, "auth.php": { @@ -51,9 +63,9 @@ }, "database.php": { "type": "-", - "size": 425, + "size": 433, "lmtime": 1771691899980, - "modified": false + "modified": true } }, "database": { @@ -99,6 +111,36 @@ "size": 4101, "lmtime": 1771877287682, "modified": false + }, + "20260224_000008_add_stock_0_buy_to_product_variants.sql": { + "type": "-", + "size": 100, + "lmtime": 1771928276614, + "modified": false + }, + "20260224_000009_add_gpsr_and_custom_fields.sql": { + "type": "-", + "size": 311, + "lmtime": 1771931873866, + "modified": false + }, + "20260224_000010_add_producer_name_to_products.sql": { + "type": "-", + "size": 84, + "lmtime": 1771935778647, + "modified": false + }, + "20260224_000011_create_cron_and_product_link_alerts_tables.sql": { + "type": "-", + "size": 3271, + "lmtime": 1771954346959, + "modified": false + }, + "20260224_000012_create_app_settings_table.sql": { + "type": "-", + "size": 711, + "lmtime": 1771954914723, + "modified": false } }, "seeders": {} @@ -116,6 +158,12 @@ "lmtime": 1771869108326, "modified": false }, + "CRON_QUEUE.md": { + "type": "-", + "size": 1192, + "lmtime": 1771955129456, + "modified": false + }, "FRONTEND_STANDARDS.md": { "type": "-", "size": 766, @@ -148,8 +196,8 @@ }, "TODO.md": { "type": "-", - "size": 357, - "lmtime": 1771883948939, + "size": 408, + "lmtime": 1771941672012, "modified": false } }, @@ -167,9 +215,9 @@ }, ".env.example": { "type": "-", - "size": 283, - "lmtime": 1771692627769, - "modified": true + "size": 321, + "lmtime": 1771955059028, + "modified": false }, ".gitignore": { "type": "-", @@ -1360,8 +1408,8 @@ "css": { "app.css": { "type": "-", - "size": 12921, - "lmtime": 1771882814106, + "size": 14093, + "lmtime": 1771954643148, "modified": false }, "app.css.map": { @@ -1373,7 +1421,7 @@ "login.css": { "type": "-", "size": 4627, - "lmtime": 1771882814620, + "lmtime": 1771954643626, "modified": false }, "login.css.map": { @@ -1403,45 +1451,56 @@ } } }, - "index.php": { - "type": "-", - "size": 157, - "lmtime": 1771459549255, - "modified": false - }, - "login.php": { - "type": "-", - "size": 2002, - "lmtime": 1771459339520, - "modified": false - }, ".htaccess": { "type": "-", - "size": 146, - "lmtime": 1771459552460, + "size": 153, + "lmtime": 1771866989000, "modified": false - } + }, + "index.php": { + "type": "-", + "size": 165, + "lmtime": 1771866989000, + "modified": false + }, + "uploads": {} }, "resources": { "lang": { "pl.php": { "type": "-", - "size": 16192, - "lmtime": 1771883897774, + "size": 21038, + "lmtime": 1771955038639, "modified": false } }, + "modules": { + "jquery-alerts": { + "jquery-alerts.js": { + "type": "-", + "size": 5768, + "lmtime": 1771873304132, + "modified": false + }, + "jquery-alerts.scss": { + "type": "-", + "size": 1964, + "lmtime": 1771873314011, + "modified": false + } + } + }, "scss": { "app.scss": { "type": "-", - "size": 12988, - "lmtime": 1771882795561, + "size": 14427, + "lmtime": 1771954582660, "modified": false }, "login.scss": { "type": "-", - "size": 2507, - "lmtime": 1771691089256, + "size": 2670, + "lmtime": 1771866989000, "modified": false }, "shared": { @@ -1457,52 +1516,52 @@ "auth": { "login.php": { "type": "-", - "size": 1649, - "lmtime": 1771691098142, + "size": 1697, + "lmtime": 1771866989000, + "modified": false + } + }, + "components": { + "table-list.php": { + "type": "-", + "size": 21805, + "lmtime": 1771925480312, "modified": false } }, "dashboard": { "index.php": { "type": "-", - "size": 308, - "lmtime": 1771460502477, + "size": 315, + "lmtime": 1771866989000, "modified": false } }, "layouts": { "app.php": { "type": "-", - "size": 2325, - "lmtime": 1771873354748, + "size": 3772, + "lmtime": 1771955025325, "modified": false }, "auth.php": { "type": "-", - "size": 765, - "lmtime": 1771460477300, + "size": 785, + "lmtime": 1771866989000, "modified": false } }, - "settings": { - "database.php": { - "type": "-", - "size": 3809, - "lmtime": 1771872920284, - "modified": false - }, - "integrations.php": { - "type": "-", - "size": 8885, - "lmtime": 1771877640234, - "modified": false - } - }, - "users": { + "marketplace": { "index.php": { "type": "-", - "size": 2379, - "lmtime": 1771691646298, + "size": 1669, + "lmtime": 1771922314125, + "modified": false + }, + "offers.php": { + "type": "-", + "size": 2493, + "lmtime": 1771922330901, "modified": false } }, @@ -1521,45 +1580,49 @@ }, "index.php": { "type": "-", - "size": 6394, - "lmtime": 1771883548330, + "size": 11064, + "lmtime": 1771956268079, "modified": false }, "links.php": { "type": "-", - "size": 12746, - "lmtime": 1771882772833, + "size": 13765, + "lmtime": 1771954576293, "modified": false }, "show.php": { "type": "-", - "size": 7524, - "lmtime": 1771883892463, + "size": 8496, + "lmtime": 1771935867712, "modified": false } }, - "components": { - "table-list.php": { + "settings": { + "cron.php": { "type": "-", - "size": 19844, - "lmtime": 1771873350334, - "modified": false - } - } - }, - "modules": { - "jquery-alerts": { - "jquery-alerts.js": { - "type": "-", - "size": 5768, - "lmtime": 1771873304132, + "size": 6667, + "lmtime": 1771954993398, "modified": false }, - "jquery-alerts.scss": { + "database.php": { "type": "-", - "size": 1964, - "lmtime": 1771873314011, + "size": 3962, + "lmtime": 1771955008323, "modified": false + }, + "integrations.php": { + "type": "-", + "size": 9039, + "lmtime": 1771955017799, + "modified": false + } + }, + "users": { + "index.php": { + "type": "-", + "size": 1569, + "lmtime": 1771691646298, + "modified": true } } } @@ -1567,8 +1630,8 @@ "routes": { "web.php": { "type": "-", - "size": 7536, - "lmtime": 1771882848671, + "size": 9086, + "lmtime": 1771955048630, "modified": false } }, @@ -1576,8 +1639,8 @@ "Core": { "Application.php": { "type": "-", - "size": 5346, - "lmtime": 1771692614516, + "size": 9212, + "lmtime": 1771955095243, "modified": false }, "Database": { @@ -1688,71 +1751,25 @@ "modified": false } }, - "Products": { - "ProductRepository.php": { + "Marketplace": { + "MarketplaceRepository.php": { "type": "-", - "size": 23385, - "lmtime": 1771883960082, + "size": 4917, + "lmtime": 1771922289322, "modified": false }, - "ProductsController.php": { + "MarketplaceController.php": { "type": "-", - "size": 41328, - "lmtime": 1771883875491, - "modified": false - }, - "ProductService.php": { - "type": "-", - "size": 15370, - "lmtime": 1771873878347, - "modified": false - }, - "ProductValidator.php": { - "type": "-", - "size": 3675, - "lmtime": 1771868735351, - "modified": false - } - }, - "Settings": { - "IntegrationRepository.php": { - "type": "-", - "size": 16971, - "lmtime": 1771883177911, - "modified": false - }, - "SettingsController.php": { - "type": "-", - "size": 50067, - "lmtime": 1771884154350, - "modified": false - }, - "ShopProClient.php": { - "type": "-", - "size": 11618, - "lmtime": 0, - "modified": false - } - }, - "Users": { - "UserRepository.php": { - "type": "-", - "size": 2878, - "lmtime": 1771691981226, - "modified": false - }, - "UsersController.php": { - "type": "-", - "size": 3359, - "lmtime": 1771691595738, + "size": 2742, + "lmtime": 1771922301785, "modified": false } }, "ProductLinks": { "ChannelOffersRepository.php": { "type": "-", - "size": 10253, - "lmtime": 1771878369693, + "size": 10755, + "lmtime": 1771954497515, "modified": false }, "LinkMatcherService.php": { @@ -1763,8 +1780,8 @@ }, "OfferImportService.php": { "type": "-", - "size": 7639, - "lmtime": 1771877602612, + "size": 8091, + "lmtime": 1771954510399, "modified": false }, "ProductLinksController.php": { @@ -1775,14 +1792,112 @@ }, "ProductLinksRepository.php": { "type": "-", - "size": 16078, - "lmtime": 1771882404737, + "size": 20901, + "lmtime": 1771954562275, "modified": false }, "ProductLinksService.php": { "type": "-", - "size": 14928, - "lmtime": 1771882703758, + "size": 14754, + "lmtime": 1771927037312, + "modified": false + } + }, + "Products": { + "ProductRepository.php": { + "type": "-", + "size": 24434, + "lmtime": 1771935798473, + "modified": false + }, + "ProductsController.php": { + "type": "-", + "size": 45649, + "lmtime": 1771925561358, + "modified": false + }, + "ProductService.php": { + "type": "-", + "size": 15635, + "lmtime": 1771935807204, + "modified": false + }, + "ProductValidator.php": { + "type": "-", + "size": 3675, + "lmtime": 1771868735351, + "modified": false + }, + "ShopProExportService.php": { + "type": "-", + "size": 44306, + "lmtime": 1771953661660, + "modified": false + } + }, + "Settings": { + "AppSettingsRepository.php": { + "type": "-", + "size": 1905, + "lmtime": 1771954924419, + "modified": false + }, + "IntegrationRepository.php": { + "type": "-", + "size": 16971, + "lmtime": 1771883177911, + "modified": false + }, + "SettingsController.php": { + "type": "-", + "size": 55652, + "lmtime": 1771954956601, + "modified": false + }, + "ShopProClient.php": { + "type": "-", + "size": 28291, + "lmtime": 1771955377138, + "modified": false + } + }, + "Users": { + "UserRepository.php": { + "type": "-", + "size": 5346, + "lmtime": 1771691981226, + "modified": true + }, + "UsersController.php": { + "type": "-", + "size": 7018, + "lmtime": 1771922207339, + "modified": false + } + }, + "Cron": { + "CronJobProcessor.php": { + "type": "-", + "size": 6385, + "lmtime": 1771954453839, + "modified": false + }, + "CronJobRepository.php": { + "type": "-", + "size": 17045, + "lmtime": 1771954938700, + "modified": false + }, + "CronJobType.php": { + "type": "-", + "size": 709, + "lmtime": 1771954354779, + "modified": false + }, + "ProductLinksHealthCheckHandler.php": { + "type": "-", + "size": 5247, + "lmtime": 1771954535742, "modified": false } } @@ -1790,48 +1905,1029 @@ }, "storage": { "cache": {}, - "logs": {}, + "data": { + "users.json": { + "type": "-", + "size": 3, + "lmtime": 1771951455622, + "modified": false + } + }, + "logs": { + "app.log": { + "type": "-", + "size": 1608, + "lmtime": 1771951455692, + "modified": false + } + }, "sessions": { + "sess_02f48b200eb829fd8a2340d7ecc080f3": { + "type": "-", + "size": 0, + "lmtime": 1771951455761, + "modified": false + }, + "sess_03af80eb6d811375f19220176a984392": { + "type": "-", + "size": 0, + "lmtime": 1771951455835, + "modified": false + }, + "sess_055a363232ef5d2dc5c43f92dc0a37b2": { + "type": "-", + "size": 84, + "lmtime": 1771951455909, + "modified": false + }, + "sess_07a2866535cb72a0a5e0434ae5dba80c": { + "type": "-", + "size": 0, + "lmtime": 1771951455977, + "modified": false + }, + "sess_07faf9d25338374d15df9dcf910112fe": { + "type": "-", + "size": 0, + "lmtime": 1771951456048, + "modified": false + }, + "sess_09a1ef717242b47ee420aa889ee47a60": { + "type": "-", + "size": 84, + "lmtime": 1771951456115, + "modified": false + }, + "sess_0ae02d9e0db674987e37088cf4db5ba0": { + "type": "-", + "size": 84, + "lmtime": 1771951456187, + "modified": false + }, + "sess_0bfc64ad7b940cd20ca470365ca3b068": { + "type": "-", + "size": 84, + "lmtime": 1771951456256, + "modified": false + }, + "sess_0e51ffe16f9329a4a99e16b6eb807f66": { + "type": "-", + "size": 84, + "lmtime": 1771951456328, + "modified": false + }, + "sess_0e6de8d71c1bce53d66521a68bfca7b8": { + "type": "-", + "size": 0, + "lmtime": 1771951456402, + "modified": false + }, + "sess_0ead125206dfdd32646d4dd923953f29": { + "type": "-", + "size": 0, + "lmtime": 1771951456472, + "modified": false + }, + "sess_10d79648c46e68bbf543cce7baae55d8": { + "type": "-", + "size": 84, + "lmtime": 1771951456543, + "modified": false + }, + "sess_1231621c39e4be33883a9f98e4e65dcc": { + "type": "-", + "size": 0, + "lmtime": 1771951456625, + "modified": false + }, + "sess_1a02d65c6d444701e5aa777fa3350b02": { + "type": "-", + "size": 0, + "lmtime": 1771951456698, + "modified": false + }, + "sess_1ac48fc3b316aa8058dccba965517ea8": { + "type": "-", + "size": 84, + "lmtime": 1771951456778, + "modified": false + }, + "sess_1dc258040e0ef869bccc02a7f675e35b": { + "type": "-", + "size": 84, + "lmtime": 1771951456854, + "modified": false + }, + "sess_1e8272aee19c4e1273ffd89d201d28e9": { + "type": "-", + "size": 84, + "lmtime": 1771951456919, + "modified": false + }, + "sess_1f08f0bcb0358fac2eabf881adac0337": { + "type": "-", + "size": 0, + "lmtime": 1771951456992, + "modified": false + }, + "sess_1f3926392e088a1ed013de95801677da": { + "type": "-", + "size": 84, + "lmtime": 1771951457067, + "modified": false + }, + "sess_208003ffcbae1f7d6d9349011d6d1ee6": { + "type": "-", + "size": 84, + "lmtime": 1771951457142, + "modified": false + }, + "sess_20878aa8a9661ab2ba1e021cbaf01099": { + "type": "-", + "size": 0, + "lmtime": 1771951457211, + "modified": false + }, + "sess_20c44599b672aeca6a3a7d3399fdcb12": { + "type": "-", + "size": 84, + "lmtime": 1771951457285, + "modified": false + }, + "sess_20e93a6d823cd699336b89776d52d0cc": { + "type": "-", + "size": 84, + "lmtime": 1771951457355, + "modified": false + }, + "sess_225dd98643950e6ef10a7ccb80b5d0fc": { + "type": "-", + "size": 84, + "lmtime": 1771951457429, + "modified": false + }, + "sess_2611f2ce8111f520de5016cf05e283e2": { + "type": "-", + "size": 0, + "lmtime": 1771951457498, + "modified": false + }, + "sess_27f0c0f3219137b3b55995d72410f16c": { + "type": "-", + "size": 84, + "lmtime": 1771951457573, + "modified": false + }, + "sess_28390340745925f528eab342affa8802": { + "type": "-", + "size": 84, + "lmtime": 1771951457638, + "modified": false + }, + "sess_29417dbfd5bc53725c6c86d29364b395": { + "type": "-", + "size": 0, + "lmtime": 1771951457705, + "modified": false + }, + "sess_29bc16e8104dc941012dbc7359afa9b4": { + "type": "-", + "size": 84, + "lmtime": 1771951457774, + "modified": false + }, + "sess_2b3c2840bd1352e72d552ca06ad8aadf": { + "type": "-", + "size": 0, + "lmtime": 1771951457849, + "modified": false + }, + "sess_2d36fb26c2071e7dcf8e070385c772d0": { + "type": "-", + "size": 0, + "lmtime": 1771951457918, + "modified": false + }, + "sess_2d7c48eebde8a80fac1d9fd655aac9fd": { + "type": "-", + "size": 84, + "lmtime": 1771951457992, + "modified": false + }, + "sess_2f70dacfc666777c014747c021749018": { + "type": "-", + "size": 84, + "lmtime": 1771951458064, + "modified": false + }, + "sess_3196f3a6689ea27522aea09c5ec526c4": { + "type": "-", + "size": 0, + "lmtime": 1771951458132, + "modified": false + }, + "sess_323059d3d84c5aa7c3fa7409796cec82": { + "type": "-", + "size": 84, + "lmtime": 1771951458200, + "modified": false + }, + "sess_3296c08e71ab8e3d475936fc51e999d5": { + "type": "-", + "size": 84, + "lmtime": 1771951458278, + "modified": false + }, + "sess_32cade31a6914d5c85d30c6e71ee03be": { + "type": "-", + "size": 0, + "lmtime": 1771951458346, + "modified": false + }, + "sess_37a347656366cda54bffe7adc7c6cb21": { + "type": "-", + "size": 84, + "lmtime": 1771951458453, + "modified": false + }, + "sess_3a0377895eee6bacdb19b272645bc06b": { + "type": "-", + "size": 84, + "lmtime": 1771951458522, + "modified": false + }, + "sess_3c5a1a7db2821c351896df33e46546c2": { + "type": "-", + "size": 84, + "lmtime": 1771951458604, + "modified": false + }, + "sess_3c8c88ff6bd8106a3eaa398d81e8151f": { + "type": "-", + "size": 84, + "lmtime": 1771951458671, + "modified": false + }, + "sess_3e375ba4cd0e11156b071b65a96a4284": { + "type": "-", + "size": 84, + "lmtime": 1771951458740, + "modified": false + }, "sess_3hu8an8qjm269lrbgrdj8hg4qf": { "type": "-", "size": 0, - "lmtime": 1771459760446, + "lmtime": 1771951458807, "modified": false }, "sess_3ja8jv4ed6toa9qr0akm6dopou": { "type": "-", "size": 84, - "lmtime": 1771459815608, + "lmtime": 1771951458882, "modified": false }, - "sess_76ae54rsc2nm1kea4shv8d7rnn": { - "type": "-", - "size": 188, - "lmtime": 1771459815683, - "modified": false - }, - "sess_mmv4rrajp3hbl7levd5bt5jrrg": { - "type": "-", - "size": 188, - "lmtime": 1771459767545, - "modified": false - }, - "sess_qlos51ql56hego9hj8m27hota1": { + "sess_40892db828938543f27c66817f8018dc": { "type": "-", "size": 84, - "lmtime": 1771459760450, + "lmtime": 1771951458945, "modified": false }, - "sess_redlci17vpou7obl939u3p81ov": { + "sess_4142bfafdbac16c0ec3f2dbc6a2d4495": { "type": "-", "size": 0, - "lmtime": 1771459815604, + "lmtime": 1771951459019, + "modified": false + }, + "sess_41a4dfc7b9583763240403d7b5bcb435": { + "type": "-", + "size": 84, + "lmtime": 1771951459093, + "modified": false + }, + "sess_421ed915f78b6ee09f45e35e1995f667": { + "type": "-", + "size": 84, + "lmtime": 1771951459156, + "modified": false + }, + "sess_44a075a46c5eba3ab48f3bc7fc53eb68": { + "type": "-", + "size": 84, + "lmtime": 1771951459226, + "modified": false + }, + "sess_45a1f70413aea2b83f9b665f02d8ba84": { + "type": "-", + "size": 84, + "lmtime": 1771951459307, + "modified": false + }, + "sess_460eb272697d17996afd5ac4dceccba4": { + "type": "-", + "size": 0, + "lmtime": 1771951459373, + "modified": false + }, + "sess_4b78c6dd1cd43d5236b360d4a41174ea": { + "type": "-", + "size": 0, + "lmtime": 1771951459440, + "modified": false + }, + "sess_4c4ed99c3b5ea1723add47cac79001f1": { + "type": "-", + "size": 84, + "lmtime": 1771951459509, + "modified": false + }, + "sess_4c7c17dc630c0e4fc7f45e3d05f2af53": { + "type": "-", + "size": 84, + "lmtime": 1771951459577, + "modified": false + }, + "sess_4eb429720096210532d18f7e59d045fc": { + "type": "-", + "size": 84, + "lmtime": 1771951459647, + "modified": false + }, + "sess_5593fde34fd1a5997a8b96451961b0d6": { + "type": "-", + "size": 84, + "lmtime": 1771951459719, + "modified": false + }, + "sess_5646fd9a5f6f4ef3d6453778f559423e": { + "type": "-", + "size": 84, + "lmtime": 1771951459791, + "modified": false + }, + "sess_5876213d76b132b50d60aaee65ff516d": { + "type": "-", + "size": 84, + "lmtime": 1771951459862, + "modified": false + }, + "sess_5904b82326e419278ab3ee17b2db03ec": { + "type": "-", + "size": 0, + "lmtime": 1771951459932, + "modified": false + }, + "sess_5a0401323d0ff982deea43035b35b032": { + "type": "-", + "size": 84, + "lmtime": 1771951460002, + "modified": false + }, + "sess_5ab2ff7c8cae71d9fcca9c76d1c70da3": { + "type": "-", + "size": 0, + "lmtime": 1771951460073, + "modified": false + }, + "sess_5c20224e4bf268303205ec72ba35a228": { + "type": "-", + "size": 0, + "lmtime": 1771951460140, + "modified": false + }, + "sess_5c2f0330eb7f634802fad6e68d1e53f2": { + "type": "-", + "size": 0, + "lmtime": 1771951460211, + "modified": false + }, + "sess_5dc85646cb40ca88ca311ea1a6e87239": { + "type": "-", + "size": 0, + "lmtime": 1771951460332, + "modified": false + }, + "sess_5dd9890739e11e24667fe200cea51ed1": { + "type": "-", + "size": 84, + "lmtime": 1771951460401, + "modified": false + }, + "sess_60c54ace856592e9c7dab7d173c95c11": { + "type": "-", + "size": 0, + "lmtime": 1771951460471, + "modified": false + }, + "sess_6351a387a4de2678c0666902c55a54cb": { + "type": "-", + "size": 0, + "lmtime": 1771951460541, + "modified": false + }, + "sess_635e172e85857077f246b2ec3823c714": { + "type": "-", + "size": 84, + "lmtime": 1771951460617, + "modified": false + }, + "sess_64f0b06c5f94dce967b207b64039c363": { + "type": "-", + "size": 84, + "lmtime": 1771951460679, + "modified": false + }, + "sess_6506e4c6c57b285243a09d15247fc38a": { + "type": "-", + "size": 0, + "lmtime": 1771951460746, + "modified": false + }, + "sess_6550071a1c1da0c8ab5bd3d39803aa4a": { + "type": "-", + "size": 84, + "lmtime": 1771951460815, + "modified": false + }, + "sess_6699b40a770c76a132d4f05554600d7b": { + "type": "-", + "size": 84, + "lmtime": 1771951460884, + "modified": false + }, + "sess_679b7571acfb6778bd18c4959199466e": { + "type": "-", + "size": 0, + "lmtime": 1771951460957, + "modified": false + }, + "sess_68402b530015238355efc94f159a1b7a": { + "type": "-", + "size": 0, + "lmtime": 1771951461029, + "modified": false + }, + "sess_6bc113e2a0b33d6849959138482dc598": { + "type": "-", + "size": 84, + "lmtime": 1771951461099, "modified": false }, "sess_6bvdsb449nmokurbt5upir865c": { "type": "-", "size": 84, - "lmtime": 1771459849455, + "lmtime": 1771951461171, + "modified": false + }, + "sess_6c7c3857e14d85139b7ef4c13079bd22": { + "type": "-", + "size": 84, + "lmtime": 1771951461239, + "modified": false + }, + "sess_6d86d3dc6f3e7e8ab202358b1f00520f": { + "type": "-", + "size": 0, + "lmtime": 1771951461307, + "modified": false + }, + "sess_6dab3412a10cb2debaa7b5af4814a777": { + "type": "-", + "size": 84, + "lmtime": 1771951461379, + "modified": false + }, + "sess_720cf685eef5b3d55f0d77528873b992": { + "type": "-", + "size": 0, + "lmtime": 1771951461454, + "modified": false + }, + "sess_76ae54rsc2nm1kea4shv8d7rnn": { + "type": "-", + "size": 188, + "lmtime": 1771951461523, + "modified": false + }, + "sess_7d8e12f1ec430d307beed810d45d9962": { + "type": "-", + "size": 239, + "lmtime": 1771951461597, + "modified": false + }, + "sess_7e0a91a31fecdffc2099662df9d8e473": { + "type": "-", + "size": 0, + "lmtime": 1771951461666, + "modified": false + }, + "sess_7fcb4456dfc5ce236ac8e45897d0831a": { + "type": "-", + "size": 0, + "lmtime": 1771951461738, + "modified": false + }, + "sess_854dbe080ec8bdc086892b3d0e9adfba": { + "type": "-", + "size": 0, + "lmtime": 1771951461807, + "modified": false + }, + "sess_87778d19cd15d41c60e2107205a0b6d6": { + "type": "-", + "size": 84, + "lmtime": 1771951461876, + "modified": false + }, + "sess_8c00e6f4622b798ef31988d886dc2e56": { + "type": "-", + "size": 84, + "lmtime": 1771951461946, + "modified": false + }, + "sess_8d118c110fdbca65c8ea2f461e4b54e1": { + "type": "-", + "size": 84, + "lmtime": 1771951462026, + "modified": false + }, + "sess_8f23314b6edefb8aa1d1b404e6f80a65": { + "type": "-", + "size": 0, + "lmtime": 1771951462091, + "modified": false + }, + "sess_8f5f540dac408c1d0ef944717e8640f7": { + "type": "-", + "size": 84, + "lmtime": 1771951462161, + "modified": false + }, + "sess_9019085d084f324714ed3edbb49aed98": { + "type": "-", + "size": 84, + "lmtime": 1771951462235, + "modified": false + }, + "sess_915da19bba34f9a8e32f0d6b8419a5b6": { + "type": "-", + "size": 239, + "lmtime": 1771951462304, + "modified": false + }, + "sess_926fa3d6608fc99e9c4d804a240522c3": { + "type": "-", + "size": 84, + "lmtime": 1771951462379, + "modified": false + }, + "sess_946b3b4e4bcabf0a8222ddb523cea981": { + "type": "-", + "size": 84, + "lmtime": 1771951462448, + "modified": false + }, + "sess_949392471ed0744a0f9cbae80e08d239": { + "type": "-", + "size": 0, + "lmtime": 1771951462533, + "modified": false + }, + "sess_94a39bd41f357ba9c5648ac04f81c3b1": { + "type": "-", + "size": 0, + "lmtime": 1771951462604, + "modified": false + }, + "sess_9501c0f892adb42b92706fd3fcda3008": { + "type": "-", + "size": 84, + "lmtime": 1771951462702, + "modified": false + }, + "sess_979f253f5982d6248e308e25c5a81331": { + "type": "-", + "size": 84, + "lmtime": 1771951462782, + "modified": false + }, + "sess_980b04f780d2f907f4c954cbdd8dd20b": { + "type": "-", + "size": 84, + "lmtime": 1771951462871, + "modified": false + }, + "sess_9a1dd26d2925008330b21aeb552ab377": { + "type": "-", + "size": 84, + "lmtime": 1771951462940, + "modified": false + }, + "sess_9aa184d56546b591aac03c96a9b18204": { + "type": "-", + "size": 84, + "lmtime": 1771951463021, + "modified": false + }, + "sess_9e624933406fb1cd80cadc8655245ca2": { + "type": "-", + "size": 84, + "lmtime": 1771951463132, + "modified": false + }, + "sess_9fbbc6ddedb2882206b1cd39ea61542e": { + "type": "-", + "size": 84, + "lmtime": 1771951463225, + "modified": false + }, + "sess_a517d8ddd6cce8f9e193f91080f2a3d5": { + "type": "-", + "size": 84, + "lmtime": 1771951463305, + "modified": false + }, + "sess_a5bbfa7c88f0b570f650c9acbaa4a9ee": { + "type": "-", + "size": 84, + "lmtime": 1771951463408, + "modified": false + }, + "sess_a789ae0403ad8ce89ae02596b96927b9": { + "type": "-", + "size": 0, + "lmtime": 1771951463499, + "modified": false + }, + "sess_a7cf283a18fbbaca955c104cf5b4e2fb": { + "type": "-", + "size": 0, + "lmtime": 1771951463582, + "modified": false + }, + "sess_aae5c39c7033be3ed55914159f4b2d80": { + "type": "-", + "size": 84, + "lmtime": 1771951463662, + "modified": false + }, + "sess_ab812a4c1327e3420f90a409bc343475": { + "type": "-", + "size": 0, + "lmtime": 1771951463741, + "modified": false + }, + "sess_ac21391ef46b927b94baf5528e2722d0": { + "type": "-", + "size": 0, + "lmtime": 1771951463831, + "modified": false + }, + "sess_ad2556fc46eb446e23112b6ba23ca5da": { + "type": "-", + "size": 0, + "lmtime": 1771951463921, + "modified": false + }, + "sess_ae170f3d304dc4ac36d3eabffbb54e85": { + "type": "-", + "size": 0, + "lmtime": 1771951464004, + "modified": false + }, + "sess_ae989998cbcd99eecfa73ef468aef345": { + "type": "-", + "size": 84, + "lmtime": 1771951464084, + "modified": false + }, + "sess_af07c7eac0ec591d17a7133ddfb0fb1c": { + "type": "-", + "size": 0, + "lmtime": 1771951464177, + "modified": false + }, + "sess_af7f67e2cd98979713b238146fc28a96": { + "type": "-", + "size": 0, + "lmtime": 1771951464267, + "modified": false + }, + "sess_af808d914ac3af2539dee6ff7ca925ab": { + "type": "-", + "size": 84, + "lmtime": 1771951464345, + "modified": false + }, + "sess_b0a72e826b12ffe2e46dccb8e4eaee27": { + "type": "-", + "size": 0, + "lmtime": 1771951464437, + "modified": false + }, + "sess_b14521484866259d933829d3e7f4ad89": { + "type": "-", + "size": 0, + "lmtime": 1771951464507, + "modified": false + }, + "sess_b1b6306497ddeec4fd327d42fa67c6dd": { + "type": "-", + "size": 84, + "lmtime": 1771951464575, + "modified": false + }, + "sess_b294cd0cdb3a504794b10756348e4b8d": { + "type": "-", + "size": 0, + "lmtime": 1771951464645, + "modified": false + }, + "sess_b2fd6faa7bb8fe8fc5e27471f16dcb8e": { + "type": "-", + "size": 84, + "lmtime": 1771951464714, + "modified": false + }, + "sess_b40d1bbc7c30a7bc80f9075bb9b24469": { + "type": "-", + "size": 0, + "lmtime": 1771951464782, + "modified": false + }, + "sess_b4dc8ebd43c00908390b9e6c4b722032": { + "type": "-", + "size": 84, + "lmtime": 1771951464852, + "modified": false + }, + "sess_b78562f081b628259babb86c6a69756e": { + "type": "-", + "size": 84, + "lmtime": 1771951464931, + "modified": false + }, + "sess_b81fcdb9dd34a72f9eecb861072ac770": { + "type": "-", + "size": 84, + "lmtime": 1771951465005, + "modified": false + }, + "sess_b96e63ef958afb6c7b1a67f2fab1b45f": { + "type": "-", + "size": 84, + "lmtime": 1771951465074, + "modified": false + }, + "sess_b9f968e9ddaee1343d177ad5d09103bc": { + "type": "-", + "size": 0, + "lmtime": 1771951465142, + "modified": false + }, + "sess_c1199a1c3d90f1da1bdb23923ca0d7ec": { + "type": "-", + "size": 0, + "lmtime": 1771951465213, + "modified": false + }, + "sess_c233a273877d0f13f622044c10ec7a27": { + "type": "-", + "size": 84, + "lmtime": 1771951465284, + "modified": false + }, + "sess_c25c0cb178f3711f905ee59a2ca9646c": { + "type": "-", + "size": 84, + "lmtime": 1771951465354, + "modified": false + }, + "sess_c2680664c034a887825dffe88e4cdf53": { + "type": "-", + "size": 0, + "lmtime": 1771951465426, + "modified": false + }, + "sess_c3e1bd69936be92470ab8516d1196128": { + "type": "-", + "size": 84, + "lmtime": 1771951465495, + "modified": false + }, + "sess_c63faa2381d1b925b436c321ed281de9": { + "type": "-", + "size": 84, + "lmtime": 1771951465565, + "modified": false + }, + "sess_cd2572bc0c9d203b80e9e4b923bbb169": { + "type": "-", + "size": 84, + "lmtime": 1771951465637, + "modified": false + }, + "sess_ce8e28637c028b602cdad332c753c969": { + "type": "-", + "size": 84, + "lmtime": 1771951465704, + "modified": false + }, + "sess_cf0f7ee63b9db629311e5c5387a8a9f1": { + "type": "-", + "size": 0, + "lmtime": 1771951465779, + "modified": false + }, + "sess_d1b046de8f8ecb2280433281f94c2c17": { + "type": "-", + "size": 0, + "lmtime": 1771951465848, + "modified": false + }, + "sess_d338628cbad5111a9fa8d4041bdb49ce": { + "type": "-", + "size": 0, + "lmtime": 1771951465917, + "modified": false + }, + "sess_d36925071c0f9aaf5e2706646811eb35": { + "type": "-", + "size": 84, + "lmtime": 1771951465985, + "modified": false + }, + "sess_d57c89d3193958c0ba8c71e7dba02b94": { + "type": "-", + "size": 84, + "lmtime": 1771951466054, + "modified": false + }, + "sess_d6775397e6eb1ebccbb9effb84bb8a4b": { + "type": "-", + "size": 84, + "lmtime": 1771951466124, + "modified": false + }, + "sess_d8e30d59d4def2eb1c2ce0f6626740d5": { + "type": "-", + "size": 84, + "lmtime": 1771951466193, + "modified": false + }, + "sess_d9e17d4b222b63ecbdc923b7d3fc5e1e": { + "type": "-", + "size": 84, + "lmtime": 1771951466260, + "modified": false + }, + "sess_dd39256bd99c630bf237443d65ec34d1": { + "type": "-", + "size": 84, + "lmtime": 1771951466327, + "modified": false + }, + "sess_de5da3d74ef4d2be86b2b8a648330ee4": { + "type": "-", + "size": 0, + "lmtime": 1771951466403, + "modified": false + }, + "sess_dedab89dd090ed903e4709c5a741fd58": { + "type": "-", + "size": 84, + "lmtime": 1771951466479, + "modified": false + }, + "sess_df9a21339eb15201d4b3c521cb490c7e": { + "type": "-", + "size": 239, + "lmtime": 1771951466561, + "modified": false + }, + "sess_e0d9ccad56e75d18f2465dd090772ef2": { + "type": "-", + "size": 84, + "lmtime": 1771951466632, + "modified": false + }, + "sess_e209038fc03a393bd0f9cfb6e8f6e8de": { + "type": "-", + "size": 0, + "lmtime": 1771951466702, + "modified": false + }, + "sess_e4690de68d4822f9c798558a9044bc38": { + "type": "-", + "size": 84, + "lmtime": 1771951466770, + "modified": false + }, + "sess_e700d0c47b2ebf7a0f42263e1c6264a2": { + "type": "-", + "size": 84, + "lmtime": 1771951466837, + "modified": false + }, + "sess_e7276630b9db477e3b275bc791dd66dc": { + "type": "-", + "size": 0, + "lmtime": 1771951466911, + "modified": false + }, + "sess_eac1f4e587a35c774c194076362bd53e": { + "type": "-", + "size": 0, + "lmtime": 1771951466980, + "modified": false + }, + "sess_eaf122fb84c04fb28cfbbaba84c594ce": { + "type": "-", + "size": 84, + "lmtime": 1771951467050, + "modified": false + }, + "sess_ebe71f457c8cb8fa7abb2dcd15bed8b0": { + "type": "-", + "size": 84, + "lmtime": 1771951467132, + "modified": false + }, + "sess_eca5fc38fb7552cf5540eef8f7dcb32d": { + "type": "-", + "size": 84, + "lmtime": 1771951467208, + "modified": false + }, + "sess_ecbe33f2c34abf04220d64fe0efe5e13": { + "type": "-", + "size": 84, + "lmtime": 1771951467279, + "modified": false + }, + "sess_ed9584a5891ce1677aba5eb2210e6335": { + "type": "-", + "size": 84, + "lmtime": 1771951467349, + "modified": false + }, + "sess_efeccd922b226f9c42f6262622bba3d8": { + "type": "-", + "size": 84, + "lmtime": 1771951467416, + "modified": false + }, + "sess_f6c84466922d260b8665bacde3898680": { + "type": "-", + "size": 84, + "lmtime": 1771951467506, + "modified": false + }, + "sess_f7870204b4cb15908af6e5beff181204": { + "type": "-", + "size": 84, + "lmtime": 1771951467576, + "modified": false + }, + "sess_f7e39236de4aad5cf07a5ba9ceb77b2b": { + "type": "-", + "size": 84, + "lmtime": 1771951467645, + "modified": false + }, + "sess_fa7b23410f34b4e1c7e1e3a7c0e3566d": { + "type": "-", + "size": 84, + "lmtime": 1771951467712, + "modified": false + }, + "sess_fce25fc00f20e34930f545640a7f5476": { + "type": "-", + "size": 84, + "lmtime": 1771951467784, + "modified": false + }, + "sess_ffc694af573cd0d40e16c05bf80bb94b": { + "type": "-", + "size": 84, + "lmtime": 1771951467854, + "modified": false + }, + "sess_mmv4rrajp3hbl7levd5bt5jrrg": { + "type": "-", + "size": 188, + "lmtime": 1771951467927, + "modified": false + }, + "sess_qlos51ql56hego9hj8m27hota1": { + "type": "-", + "size": 84, + "lmtime": 1771951468004, + "modified": false + }, + "sess_redlci17vpou7obl939u3p81ov": { + "type": "-", + "size": 0, + "lmtime": 1771951468075, "modified": false } }, diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2d780a9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP \ No newline at end of file diff --git a/DOCS/CRON_QUEUE.md b/DOCS/CRON_QUEUE.md new file mode 100644 index 0000000..0d5d794 --- /dev/null +++ b/DOCS/CRON_QUEUE.md @@ -0,0 +1,27 @@ +# Kolejka Cron (DB) + +## Cel +- Zadania cron sa zapisywane w bazie (`cron_jobs`) i planowane przez harmonogram (`cron_schedules`). +- Aktualnie domyslnie dziala zadanie: `product_links_health_check` (co 7 dni). + +## Tabele +- `cron_jobs` - kolejka zadan z priorytetem, retry i backoff. +- `cron_schedules` - definicje cyklicznych zadan. +- `product_link_alerts` - alerty dla nieistniejacych powiazan produktu. + +## Uruchamianie +- Jednorazowo: `php bin/cron.php` +- Z limitem batcha: `php bin/cron.php --limit=50` +- Z panelu (`Ustawienia -> Cron`) mozna wlaczyc uruchamianie workera podczas requestow HTTP. + +## Zalecenie dla systemowego crona +- Uruchamiaj `php /sciezka/do/orderPRO/bin/cron.php` co 1-5 minut. +- Harmonogram 7-dniowy jest liczony przez `cron_schedules.next_run_at`, wiec sam worker powinien byc uruchamiany regularnie. + +## Jak dziala `product_links_health_check` +1. Pobiera aktywne integracje `shoppro` z API key. +2. Odswieza cache ofert (`channel_offers`) przez import API. +3. Czyści nieaktualne rekordy ofert z cache. +4. Weryfikuje aktywne powiazania `product_channel_map`. +5. Dla brakujacych powiazan ustawia alert `missing_remote_link`. +6. Dla przywroconych powiazan zamyka alert. diff --git a/DOCS/TODO.md b/DOCS/TODO.md index 4726825..e166623 100644 --- a/DOCS/TODO.md +++ b/DOCS/TODO.md @@ -1,4 +1,7 @@ 1. Na podglądzie produktu zmień wyświetlanie zdjęć na siatkę (grid) 2. W tabelach w filtrach filtr Na strone nie jest potrzebny bo ta opcja jest na dole w stronicowaniu. 3. W tabelach w sortowanie nie jest potrzebny bo ta opcja jest dostępna w nagłowkach tabel. -4. https://orderpro.projectpro.pl/products/8 rozszerzyć kolumnę z nazwami parametrów \ No newline at end of file +4. https://orderpro.projectpro.pl/products/8 rozszerzyć kolumnę z nazwami parametrów +5. Rozbudować dane o producencie o pola z shopPRO +6. ~~https://orderpro.projectpro.pl/products dodać kolumnę z EAN~~ +7. https://orderpro.projectpro.pl/products domyślnie sortowanie po dacie dodanie DESC \ No newline at end of file diff --git a/MojeGS1 API.htm b/MojeGS1 API.htm new file mode 100644 index 0000000..718ccf5 --- /dev/null +++ b/MojeGS1 API.htm @@ -0,0 +1,21 @@ + + + + + + MojeGS1 API + + + + + + + + +
+ + + + + + diff --git a/bin/cron.php b/bin/cron.php new file mode 100644 index 0000000..481fee5 --- /dev/null +++ b/bin/cron.php @@ -0,0 +1,84 @@ + $dbConfig */ +$dbConfig = require $basePath . '/config/database.php'; +/** @var array $appConfig */ +$appConfig = require $basePath . '/config/app.php'; + +$limit = 20; +foreach ($argv as $argument) { + if (!str_starts_with((string) $argument, '--limit=')) { + continue; + } + + $limitValue = (int) substr((string) $argument, strlen('--limit=')); + if ($limitValue > 0) { + $limit = min(200, $limitValue); + } +} + +try { + $pdo = ConnectionFactory::make($dbConfig); + + $cronJobs = new CronJobRepository($pdo); + $processor = new CronJobProcessor($cronJobs); + + $integrationRepository = new IntegrationRepository( + $pdo, + (string) (($appConfig['integrations']['secret'] ?? '') ?: '') + ); + $offersRepository = new ChannelOffersRepository($pdo); + $linksRepository = new ProductLinksRepository($pdo); + $shopProClient = new ShopProClient(); + $offerImportService = new OfferImportService($shopProClient, $offersRepository, $pdo); + $linksHealthCheckHandler = new ProductLinksHealthCheckHandler( + $integrationRepository, + $offerImportService, + $linksRepository, + $offersRepository + ); + + $processor->registerHandler(CronJobType::PRODUCT_LINKS_HEALTH_CHECK, $linksHealthCheckHandler); + + $result = $processor->run($limit); + + echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; +} catch (\Throwable $exception) { + fwrite(STDERR, '[error] ' . $exception->getMessage() . PHP_EOL); + exit(1); +} diff --git a/bin/test_gs1_api.php b/bin/test_gs1_api.php new file mode 100644 index 0000000..518f1c7 --- /dev/null +++ b/bin/test_gs1_api.php @@ -0,0 +1,231 @@ + [gtin] + * + * If login/password are not provided, script tries to read them from DB app_settings. + */ + +$login = $argv[1] ?? ''; +$password = $argv[2] ?? ''; +$targetGtin = $argv[3] ?? '5905323904514'; + +if ($login === '' || $password === '') { + $envFile = dirname(__DIR__) . '/.env'; + $env = []; + if (is_file($envFile)) { + foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') { + continue; + } + $pos = strpos($line, '='); + if ($pos === false) { + continue; + } + $env[trim(substr($line, 0, $pos))] = trim(trim(substr($line, $pos + 1)), "\"'"); + } + } + + try { + $pdo = new PDO( + sprintf( + 'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', + $env['DB_HOST'] ?? '127.0.0.1', + $env['DB_PORT'] ?? '3306', + $env['DB_DATABASE'] ?? 'orderpro' + ), + $env['DB_USERNAME'] ?? 'root', + $env['DB_PASSWORD'] ?? '', + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] + ); + + $stmt = $pdo->prepare('SELECT setting_key, setting_value FROM app_settings WHERE setting_key IN (?, ?)'); + $stmt->execute(['gs1_api_login', 'gs1_api_password']); + foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { + if ($row['setting_key'] === 'gs1_api_login') { + $login = (string) $row['setting_value']; + } + if ($row['setting_key'] === 'gs1_api_password') { + $password = (string) $row['setting_value']; + } + } + } catch (Throwable) { + // Ignore and use CLI args fallback. + } +} + +if ($login === '' || $password === '') { + fwrite(STDERR, "Brak credentials. Uzyj: php bin/test_gs1_api.php [gtin]\n"); + exit(1); +} + +if (!preg_match('/^\d{13,14}$/', $targetGtin)) { + fwrite(STDERR, "Nieprawidlowy GTIN: {$targetGtin}. Oczekiwane 13 lub 14 cyfr.\n"); + exit(1); +} + +const GS1_BASE = 'https://mojegs1.pl/api/v2'; + +/** + * @return array{status:int, body:string, error:string} + */ +function gs1Request( + string $method, + string $url, + string $login, + string $password, + ?string $rawBody = null, + string $contentType = 'application/json', + string $accept = 'application/json' +): array { + $curl = curl_init($url); + if ($curl === false) { + return ['status' => 0, 'body' => '', 'error' => 'Nie mozna zainicjalizowac cURL']; + } + + $headers = ['Accept: ' . $accept]; + if ($rawBody !== null) { + $headers[] = 'Content-Type: ' . $contentType; + } + + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_USERPWD => $login . ':' . $password, + CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_CUSTOMREQUEST => $method, + ]); + + if ($rawBody !== null) { + curl_setopt($curl, CURLOPT_POSTFIELDS, $rawBody); + } + + $body = curl_exec($curl); + $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + $error = curl_error($curl); + curl_close($curl); + + return [ + 'status' => $status, + 'body' => is_string($body) ? $body : '', + 'error' => $error, + ]; +} + +function toJson(mixed $value): string +{ + $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + return $json === false ? '[json_encode error]' : $json; +} + +function printResponse(string $label, array $response): void +{ + echo $label . PHP_EOL; + echo 'HTTP: ' . $response['status'] . PHP_EOL; + if ($response['error'] !== '') { + echo 'cURL error: ' . $response['error'] . PHP_EOL; + } + if ($response['body'] === '') { + echo "BODY: [empty]\n\n"; + return; + } + + $decoded = json_decode($response['body'], true); + if (is_array($decoded)) { + echo "BODY:\n" . toJson($decoded) . PHP_EOL; + if (!empty($decoded['errors'])) { + echo "ERRORS:\n" . toJson($decoded['errors']) . PHP_EOL; + } + } else { + echo "BODY:\n" . $response['body'] . PHP_EOL; + } + echo PHP_EOL; +} + +function putProduct(string $gtin, array $attributes, string $login, string $password): array +{ + $payload = [ + 'data' => [ + 'type' => 'products', + 'id' => $gtin, + 'attributes' => $attributes, + ], + ]; + + return gs1Request( + 'PUT', + GS1_BASE . '/products/' . rawurlencode($gtin), + $login, + $password, + json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ); +} + +echo "=== GS1 Diagnostic v5 ===\n"; +echo "Target GTIN: {$targetGtin}\n\n"; + +// 1) Check target GTIN +$targetGet = gs1Request('GET', GS1_BASE . '/products/' . rawurlencode($targetGtin), $login, $password); +printResponse('--- Step 1: GET target GTIN ---', $targetGet); + +// 2) PUT with values aligned to external API swagger example (language as `pl`) +$attrsSwaggerLike = [ + 'brandName' => 'Test Marka', + 'subBrandName' => 'Test Podmarka', + 'commonName' => 'Test produkt', + 'name' => 'Test produkt API', + 'description' => 'Test opisu produktu', + 'gpcCode' => 10000002, + 'netContent' => 1.5, + 'netContentUnit' => 'kg', + 'status' => 'ACT', + 'targetMarket' => ['PL'], + 'descriptionLanguage' => 'pl', +]; +$putTargetSwaggerLike = putProduct($targetGtin, $attrsSwaggerLike, $login, $password); +printResponse('--- Step 2: PUT target GTIN (swagger-like payload) ---', $putTargetSwaggerLike); + +// 3) PUT with original app defaults (for direct comparison with old script behavior) +$attrsAppLike = [ + 'brandName' => 'marianek.pl', + 'subBrandName' => 'marianek.pl', + 'commonName' => 'Produkt testowy', + 'name' => 'Produkt testowy API', + 'gpcCode' => 10008365, + 'netContent' => 1, + 'netContentUnit' => 'szt', + 'status' => 'ACT', + 'targetMarket' => ['PL'], + 'descriptionLanguage' => 'PL', +]; +$putTargetAppLike = putProduct($targetGtin, $attrsAppLike, $login, $password); +printResponse('--- Step 3: PUT target GTIN (app-like payload) ---', $putTargetAppLike); + +// 4) Permission check: read an existing product and try no-op PUT. +$list = gs1Request('GET', GS1_BASE . '/products?page[offset]=1&page[limit]=1&sort=name', $login, $password); +printResponse('--- Step 4a: GET list (first product) ---', $list); + +$existingGtin = null; +$existingAttributes = null; +$listDecoded = json_decode($list['body'], true); +if (is_array($listDecoded) && isset($listDecoded['data'][0]['id'])) { + $existingGtin = (string) $listDecoded['data'][0]['id']; + $existingAttributes = $listDecoded['data'][0]['attributes'] ?? null; +} + +if ($existingGtin !== null && is_array($existingAttributes)) { + $noOpPut = putProduct($existingGtin, $existingAttributes, $login, $password); + printResponse('--- Step 4b: PUT existing product with attributes from GET (no-op) ---', $noOpPut); +} else { + echo "--- Step 4b: skipped (no product data from list) ---\n\n"; +} + +echo "=== DONE ===\n"; diff --git a/composer.json b/composer.json index 83c1514..a5857a7 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ }, "scripts": { "serve": "php -S localhost:8000 -t public public/index.php", - "migrate": "php bin/migrate.php" + "migrate": "php bin/migrate.php", + "cron": "php bin/cron.php" } } diff --git a/config/app.php b/config/app.php index 0ab9e76..96720f3 100644 --- a/config/app.php +++ b/config/app.php @@ -16,6 +16,10 @@ return [ 'integrations' => [ 'secret' => Env::get('INTEGRATIONS_SECRET', ''), ], + 'cron' => [ + 'run_on_web_default' => Env::bool('CRON_RUN_ON_WEB', false), + 'web_limit_default' => max(1, min(100, (int) Env::get('CRON_WEB_LIMIT', '5'))), + ], 'view_path' => dirname(__DIR__) . '/resources/views', 'lang_path' => dirname(__DIR__) . '/resources/lang', 'log_path' => dirname(__DIR__) . '/storage/logs/app.log', diff --git a/database/migrations/20260224_000008_add_stock_0_buy_to_product_variants.sql b/database/migrations/20260224_000008_add_stock_0_buy_to_product_variants.sql new file mode 100644 index 0000000..0f3a92f --- /dev/null +++ b/database/migrations/20260224_000008_add_stock_0_buy_to_product_variants.sql @@ -0,0 +1,2 @@ +ALTER TABLE product_variants + ADD COLUMN stock_0_buy TINYINT(1) NOT NULL DEFAULT 0 AFTER status; diff --git a/database/migrations/20260224_000009_add_gpsr_and_custom_fields.sql b/database/migrations/20260224_000009_add_gpsr_and_custom_fields.sql new file mode 100644 index 0000000..c60ae7c --- /dev/null +++ b/database/migrations/20260224_000009_add_gpsr_and_custom_fields.sql @@ -0,0 +1,7 @@ +-- Add security_information (GPSR) to product_translations +ALTER TABLE product_translations + ADD COLUMN security_information MEDIUMTEXT NULL AFTER seo_link; + +-- Add custom_fields_json (Dodatkowe pola) to products +ALTER TABLE products + ADD COLUMN custom_fields_json TEXT NULL AFTER product_unit_id; diff --git a/database/migrations/20260224_000010_add_producer_name_to_products.sql b/database/migrations/20260224_000010_add_producer_name_to_products.sql new file mode 100644 index 0000000..cfa4116 --- /dev/null +++ b/database/migrations/20260224_000010_add_producer_name_to_products.sql @@ -0,0 +1 @@ +ALTER TABLE products ADD COLUMN producer_name VARCHAR(255) NULL AFTER producer_id; diff --git a/database/migrations/20260224_000011_create_cron_and_product_link_alerts_tables.sql b/database/migrations/20260224_000011_create_cron_and_product_link_alerts_tables.sql new file mode 100644 index 0000000..2b6a52e --- /dev/null +++ b/database/migrations/20260224_000011_create_cron_and_product_link_alerts_tables.sql @@ -0,0 +1,76 @@ +CREATE TABLE IF NOT EXISTS cron_jobs ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + job_type VARCHAR(80) NOT NULL, + status ENUM('pending', 'processing', 'completed', 'failed', 'cancelled') NOT NULL DEFAULT 'pending', + priority TINYINT UNSIGNED NOT NULL DEFAULT 100, + payload JSON NULL, + result JSON NULL, + attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0, + max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 3, + last_error VARCHAR(500) NULL, + scheduled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + started_at DATETIME NULL, + completed_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + KEY cron_jobs_status_priority_scheduled_idx (status, priority, scheduled_at), + KEY cron_jobs_job_type_idx (job_type), + KEY cron_jobs_scheduled_at_idx (scheduled_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS cron_schedules ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + job_type VARCHAR(80) NOT NULL, + interval_seconds INT UNSIGNED NOT NULL, + priority TINYINT UNSIGNED NOT NULL DEFAULT 100, + max_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 3, + payload JSON NULL, + enabled TINYINT(1) NOT NULL DEFAULT 1, + last_run_at DATETIME NULL, + next_run_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 cron_schedules_job_type_unique (job_type), + KEY cron_schedules_enabled_next_run_idx (enabled, next_run_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS product_link_alerts ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + product_channel_map_id INT UNSIGNED NOT NULL, + alert_type VARCHAR(64) NOT NULL, + status ENUM('active', 'resolved') NOT NULL DEFAULT 'active', + message VARCHAR(255) NOT NULL, + first_detected_at DATETIME NOT NULL, + last_detected_at DATETIME NOT NULL, + resolved_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 product_link_alerts_map_alert_type_unique (product_channel_map_id, alert_type), + KEY product_link_alerts_status_type_idx (status, alert_type), + KEY product_link_alerts_first_detected_at_idx (first_detected_at), + CONSTRAINT product_link_alerts_map_fk + FOREIGN KEY (product_channel_map_id) REFERENCES product_channel_map(id) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO cron_schedules ( + job_type, interval_seconds, priority, max_attempts, payload, enabled, last_run_at, next_run_at, created_at, updated_at +) VALUES ( + 'product_links_health_check', + 604800, + 110, + 3, + NULL, + 1, + NULL, + NOW(), + NOW(), + NOW() +) +ON DUPLICATE KEY UPDATE + interval_seconds = VALUES(interval_seconds), + priority = VALUES(priority), + max_attempts = VALUES(max_attempts), + payload = VALUES(payload), + enabled = VALUES(enabled), + updated_at = VALUES(updated_at); diff --git a/database/migrations/20260224_000012_create_app_settings_table.sql b/database/migrations/20260224_000012_create_app_settings_table.sql new file mode 100644 index 0000000..b4a133d --- /dev/null +++ b/database/migrations/20260224_000012_create_app_settings_table.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS app_settings ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + setting_key VARCHAR(120) NOT NULL, + setting_value TEXT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY app_settings_setting_key_unique (setting_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at) +VALUES + ('cron_run_on_web', '0', NOW(), NOW()), + ('cron_web_limit', '5', NOW(), NOW()) +ON DUPLICATE KEY UPDATE + setting_value = VALUES(setting_value), + updated_at = VALUES(updated_at); diff --git a/database/migrations/20260224_000013_add_gs1_settings.sql b/database/migrations/20260224_000013_add_gs1_settings.sql new file mode 100644 index 0000000..ceef047 --- /dev/null +++ b/database/migrations/20260224_000013_add_gs1_settings.sql @@ -0,0 +1,9 @@ +INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at) +VALUES + ('gs1_api_login', '', NOW(), NOW()), + ('gs1_api_password', '', NOW(), NOW()), + ('gs1_prefix', '590532390', NOW(), NOW()), + ('gs1_default_brand', 'marianek.pl', NOW(), NOW()), + ('gs1_default_gpc_code', '10008365', NOW(), NOW()) +ON DUPLICATE KEY UPDATE + updated_at = VALUES(updated_at); diff --git a/log.md b/log.md new file mode 100644 index 0000000..37f9c32 --- /dev/null +++ b/log.md @@ -0,0 +1,86 @@ +=== GS1 Diagnostic v4 === + +--- Step 1: GET existing product --- +GTIN: 5905316173910 +Status: WIT + +--- Step 2: Verbose PUT (minimal + name + subBrandName) --- +REQ: {"data":{"type":"products","id":"5905316173910","attributes":{"brandName":"bibs","subBrandName":"bibs","commonName":"smoczek","name":"bibs smoczek Baby Blue - On size","gpcCode":10000504,"netContent":1,"netContentUnit":"szt","status":"WIT","targetMarket":["PL"],"descriptionLanguage":"PL"}}} +HTTP 400: {"status":400,"title":"Niepoprawny parametr","detail":"Pole zawiera niepoprawną wartość","errors":[]} +VERBOSE: +* Host mojegs1.pl:443 was resolved. +* IPv6: (none) +* IPv4: 9.223.73.230 +* Trying 9.223.73.230:443... +* ALPN: curl offers h2,http/1.1 +* CAfile: /etc/pki/tls/certs/ca-bundle.crt +* CApath: none +* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / [blank] / UNDEF +* ALPN: server accepted h2 +* Server certificate: +* subject: C=PL; ST=wielkopolskie; L=Pozna\U0144; O=Fundacja GS1 Polska; CN=*.mojegs1.pl +* start date: Oct 24 09:41:36 2025 GMT +* expire date: Oct 24 09:41:35 2026 GMT +* subjectAltName: host "mojegs1.pl" matched cert's "mojegs1.pl" +* issuer: C=PL; O=Asseco Data Systems S.A.; CN=Certum OV TLS G2 R39 CA +* SSL certificate verify ok. +* Certificate level 0: Public key type ? (2048/112 Bits/secBits), signed using sha256WithRSAEncryption +* Certificate level 1: Public key type ? (4096/128 Bits/secBits), signed using sha512WithRSAEncryption +* Certificate level 2: Public key type ? (4096/128 Bits/secBits), signed using sha512WithRSAEncryption +* Connected to mojegs1.pl (9.223.73.230) port 443 +* using HTTP/2 +* Server auth using Basic with user 'user_85c27342-bc97-4f42-8890-f6b27d3233c4' +* [HTTP/2] [1] OPENED stream for https://mojegs1.pl/api/v2/products/5905316173910 +* [HTTP/2] [1] [:method: PUT] +* [HTTP/2] [1] [:scheme: https] +* [HTTP/2] [1] [:authority: mojegs1.pl] +* [HTTP/2] [1] [:path: /api/v2/products/5905316173910] +* [HTTP/2] [1] [authorization: Basic dXNlcl84NWMyNzM0Mi1iYzk3LTRmNDItODg5MC1mNmIyN2QzMjMzYzQ6SzNzYXdHQTNYP0w/ZV5iSiRacXloaWVGRyl3I2M4Zis/Vl56] +* [HTTP/2] [1] [accept: application/json] +* [HTTP/2] [1] [content-type: application/json] +* [HTTP/2] [1] [content-length: 291] +> PUT /api/v2/products/5905316173910 HTTP/2 +Host: mojegs1.pl +Authorization: Basic dXNlcl84NWMyNzM0Mi1iYzk3LTRmNDItODg5MC1mNmIyN2QzMjMzYzQ6SzNzYXdHQTNYP0w/ZV5iSiRacXloaWVGRyl3I2M4Zis/Vl56 +Accept: application/json +Content-Type: application/json +Content-Length: 291 + +* upload completely sent off: 291 bytes +< HTTP/2 400 +< date: Tue, 24 Feb 2026 20:08:31 GMT +< content-type: application/json; charset=utf-8 +< server: nginx +< +* Connection #0 to host mojegs1.pl left intact + + +--- Step 3: PATCH instead of PUT --- +HTTP 405: + +--- Step 4: gpcCode as string --- +REQ: {"data":{"type":"products","id":"5905316173910","attributes":{"brandName":"bibs","subBrandName":"bibs","commonName":"smoczek","name":"bibs smoczek Baby Blue - On size","gpcCode":"10000504","netContent":1,"netContentUnit":"szt","status":"WIT","targetMarket":["PL"],"descriptionLanguage":"PL"}}} +HTTP 400: {"status":400,"title":"Niepoprawny parametr","detail":"Pole zawiera niepoprawną wartość","errors":[]} + +--- Step 5: vnd.api+json content type --- +HTTP 400: {"status":400,"title":"Niepoprawny parametr","detail":"Pole zawiera niepoprawną wartość","errors":[]} + +--- Step 6: netContent as float 1.0 --- +REQ: {"data":{"type":"products","id":"5905316173910","attributes":{"brandName":"bibs","subBrandName":"bibs","commonName":"smoczek","name":"bibs smoczek Baby Blue - On size","gpcCode":10000504,"netContent":1.0,"netContentUnit":"szt","status":"WIT","targetMarket":["PL"],"descriptionLanguage":"PL"}}} +HTTP 400: {"status":400,"title":"Niepoprawny parametr","detail":"Pole zawiera niepoprawną wartość","errors":[]} + +--- Step 7: Empty attributes --- +REQ: {"data":{"type":"products","id":"5905316173910","attributes":{}}} +HTTP 400: {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Data.Attributes.Status":["The Status field is required."],"Data.Attributes.TargetMarket":["The TargetMarket field is required."]},"traceId":"00-7f7d0419a26ec9bd0d2f56422ca3b837-5e5d8704f5b4d14e-01"} + +--- Step 8: Single field tests --- + brandName => HTTP 400 + commonName => HTTP 400 + gpcCode => HTTP 400 + netContent => HTTP 400 + netContentUnit => HTTP 400 + status => HTTP 400 + targetMarket => HTTP 400 + descriptionLanguage => HTTP 400 + +=== DONE === diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 75ec4bb..5891f8e 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.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:38px;padding:8px 16px;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:38px;border:1px solid var(--c-border);border-radius:8px;padding:7px 12px;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:7px}.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}.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:14px;color:var(--c-text);background:var(--c-bg)}a{color:var(--c-primary)}.app-shell{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.sidebar{border-right:1px solid var(--c-border);border-right-color:#243041;background:#111a28;padding:18px 14px}.sidebar__brand{margin:4px 10px 16px;color:#e9f0ff;font-size:24px;font-weight:300;letter-spacing:-0.02em}.sidebar__brand strong{font-weight:700}.sidebar__nav{display:grid;gap:6px}.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}.app-main{min-width:0}.topbar{height:56px;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% - 28px);margin:18px 14px;padding:0 6px 24px}.card{background:var(--c-surface);border-radius:10px;box-shadow:var(--shadow-card);padding:24px}.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:20px;font-weight:700}.mt-12{margin-top:12px}.mt-16{margin-top:16px}.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}.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-actions{display:flex;gap:8px;flex-wrap:wrap}.field-inline{display:flex;align-items:center;gap:8px;margin-top:6px}.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-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-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-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}.product-show-image{width:100%;max-height:260px;object-fit:cover;border-radius:8px;border:1px solid #d9e0ea}@media(max-width: 768px){.app-shell{grid-template-columns:1fr}.sidebar{border-right:0;border-bottom:1px solid #243041;padding:14px}.sidebar__brand{margin:0 0 10px;font-size:22px}.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 4px 18px}.settings-grid{grid-template-columns:1fr}.page-head{flex-direction:column;align-items:flex-start}.filters-grid,.form-grid,.table-list-filters,.product-links-search-form,.product-links-inline-form{grid-template-columns:1fr}.filters-actions{align-items:center}.table-list__header,.table-list__footer{align-items:flex-start}.product-links-head{grid-template-columns:1fr}.card{padding:18px}.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:38px;padding:8px 16px;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:38px;border:1px solid var(--c-border);border-radius:8px;padding:7px 12px;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:7px}.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}.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:14px;color:var(--c-text);background:var(--c-bg)}a{color:var(--c-primary)}.app-shell{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.sidebar{border-right:1px solid var(--c-border);border-right-color:#243041;background:#111a28;padding:18px 14px}.sidebar__brand{margin:4px 10px 16px;color:#e9f0ff;font-size:24px;font-weight:300;letter-spacing:-0.02em}.sidebar__brand strong{font-weight:700}.sidebar__nav{display:grid;gap:6px}.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:6px}.sidebar__group-toggle{list-style:none;border-radius:8px;padding:10px 12px;color:#cbd5e1;font-weight:600;cursor:pointer}.sidebar__group-toggle::-webkit-details-marker{display:none}.sidebar__group:hover .sidebar__group-toggle{color:#f8fafc;background:#1b2a3f}.sidebar__group.is-active .sidebar__group-toggle{color:#fff;background:#2e4f93}.sidebar__group-links{display:grid;gap:4px;padding-left:8px}.sidebar__sublink{border-radius:8px;padding:8px 10px;text-decoration:none;color:#cbd5e1;font-size:13px;font-weight:500}.sidebar__sublink:hover{color:#f8fafc;background:#1b2a3f}.sidebar__sublink.is-active{color:#fff;background:#2e4f93}.app-main{min-width:0}.topbar{height:56px;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% - 28px);margin:18px 14px;padding:0 6px 24px}.card{background:var(--c-surface);border-radius:10px;box-shadow:var(--shadow-card);padding:24px}.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:20px;font-weight:700}.mt-12{margin-top:12px}.mt-16{margin-top:16px}.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}.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-actions{display:flex;gap:8px;flex-wrap:wrap}.field-inline{display:flex;align-items:center;gap:8px;margin-top:6px}.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-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}.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}.product-show-image{width:100%;max-height:260px;object-fit:cover;border-radius:8px;border:1px solid #d9e0ea}@media(max-width: 768px){.app-shell{grid-template-columns:1fr}.sidebar{border-right:0;border-bottom:1px solid #243041;padding:14px}.sidebar__brand{margin:0 0 10px;font-size:22px}.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 4px 18px}.settings-grid{grid-template-columns:1fr}.page-head{flex-direction:column;align-items:flex-start}.filters-grid,.form-grid,.table-list-filters,.product-links-search-form,.product-links-inline-form{grid-template-columns:1fr}.filters-actions{align-items:center}.table-list__header,.table-list__footer{align-items:flex-start}.product-links-head{grid-template-columns:1fr}.card{padding:18px}.modal--image-preview{width:min(92vw,100%)}} diff --git a/public/test_gs1.php b/public/test_gs1.php new file mode 100644 index 0000000..93e8186 --- /dev/null +++ b/public/test_gs1.php @@ -0,0 +1,159 @@ + PDO::ERRMODE_EXCEPTION]); + $s = $pdo->prepare('SELECT setting_key, setting_value FROM app_settings WHERE setting_key LIKE ?'); + $s->execute(['gs1_%']); + $cfg = []; + foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) $cfg[$r['setting_key']] = $r['setting_value']; +} catch (Throwable $e) { die('DB: ' . $e->getMessage()); } + +$login = $cfg['gs1_api_login'] ?? ''; +$password = $cfg['gs1_api_password'] ?? ''; +if ($login === '' || $password === '') die("Brak credentials\n"); + +function gs1v(string $method, string $url, ?string $body, string $login, string $pw, array $extraHeaders = []): array { + $curl = curl_init($url); + $h = ['Accept: application/json']; + if ($body !== null) $h[] = 'Content-Type: application/json'; + $h = array_merge($h, $extraHeaders); + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $h, + CURLOPT_USERPWD => $login . ':' . $pw, CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_VERBOSE => true, + ]); + // Capture verbose output + $verbose = fopen('php://temp', 'w+'); + curl_setopt($curl, CURLOPT_STDERR, $verbose); + if ($body !== null) curl_setopt($curl, CURLOPT_POSTFIELDS, $body); + $resp = curl_exec($curl); + $code = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + curl_close($curl); + rewind($verbose); + $verboseLog = stream_get_contents($verbose); + fclose($verbose); + return [$code, is_string($resp) ? $resp : '', $verboseLog]; +} + +$base = 'https://mojegs1.pl/api/v2'; +$jf = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; + +echo "=== GS1 Diagnostic v4 ===\n\n"; + +// Step 1: GET existing product +echo "--- Step 1: GET existing product ---\n"; +[$c, $b, $v] = gs1v('GET', $base . '/products?page[offset]=1&page[limit]=1', null, $login, $password); +$d = json_decode($b, true); +$existing = $d['data'][0] ?? null; +if (!$existing) die("Nie mozna pobrac produktu\n"); +$exGtin = $existing['id']; +$exAttrs = $existing['attributes']; +echo "GTIN: {$exGtin}\n"; +echo "Status: {$exAttrs['status']}\n\n"; + +// Step 2: Verbose PUT - minimal + subBrandName + name +echo "--- Step 2: Verbose PUT (minimal + name + subBrandName) ---\n"; +$attrs2 = [ + 'brandName' => $exAttrs['brandName'], + 'subBrandName' => $exAttrs['brandName'], + 'commonName' => $exAttrs['commonName'], + 'name' => $exAttrs['name'] ?? ($exAttrs['brandName'] . ' ' . $exAttrs['commonName']), + 'gpcCode' => $exAttrs['gpcCode'], + 'netContent' => $exAttrs['netContent'], + 'netContentUnit' => $exAttrs['netContentUnit'], + 'status' => $exAttrs['status'], + 'targetMarket' => $exAttrs['targetMarket'], + 'descriptionLanguage' => $exAttrs['descriptionLanguage'], +]; +$payload2 = json_encode(['data' => ['type' => 'products', 'id' => $exGtin, 'attributes' => $attrs2]], $jf); +echo "REQ: {$payload2}\n"; +[$c, $b, $v] = gs1v('PUT', $base . '/products/' . $exGtin, $payload2, $login, $password); +echo "HTTP {$c}: {$b}\n"; +echo "VERBOSE:\n{$v}\n\n"; + +// Step 3: Try PATCH instead of PUT +echo "--- Step 3: PATCH instead of PUT ---\n"; +[$c, $b, $v] = gs1v('PATCH', $base . '/products/' . $exGtin, $payload2, $login, $password); +echo "HTTP {$c}: {$b}\n\n"; + +// Step 4: Try gpcCode as string +echo "--- Step 4: gpcCode as string ---\n"; +$attrs4 = $attrs2; +$attrs4['gpcCode'] = (string) $attrs4['gpcCode']; +$payload4 = json_encode(['data' => ['type' => 'products', 'id' => $exGtin, 'attributes' => $attrs4]], $jf); +echo "REQ: {$payload4}\n"; +[$c, $b, $v] = gs1v('PUT', $base . '/products/' . $exGtin, $payload4, $login, $password); +echo "HTTP {$c}: {$b}\n\n"; + +// Step 5: Try application/vnd.api+json for both Content-Type and Accept +echo "--- Step 5: vnd.api+json content type ---\n"; +$curl5 = curl_init($base . '/products/' . $exGtin); +curl_setopt_array($curl5, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/vnd.api+json', 'Accept: application/vnd.api+json'], + CURLOPT_USERPWD => $login . ':' . $password, CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => $payload2, +]); +$resp5 = curl_exec($curl5); +$code5 = (int) curl_getinfo($curl5, CURLINFO_RESPONSE_CODE); +curl_close($curl5); +echo "HTTP {$code5}: {$resp5}\n\n"; + +// Step 6: Try with netContent as float explicitly +echo "--- Step 6: netContent as float 1.0 ---\n"; +$attrs6 = $attrs2; +$attrs6['netContent'] = 1.0; +$payload6 = json_encode(['data' => ['type' => 'products', 'id' => $exGtin, 'attributes' => $attrs6]], $jf); +// Force 1.0 in JSON (json_encode may output 1 for 1.0) +$payload6 = str_replace('"netContent":1,', '"netContent":1.0,', $payload6); +echo "REQ: {$payload6}\n"; +[$c, $b, $v] = gs1v('PUT', $base . '/products/' . $exGtin, $payload6, $login, $password); +echo "HTTP {$c}: {$b}\n\n"; + +// Step 7: Try completely empty attributes +echo "--- Step 7: Empty attributes ---\n"; +$payload7 = json_encode(['data' => ['type' => 'products', 'id' => $exGtin, 'attributes' => new \stdClass()]], $jf); +echo "REQ: {$payload7}\n"; +[$c, $b, $v] = gs1v('PUT', $base . '/products/' . $exGtin, $payload7, $login, $password); +echo "HTTP {$c}: {$b}\n\n"; + +// Step 8: Binary search - ONE field at a time +echo "--- Step 8: Single field tests ---\n"; +$singleFields = [ + 'brandName' => $exAttrs['brandName'], + 'commonName' => $exAttrs['commonName'], + 'gpcCode' => $exAttrs['gpcCode'], + 'netContent' => $exAttrs['netContent'], + 'netContentUnit' => $exAttrs['netContentUnit'], + 'status' => $exAttrs['status'], + 'targetMarket' => $exAttrs['targetMarket'], + 'descriptionLanguage' => $exAttrs['descriptionLanguage'], +]; +foreach ($singleFields as $key => $val) { + $p = json_encode(['data' => ['type' => 'products', 'id' => $exGtin, 'attributes' => [$key => $val]]], $jf); + [$c, $b, $v] = gs1v('PUT', $base . '/products/' . $exGtin, $p, $login, $password); + echo " {$key} => HTTP {$c}\n"; +} + +echo "\n=== DONE ===\n"; diff --git a/resources/lang/pl.php b/resources/lang/pl.php index e88b764..b46c503 100644 --- a/resources/lang/pl.php +++ b/resources/lang/pl.php @@ -20,9 +20,39 @@ return [ 'main_menu' => 'Menu glowne', 'users' => 'Uzytkownicy', 'products' => 'Produkty', + 'marketplace' => 'Marketplace', + 'cron' => 'Cron', 'dashboard' => 'Dashboard', 'settings' => 'Ustawienia', ], + 'marketplace' => [ + 'title' => 'Marketplace', + 'description' => 'Aktywne integracje i powiazane oferty marketplace.', + 'integrations_title' => 'Aktywne integracje', + 'offers_title' => 'Powiazane oferty: :name', + 'offers_description' => 'Lista ofert ze sklepu, ktore sa powiazane z produktami w orderPRO.', + 'empty_integrations' => 'Brak aktywnych integracji.', + 'empty_offers' => 'Brak powiazanych ofert dla tej integracji.', + 'fields' => [ + 'integration' => 'Integracja', + 'linked_offers_count' => 'Powiazane oferty', + 'offer_name' => 'Oferta', + 'external_product_id' => 'External product ID', + 'external_variant_id' => 'External variant ID', + 'external_offer_id' => 'External offer ID', + 'channel' => 'Kanal', + 'product' => 'Produkt orderPRO', + 'updated_at' => 'Ostatnia zmiana', + 'actions' => 'Akcje', + ], + 'actions' => [ + 'open_offers' => 'Pokaz oferty', + 'back_to_marketplace' => 'Wroc do Marketplace', + ], + 'flash' => [ + 'integration_not_found' => 'Nie znaleziono aktywnej integracji.', + ], + ], 'auth' => [ 'login' => [ 'title' => 'Logowanie', @@ -87,9 +117,16 @@ return [ 'delete_failed' => 'Nie udalo sie usunac produktu.', 'not_found' => 'Nie znaleziono wskazanego produktu.', ], + 'gs1' => [ + 'assign_ean' => 'Przypisz EAN z GS1', + 'ean_assigned' => 'EAN :ean zostal przypisany i zarejestrowany w GS1.', + 'already_has_ean' => 'Produkt ma juz przypisany EAN.', + 'error' => 'Blad podczas przypisywania EAN z GS1:', + ], 'actions' => [ 'add' => 'Dodaj produkt', 'import_shoppro' => 'Import z shopPRO', + 'export_shoppro' => 'Eksport do shopPRO', 'preview' => 'Podglad', 'links' => 'Powiazania', 'edit' => 'Edytuj', @@ -209,6 +246,10 @@ return [ 'yes' => 'Potwierdz', 'no' => 'Anuluj', ], + 'alerts' => [ + 'missing_remote_link' => 'Powiazanie nie istnieje juz po stronie zewnetrznej.', + 'alert_since' => '(alert od: :date)', + ], 'flash' => [ 'linked' => 'Powiazanie zostalo zapisane.', 'relinked' => 'Powiazanie zostalo przepiete.', @@ -241,6 +282,30 @@ return [ 'import_warning_title' => 'Ostrzezenie po imporcie wariantow', 'import_warning_date' => 'Data ostrzezenia', ], + 'export' => [ + 'title' => 'Eksport produktow do shopPRO', + 'close' => 'Zamknij', + 'integration' => 'Integracja', + 'integration_placeholder' => '-- wybierz integracje --', + 'mode' => 'Tryb eksportu', + 'mode_simple' => 'Produkt jako prosty', + 'mode_variant' => 'Produkt jako wariantowy', + 'mode_hint' => 'Tryb wariantowy eksportuje produkt nadrzedny i jego warianty po permutation_hash.', + 'selected_count_label' => 'Zaznaczone produkty', + 'selected_hint' => 'Wybierz rekordy checkboxami w tabeli produktow.', + 'select_column_label' => 'Wybierz produkty do eksportu', + 'no_integrations' => 'Brak aktywnych integracji shopPRO z kluczem API. Skonfiguruj je w Ustawienia -> Integracje shopPRO.', + 'submit' => 'Uruchom eksport', + 'flash' => [ + 'failed' => 'Eksport produktow zakonczyl sie bledem.', + 'integration_required' => 'Wybierz integracje do eksportu.', + 'integration_not_found' => 'Nie znaleziono wskazanej integracji.', + 'api_key_missing' => 'Wybrana integracja nie ma zapisanego klucza API.', + 'mode_invalid' => 'Niepoprawny tryb eksportu.', + 'no_products_selected' => 'Zaznacz co najmniej jeden produkt do eksportu.', + 'done' => 'Eksport (:mode) zakonczony. Sukces: :exported, bledy: :failed.', + ], + ], 'import' => [ 'title' => 'Import produktow z shopPRO', 'close' => 'Zamknij', @@ -368,5 +433,63 @@ return [ 'action' => 'Importuj 1 produkt', ], ], + 'cron' => [ + 'title' => 'Cron', + 'run_on_web_title' => 'Uruchamianie crona podczas nawigacji', + 'run_on_web_description' => 'Po wlaczeniu worker cron uruchamia sie automatycznie podczas poruszania po panelu.', + 'run_on_web_label' => 'Wlacz uruchamianie crona podczas requestow HTTP', + 'web_limit' => 'Limit jobow na jedno wywolanie', + 'schedules_title' => 'Harmonogramy (przyszle uruchomienia)', + 'future_jobs_title' => 'Kolejka przyszlych jobow', + 'past_jobs_title' => 'Historia jobow (przeszle)', + 'empty_schedules' => 'Brak harmonogramow.', + 'empty_future_jobs' => 'Brak zaplanowanych jobow w przyszlosci.', + 'empty_past_jobs' => 'Brak historii jobow.', + 'enabled' => [ + 'yes' => 'Tak', + 'no' => 'Nie', + ], + 'fields' => [ + 'job_type' => 'Typ joba', + 'enabled' => 'Aktywny', + 'interval' => 'Interwal (sek)', + 'priority' => 'Priorytet', + 'next_run_at' => 'Nastepne uruchomienie', + 'last_run_at' => 'Ostatnie uruchomienie', + 'status' => 'Status', + 'scheduled_at' => 'Zaplanowano', + 'attempts' => 'Proby', + 'completed_at' => 'Zakonczenie', + 'last_error' => 'Ostatni blad', + ], + 'actions' => [ + 'save' => 'Zapisz ustawienia', + ], + 'flash' => [ + 'saved' => 'Ustawienia crona zostaly zapisane.', + 'save_failed' => 'Nie udalo sie zapisac ustawien crona.', + 'load_failed' => 'Nie udalo sie pobrac danych crona.', + ], + ], + 'gs1' => [ + 'title' => 'GS1 / EAN', + 'description' => 'Konfiguracja polaczenia z API MojeGS1 do automatycznego przypisywania kodow EAN.', + 'fields' => [ + 'api_login' => 'Login API', + 'api_password' => 'Haslo API', + 'prefix' => 'Prefiks GS1 (9 cyfr)', + 'default_brand' => 'Domyslna marka (brandName)', + 'default_gpc_code' => 'Domyslny kod GPC', + ], + 'actions' => [ + 'save' => 'Zapisz ustawienia GS1', + ], + 'flash' => [ + 'saved' => 'Ustawienia GS1 zostaly zapisane.', + 'save_failed' => 'Nie udalo sie zapisac ustawien GS1.', + ], + ], ], ]; + + diff --git a/resources/scss/app.scss b/resources/scss/app.scss index 5ea83ad..b747abf 100644 --- a/resources/scss/app.scss +++ b/resources/scss/app.scss @@ -69,6 +69,59 @@ a { background: #2e4f93; } +.sidebar__group { + display: grid; + gap: 6px; +} + +.sidebar__group-toggle { + list-style: none; + border-radius: 8px; + padding: 10px 12px; + color: #cbd5e1; + font-weight: 600; + cursor: pointer; +} + +.sidebar__group-toggle::-webkit-details-marker { + display: none; +} + +.sidebar__group:hover .sidebar__group-toggle { + color: #f8fafc; + background: #1b2a3f; +} + +.sidebar__group.is-active .sidebar__group-toggle { + color: #ffffff; + background: #2e4f93; +} + +.sidebar__group-links { + display: grid; + gap: 4px; + padding-left: 8px; +} + +.sidebar__sublink { + border-radius: 8px; + padding: 8px 10px; + text-decoration: none; + color: #cbd5e1; + font-size: 13px; + font-weight: 500; +} + +.sidebar__sublink:hover { + color: #f8fafc; + background: #1b2a3f; +} + +.sidebar__sublink.is-active { + color: #ffffff; + background: #2e4f93; +} + .app-main { min-width: 0; } @@ -507,6 +560,21 @@ a { 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; +} .table-inline-action { display: inline-block; margin-right: 6px; @@ -681,6 +749,27 @@ a { 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; @@ -801,3 +890,4 @@ a { width: min(92vw, 100%); } } + diff --git a/resources/views/components/table-list.php b/resources/views/components/table-list.php index 4a85fe4..1ffaf46 100644 --- a/resources/views/components/table-list.php +++ b/resources/views/components/table-list.php @@ -16,6 +16,10 @@ $emptyMessage = (string) ($config['empty_message'] ?? 'Brak rekordow.'); $showActions = (bool) ($config['show_actions'] ?? true); $actionsLabel = (string) ($config['actions_label'] ?? 'Akcje'); $listKey = (string) ($config['list_key'] ?? md5($basePath !== '' ? $basePath : 'table-list')); +$selectable = (bool) ($config['selectable'] ?? false); +$selectName = (string) ($config['select_name'] ?? 'selected_ids[]'); +$selectValueKey = (string) ($config['select_value_key'] ?? 'id'); +$selectColumnLabel = (string) ($config['select_column_label'] ?? 'Wybierz'); $currentSort = (string) ($query['sort'] ?? 'id'); $currentDir = strtoupper((string) ($query['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; @@ -164,6 +168,13 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string + + + $column): ?> $sortKey, 'sort_dir' => $nextDir, 'page' => 1])) ?>" class="table-sort-link"> - + - + @@ -197,13 +208,22 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string - + + + + + $column): ?> - « - + « + @@ -281,8 +301,8 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string - - » + + » @@ -409,6 +429,35 @@ $buildUrl = static function (array $params = []) use ($basePath, $query): string }); }); + var selectAll = root.querySelector('.js-table-select-all'); + var selectItems = Array.prototype.slice.call(root.querySelectorAll('.js-table-select-item')); + function syncSelectAllState() { + if (!selectAll) return; + if (selectItems.length === 0) { + selectAll.checked = false; + selectAll.indeterminate = false; + return; + } + + var checkedCount = selectItems.filter(function(input) { return input.checked; }).length; + selectAll.checked = checkedCount === selectItems.length; + selectAll.indeterminate = checkedCount > 0 && checkedCount < selectItems.length; + } + + if (selectAll) { + selectAll.addEventListener('change', function() { + selectItems.forEach(function(input) { + input.checked = selectAll.checked; + }); + syncSelectAllState(); + }); + } + + selectItems.forEach(function(input) { + input.addEventListener('change', syncSelectAllState); + }); + syncSelectAllState(); + var clearBtn = root.querySelector('.js-table-filters-clear'); if (clearBtn) { clearBtn.addEventListener('click', function() { diff --git a/resources/views/layouts/app.php b/resources/views/layouts/app.php index b8662d9..b383492 100644 --- a/resources/views/layouts/app.php +++ b/resources/views/layouts/app.php @@ -12,6 +12,8 @@ + +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
ID
+ + + +
+ + + + diff --git a/resources/views/marketplace/offers.php b/resources/views/marketplace/offers.php new file mode 100644 index 0000000..90f7d25 --- /dev/null +++ b/resources/views/marketplace/offers.php @@ -0,0 +1,58 @@ + + + +
+

(string) ($integrationData['name'] ?? '')])) ?>

+

+
+ +
+ + + + + + + +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SKUEAN
+ + + +
+
+ +
+ diff --git a/resources/views/products/index.php b/resources/views/products/index.php index e09429e..a7921ba 100644 --- a/resources/views/products/index.php +++ b/resources/views/products/index.php @@ -68,11 +68,11 @@ $integrations = is_array($shopProIntegrations ?? null) ? $shopProIntegrations :
@@ -97,6 +97,58 @@ $integrations = is_array($shopProIntegrations ?? null) ? $shopProIntegrations : + + + + + + + + + MojeGS1 + + + + + + + + +
+ + +